From feda81bf2f808e952ea1466755d85a438ce0e990 Mon Sep 17 00:00:00 2001 From: upstream-mirror Date: Wed, 23 Sep 2026 17:00:42 +0500 Subject: [PATCH] snapshot of https://github.com/tauri-apps/tauri.git (dev, 2026-09-23; blobs > 131072B omitted) --- .cargo/audit.toml | 14 + .cargo/config.toml | 4 + .changes/README.md | 44 + .changes/acl-deny-execution-context.md | 5 + .changes/additional-watch-folders-alias.md | 5 + .changes/allow-crlf-cargo-toml.md | 6 + .changes/android-compileroptions-dsl.md | 9 + .changes/android-consumer-proguard.md | 23 + .changes/android-graceful-exit.md | 5 + .changes/android-gradle-9.md | 8 + .changes/android-java-gradle-compat-hint.md | 6 + .changes/android-json-org-serialization.md | 5 + .changes/android-material3.md | 6 + .../android-plugin-manager-no-activity.md | 5 + .changes/android-plugin-manager-relaunch.md | 5 + .changes/android-target-sdk-37.md | 6 + .changes/android-video-dir.md | 7 + .changes/app-bound-domains.md | 8 + .changes/app-exit-api.md | 6 + .changes/appimage-respect-gdk-backend.md | 5 + .changes/asset-protocol-multi-range.md | 5 + .changes/asset-protocol-off-main-thread.md | 5 + .changes/brotli-9.md | 6 + .changes/build-config-path.md | 5 + .changes/build-windows-icon-config-root.md | 5 + .changes/bundle-vc-runtime.md | 8 + .changes/bundler-breaking-changes.md | 5 + .changes/bundler-show-sizes.md | 7 + .changes/cargo-toml-1.md | 6 + .changes/change-pr-15578.md | 5 + .changes/change-pr-15593.md | 5 + .changes/change-pr-15677.md | 5 + .changes/change-pr-15952.md | 6 + .../clarify-init-frontend-command-prompts.md | 6 + .changes/cleanup-js-menu-channel.md | 5 + .changes/cli-dev-kill-process-tree.md | 6 + .changes/cli-force-noninteractive.md | 6 + .changes/cli-gradlew-crlf.md | 6 + .changes/cli-help-text-fixes.md | 6 + .changes/cli-ios-sim-build-rebuild.md | 6 + .changes/config.json | 224 ++ .changes/convert-file-src.md | 5 + .changes/ctor-1.md | 5 + .changes/default-product-name-warning.md | 7 + .changes/disable-binary-patching.md | 7 + .changes/dpi-type-wrappers.md | 5 + .changes/edition-2024-template.md | 6 + .changes/edition-2024.md | 16 + .changes/event-listener-cleanup-on-destroy.md | 5 + .changes/exit-with-code.md | 6 + .changes/explicit-resource-management.md | 15 + ...expose-set-activate-ignoring-other-apps.md | 7 + .changes/fix-appimage-xdg-utils.md | 7 + .../fix-capabilities-rerun-absolute-path.md | 5 + .../fix-channel-data-queue-webview-binding.md | 7 + .changes/fix-cli-add-npm-frontend-dir.md | 6 + .changes/fix-copy-new-resource-files.md | 6 + .../fix-deterministic-config-serialization.md | 5 + ...x-deterministic-embedded-assets-codegen.md | 5 + .changes/fix-ico-broken-in-task-manager.md | 6 + .changes/fix-ico-largest-entry.md | 5 + .../fix-js-listeners-leak-on-webview-close.md | 5 + .../fix-js-set-background-color-arguments.md | 5 + .changes/fix-monitor-queries-off-thread.md | 6 + .changes/fix-objc-retain-leak.md | 6 + .changes/fix-send-sync.md | 5 + .changes/fix-unlisten-guard-missing-entry.md | 5 + .changes/fix-wix-external-bin-id.md | 7 + .changes/fix-wix-resource-target-name.md | 8 + ...header-source-map-deterministic-display.md | 5 + .changes/icon-non-square-fit.md | 6 + .changes/image-from-resource.md | 7 + .changes/image-icon-zero-copy.md | 5 + .changes/image-rgba-perf.md | 5 + .changes/image-rgba-uint8array-generic.md | 5 + .changes/ios-27.md | 6 + .changes/ios-monitor-off-main-thread.md | 5 + .changes/js-image-type-alias.md | 5 + .changes/lock-minor-versions.md | 12 + .changes/macos-app-set-theme-window-theme.md | 5 + .changes/macos-liquid-glass.md | 7 + .changes/menu-command-panic-on-wrong-input.md | 9 + .changes/mobile-init-pnpm-native.md | 6 + .changes/mobile-resumed-suspended-event.md | 6 + .../mock-runtime-run-main-thread-deadlock.md | 5 + .changes/msrv-1.90.md | 16 + .changes/no-redirection-bitmap-api.md | 5 + .changes/no-redirection-bitmap-config.md | 6 + .changes/no-redirection-bitmap.md | 7 + .changes/nsis-restart-manager.md | 5 + .changes/objc2-0.3.2.md | 7 + .changes/path-normalize-empty.md | 5 + .changes/permission-handler.md | 9 + .changes/permissions-policy-header-name.md | 5 + .changes/plugin-cleanup-before-exit.md | 5 + .changes/plugin-template-ts7.md | 6 + .changes/png-0.18.md | 5 + ...olved-command-reference-debug-assertion.md | 8 + .changes/runtime-handle-monitor-fns.md | 6 + .changes/runtime-wry-avoid-clones.md | 5 + .changes/runtime-wry-windows-store-debug.md | 5 + .changes/set-fullscreen-on-monitor.md | 8 + .changes/signing-env-vars.md | 6 + .changes/static-vc-runtime.md | 7 + .changes/swift-27.md | 6 + .changes/target-dir-nightly-out-dir.md | 5 + .changes/tauri-build-static-vc-runtime.md | 5 + .changes/tauri-info-skip-rust-only-js.md | 6 + .changes/unstable-webview-focus.md | 6 + .changes/update-dom-query.md | 5 + .changes/updater-sign-version.md | 10 + .changes/validate-inline-capabilities.md | 5 + .changes/wait-async-window-creation.md | 5 + .changes/webkitgtk-resize-cursor.md | 6 + .changes/webview-focus-on-move.md | 6 + .changes/window-effect-non-exhaustive.md | 5 + .changes/windows-0.62.md | 7 + ...dows-multiwebview-cached-focused-states.md | 6 + .changes/wry-focused-webview.md | 5 + .changes/xcode-27.md | 6 + .devcontainer/Dockerfile | 12 + .devcontainer/README.md | 39 + .devcontainer/devcontainer.json | 30 + .docker/cross/aarch64.Dockerfile | 44 + .docker/cross/cmake.sh | 31 + .docker/cross/common.sh | 40 + .docker/cross/deny-debian-packages.sh | 19 + .docker/cross/dropbear.sh | 56 + .docker/cross/lib.sh | 45 + .docker/cross/linux-image.sh | 274 ++ .docker/cross/linux-runner | 173 + .docker/cross/qemu.sh | 233 ++ .docker/cross/xargo.sh | 28 + .editorconfig | 9 + .github/CODEOWNERS | 8 + .github/CODE_OF_CONDUCT.md | 132 + .github/CONTRIBUTING.md | 120 + .github/FUNDING.yml | 12 + .github/ISSUE_TEMPLATE/bug_report.yml | 63 + .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/docs_report.md | 7 + .github/ISSUE_TEMPLATE/feature_request.yml | 39 + .github/PULL_REQUEST_TEMPLATE.md | 21 + .github/RELEASING.md | 57 + .github/icon.png | Bin 0 -> 10955 bytes .github/splash.png | Bin 0 -> 60411 bytes .github/sponsors/crabnebula.svg | 31 + .github/workflows/audit.yml | 55 + .github/workflows/bench.yml | 120 + .github/workflows/check-change-tags.yml | 47 + .github/workflows/check-generated-files.yml | 85 + .github/workflows/check-license-header.yml | 31 + .../workflows/covector-comment-on-fork.yml | 30 + .github/workflows/covector-status.yml | 25 + .../workflows/covector-version-or-publish.yml | 150 + .github/workflows/deploy-schema-worker.yml | 28 + .github/workflows/docker.yml | 133 + .github/workflows/fmt.yml | 54 + .github/workflows/lint-js.yml | 45 + .github/workflows/lint-rust.yml | 89 + .github/workflows/publish-cli-js.yml | 289 ++ .github/workflows/publish-cli-rs.yml | 119 + .github/workflows/supply-chain.yml | 48 + .github/workflows/test-android.yml | 108 + .github/workflows/test-api-e2e-mobile.yml | 243 ++ .github/workflows/test-api-e2e.yml | 189 + .github/workflows/test-cli-js.yml | 72 + .github/workflows/test-cli-rs.yml | 78 + .github/workflows/test-core.yml | 139 + .github/workflows/udeps.yml | 140 + .gitignore | 55 + .prettierignore | 40 + .prettierrc | 6 + .scripts/ci/check-change-tags.js | 81 + .scripts/ci/check-license-header.js | 123 + .scripts/ci/collect-macos-crash-reports.sh | 50 + .scripts/ci/has-diff.sh | 15 + .scripts/ci/pack-cli.sh | 23 + .scripts/ci/sync-cli-metadata.js | 74 + .vscode/extensions.json | 8 + ARCHITECTURE.md | 193 + Cargo.toml | 92 + LICENSE-APACHE-2.0 | 177 + LICENSE-MIT | 21 + LICENSE.spdx | 20 + README.md | 96 + SECURITY.md | 28 + bench/Cargo.toml | 27 + bench/README.md | 38 + bench/src/build_benchmark_jsons.rs | 69 + bench/src/run_benchmark.rs | 394 ++ bench/src/utils.rs | 275 ++ bench/tests/cpu_intensive/public/index.css | 8 + bench/tests/cpu_intensive/public/index.html | 16 + bench/tests/cpu_intensive/public/site.js | 57 + bench/tests/cpu_intensive/public/worker.js | 49 + .../tests/cpu_intensive/src-tauri/.gitignore | 3 + .../tests/cpu_intensive/src-tauri/Cargo.toml | 19 + bench/tests/cpu_intensive/src-tauri/build.rs | 7 + .../tests/cpu_intensive/src-tauri/src/main.rs | 18 + .../cpu_intensive/src-tauri/tauri.conf.json | 31 + bench/tests/files_transfer/public/index.html | 30 + .../tests/files_transfer/src-tauri/.gitignore | 3 + .../tests/files_transfer/src-tauri/Cargo.toml | 19 + bench/tests/files_transfer/src-tauri/build.rs | 7 + .../files_transfer/src-tauri/src/main.rs | 31 + .../files_transfer/src-tauri/tauri.conf.json | 31 + bench/tests/helloworld/public/index.html | 17 + bench/tests/helloworld/src-tauri/.gitignore | 3 + bench/tests/helloworld/src-tauri/Cargo.toml | 19 + bench/tests/helloworld/src-tauri/build.rs | 7 + bench/tests/helloworld/src-tauri/src/main.rs | 18 + .../helloworld/src-tauri/tauri.conf.json | 33 + crates/tauri-build/CHANGELOG.md | 974 +++++ crates/tauri-build/Cargo.toml | 55 + crates/tauri-build/LICENSE-APACHE-2.0 | 177 + crates/tauri-build/LICENSE-MIT | 21 + crates/tauri-build/README.md | 42 + crates/tauri-build/src/acl.rs | 479 +++ crates/tauri-build/src/codegen/context.rs | 177 + crates/tauri-build/src/codegen/mod.rs | 5 + crates/tauri-build/src/lib.rs | 1099 ++++++ crates/tauri-build/src/manifest.rs | 205 ++ crates/tauri-build/src/mobile.rs | 201 ++ crates/tauri-build/src/static_vcruntime.rs | 58 + .../tauri-build/src/windows-app-manifest.xml | 14 + crates/tauri-bundler/CHANGELOG.md | 1570 ++++++++ crates/tauri-bundler/Cargo.toml | 85 + crates/tauri-bundler/License-Apache.md | 192 + crates/tauri-bundler/License-MIT.md | 31 + crates/tauri-bundler/README.md | 127 + crates/tauri-bundler/src/bundle.rs | 393 ++ crates/tauri-bundler/src/bundle/category.rs | 471 +++ crates/tauri-bundler/src/bundle/kmp/mod.rs | 59 + .../appimage/linuxdeploy-plugin-gstreamer.sh | 165 + .../linux/appimage/linuxdeploy-plugin-gtk.sh | 326 ++ .../src/bundle/linux/appimage/linuxdeploy.rs | 272 ++ .../src/bundle/linux/appimage/mod.rs | 25 + .../tauri-bundler/src/bundle/linux/debian.rs | 403 +++ .../src/bundle/linux/freedesktop/main.desktop | 14 + .../src/bundle/linux/freedesktop/mod.rs | 186 + crates/tauri-bundler/src/bundle/linux/mod.rs | 9 + crates/tauri-bundler/src/bundle/linux/rpm.rs | 243 ++ crates/tauri-bundler/src/bundle/macos/app.rs | 684 ++++ .../src/bundle/macos/dmg/bundle_dmg | 638 ++++ .../macos/dmg/eula-resources-template.xml | 105 + .../tauri-bundler/src/bundle/macos/dmg/mod.rs | 213 ++ .../src/bundle/macos/dmg/template.applescript | 74 + crates/tauri-bundler/src/bundle/macos/icon.rs | 326 ++ crates/tauri-bundler/src/bundle/macos/ios.rs | 211 ++ crates/tauri-bundler/src/bundle/macos/mod.rs | 10 + crates/tauri-bundler/src/bundle/macos/sign.rs | 165 + crates/tauri-bundler/src/bundle/platform.rs | 134 + crates/tauri-bundler/src/bundle/settings.rs | 1369 +++++++ .../src/bundle/updater_bundle.rs | 291 ++ .../tauri-bundler/src/bundle/windows/mod.rs | 16 + .../windows/msi/default-locale-strings.xml | 6 + .../src/bundle/windows/msi/install-task.ps1 | 28 + .../src/bundle/windows/msi/languages.json | 154 + .../src/bundle/windows/msi/main.wxs | 381 ++ .../src/bundle/windows/msi/mod.rs | 1154 ++++++ .../src/bundle/windows/msi/uninstall-task.ps1 | 23 + .../src/bundle/windows/msi/update-task.xml | 43 + .../bundle/windows/nsis/FileAssociation.nsh | 135 + .../src/bundle/windows/nsis/installer.nsi | 978 +++++ .../bundle/windows/nsis/languages/Arabic.nsh | 27 + .../windows/nsis/languages/Bulgarian.nsh | 27 + .../bundle/windows/nsis/languages/Dutch.nsh | 27 + .../bundle/windows/nsis/languages/English.nsh | 27 + .../bundle/windows/nsis/languages/French.nsh | 27 + .../bundle/windows/nsis/languages/German.nsh | 27 + .../bundle/windows/nsis/languages/Hebrew.nsh | 27 + .../bundle/windows/nsis/languages/Italian.nsh | 27 + .../windows/nsis/languages/Japanese.nsh | 27 + .../bundle/windows/nsis/languages/Korean.nsh | 27 + .../windows/nsis/languages/Norwegian.nsh | 27 + .../bundle/windows/nsis/languages/Persian.nsh | 27 + .../windows/nsis/languages/Portuguese.nsh | 27 + .../windows/nsis/languages/PortugueseBR.nsh | 27 + .../bundle/windows/nsis/languages/Russian.nsh | 27 + .../windows/nsis/languages/SimpChinese.nsh | 27 + .../bundle/windows/nsis/languages/Spanish.nsh | 27 + .../nsis/languages/SpanishInternational.nsh | 27 + .../bundle/windows/nsis/languages/Swedish.nsh | 27 + .../windows/nsis/languages/TradChinese.nsh | 27 + .../bundle/windows/nsis/languages/Turkish.nsh | 27 + .../windows/nsis/languages/Ukrainian.nsh | 27 + .../windows/nsis/languages/Vietnamese.nsh | 27 + .../src/bundle/windows/nsis/mod.rs | 952 +++++ .../src/bundle/windows/nsis/utils.nsh | 188 + .../tauri-bundler/src/bundle/windows/sign.rs | 284 ++ .../tauri-bundler/src/bundle/windows/util.rs | 247 ++ crates/tauri-bundler/src/error.rs | 277 ++ crates/tauri-bundler/src/lib.rs | 33 + crates/tauri-bundler/src/utils/fs_utils.rs | 240 ++ crates/tauri-bundler/src/utils/http_utils.rs | 246 ++ crates/tauri-bundler/src/utils/mod.rs | 141 + crates/tauri-cli/Cargo.toml | 158 + crates/tauri-cli/ENVIRONMENT_VARIABLES.md | 60 + crates/tauri-cli/LICENSE-APACHE-2.0 | 177 + crates/tauri-cli/LICENSE-MIT | 21 + crates/tauri-cli/README.md | 42 + crates/tauri-cli/build.rs | 7 + crates/tauri-cli/metadata-v2.json | 9 + crates/tauri-cli/metadata.json | 8 + crates/tauri-cli/scripts/kill-children.sh | 12 + crates/tauri-cli/src/acl/capability/mod.rs | 28 + crates/tauri-cli/src/acl/capability/new.rs | 144 + crates/tauri-cli/src/acl/mod.rs | 42 + crates/tauri-cli/src/acl/permission/add.rs | 282 ++ crates/tauri-cli/src/acl/permission/ls.rs | 158 + crates/tauri-cli/src/acl/permission/mod.rs | 39 + crates/tauri-cli/src/acl/permission/new.rs | 124 + crates/tauri-cli/src/acl/permission/rm.rs | 208 ++ crates/tauri-cli/src/add.rs | 328 ++ crates/tauri-cli/src/build.rs | 279 ++ crates/tauri-cli/src/bundle.rs | 350 ++ crates/tauri-cli/src/completions.rs | 103 + crates/tauri-cli/src/dev.rs | 374 ++ crates/tauri-cli/src/dev/auto-reload.js | 30 + .../tauri-cli/src/dev/builtin_dev_server.rs | 169 + crates/tauri-cli/src/error.rs | 105 + crates/tauri-cli/src/helpers/app_paths.rs | 167 + crates/tauri-cli/src/helpers/cargo.rs | 99 + .../tauri-cli/src/helpers/cargo_manifest.rs | 209 ++ crates/tauri-cli/src/helpers/config.rs | 362 ++ crates/tauri-cli/src/helpers/flock.rs | 374 ++ crates/tauri-cli/src/helpers/framework.rs | 131 + crates/tauri-cli/src/helpers/fs.rs | 55 + crates/tauri-cli/src/helpers/http.rs | 26 + crates/tauri-cli/src/helpers/icns.json | 52 + crates/tauri-cli/src/helpers/mod.rs | 152 + crates/tauri-cli/src/helpers/npm.rs | 459 +++ crates/tauri-cli/src/helpers/pbxproj.rs | 327 ++ crates/tauri-cli/src/helpers/plist.rs | 42 + crates/tauri-cli/src/helpers/plugins.rs | 98 + crates/tauri-cli/src/helpers/prompts.rs | 61 + crates/tauri-cli/src/helpers/template.rs | 97 + .../src/helpers/updater_signature.rs | 297 ++ crates/tauri-cli/src/icon.rs | 1160 ++++++ crates/tauri-cli/src/info/app.rs | 51 + crates/tauri-cli/src/info/env_nodejs.rs | 55 + crates/tauri-cli/src/info/env_rust.rs | 95 + crates/tauri-cli/src/info/env_system.rs | 359 ++ crates/tauri-cli/src/info/ios.rs | 27 + crates/tauri-cli/src/info/mod.rs | 349 ++ crates/tauri-cli/src/info/packages_nodejs.rs | 199 + crates/tauri-cli/src/info/packages_rust.rs | 100 + crates/tauri-cli/src/info/plugins.rs | 170 + crates/tauri-cli/src/init.rs | 465 +++ crates/tauri-cli/src/inspect.rs | 57 + crates/tauri-cli/src/interface/mod.rs | 97 + crates/tauri-cli/src/interface/rust.rs | 1968 ++++++++++ .../src/interface/rust/cargo_config.rs | 149 + .../tauri-cli/src/interface/rust/desktop.rs | 462 +++ .../src/interface/rust/installation.rs | 39 + .../tauri-cli/src/interface/rust/manifest.rs | 507 +++ crates/tauri-cli/src/lib.rs | 444 +++ crates/tauri-cli/src/main.rs | 43 + .../tauri-cli/src/migrate/migrations/mod.rs | 6 + .../src/migrate/migrations/v1/config.rs | 1260 +++++++ .../v1/fixtures/api-example.tauri.conf.json | 136 + .../v1/fixtures/cli-template.tauri.conf.json | 65 + .../src/migrate/migrations/v1/frontend.rs | 736 ++++ .../v1/frontend/partial_loader/mod.rs | 69 + .../v1/frontend/partial_loader/svelte.rs | 93 + .../v1/frontend/partial_loader/vue.rs | 246 ++ .../src/migrate/migrations/v1/manifest.rs | 415 +++ .../src/migrate/migrations/v1/mod.rs | 34 + .../src/migrate/migrations/v2_beta.rs | 194 + crates/tauri-cli/src/migrate/mod.rs | 72 + .../mobile/android/android_studio_script.rs | 334 ++ crates/tauri-cli/src/mobile/android/build.rs | 342 ++ .../android/check_java_gradle_versions.rs | 156 + crates/tauri-cli/src/mobile/android/dev.rs | 413 +++ crates/tauri-cli/src/mobile/android/mod.rs | 1233 +++++++ .../tauri-cli/src/mobile/android/project.rs | 209 ++ crates/tauri-cli/src/mobile/android/run.rs | 184 + crates/tauri-cli/src/mobile/init.rs | 414 +++ crates/tauri-cli/src/mobile/ios/build.rs | 665 ++++ crates/tauri-cli/src/mobile/ios/dev.rs | 407 +++ crates/tauri-cli/src/mobile/ios/mod.rs | 704 ++++ crates/tauri-cli/src/mobile/ios/project.rs | 239 ++ crates/tauri-cli/src/mobile/ios/run.rs | 144 + .../tauri-cli/src/mobile/ios/xcode_script.rs | 354 ++ crates/tauri-cli/src/mobile/mod.rs | 642 ++++ crates/tauri-cli/src/plugin/android.rs | 141 + crates/tauri-cli/src/plugin/init.rs | 329 ++ crates/tauri-cli/src/plugin/ios.rs | 155 + crates/tauri-cli/src/plugin/mod.rs | 96 + crates/tauri-cli/src/plugin/new.rs | 88 + crates/tauri-cli/src/remove.rs | 59 + crates/tauri-cli/src/signer/generate.rs | 77 + crates/tauri-cli/src/signer/mod.rs | 36 + crates/tauri-cli/src/signer/sign.rs | 121 + crates/tauri-cli/tauri-dev-watcher.gitignore | 5 + crates/tauri-cli/tauri.gitignore | 3 + .../templates/app/src-tauri/.gitignore | 4 + .../app/src-tauri/Cargo.crate-manifest | 31 + .../templates/app/src-tauri/build.rs | 3 + .../app/src-tauri/capabilities/default.json | 11 + .../templates/app/src-tauri/icons/128x128.png | Bin 0 -> 11059 bytes .../app/src-tauri/icons/128x128@2x.png | Bin 0 -> 23137 bytes .../templates/app/src-tauri/icons/32x32.png | Bin 0 -> 2225 bytes .../app/src-tauri/icons/Square107x107Logo.png | Bin 0 -> 9202 bytes .../app/src-tauri/icons/Square142x142Logo.png | Bin 0 -> 12530 bytes .../app/src-tauri/icons/Square150x150Logo.png | Bin 0 -> 13032 bytes .../app/src-tauri/icons/Square284x284Logo.png | Bin 0 -> 25943 bytes .../app/src-tauri/icons/Square30x30Logo.png | Bin 0 -> 2078 bytes .../app/src-tauri/icons/Square310x310Logo.png | Bin 0 -> 28507 bytes .../app/src-tauri/icons/Square44x44Logo.png | Bin 0 -> 3419 bytes .../app/src-tauri/icons/Square71x71Logo.png | Bin 0 -> 6027 bytes .../app/src-tauri/icons/Square89x89Logo.png | Bin 0 -> 7551 bytes .../app/src-tauri/icons/StoreLogo.png | Bin 0 -> 3971 bytes .../templates/app/src-tauri/icons/icon.ico | Bin 0 -> 37710 bytes .../templates/app/src-tauri/icons/icon.png | Bin 0 -> 49979 bytes .../templates/app/src-tauri/src/lib.rs | 16 + .../templates/app/src-tauri/src/main.rs | 6 + .../templates/app/src-tauri/tauri.conf.json | 1 + .../templates/mobile/android/.editorconfig | 12 + .../templates/mobile/android/.gitignore | 20 + .../templates/mobile/android/app/.gitignore | 6 + .../mobile/android/app/build.gradle.kts | 91 + .../mobile/android/app/proguard-rules.pro | 21 + .../android/app/src/main/AndroidManifest.xml | 37 + .../android/app/src/main/MainActivity.kt | 11 + .../drawable-v24/ic_launcher_foreground.xml | 30 + .../res/drawable/ic_launcher_background.xml | 170 + .../app/src/main/res/layout/activity_main.xml | 18 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3524 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 14102 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3524 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 3377 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 9081 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3377 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 7971 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 18900 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7971 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 12392 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 29506 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 12392 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 16751 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 40510 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16751 bytes .../app/src/main/res/values-night/themes.xml | 6 + .../app/src/main/res/values/colors.xml | 10 + .../app/src/main/res/values/strings.xml | 4 + .../app/src/main/res/values/themes.xml | 6 + .../app/src/main/res/xml/file_paths.xml | 5 + .../templates/mobile/android/build.gradle.kts | 24 + .../mobile/android/buildSrc/build.gradle.kts | 23 + .../buildSrc/src/main/kotlin/BuildTask.kt | 75 + .../buildSrc/src/main/kotlin/RustPlugin.kt | 86 + .../mobile/android/gradle.properties | 26 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48462 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + .../templates/mobile/android/gradlew | 248 ++ .../templates/mobile/android/gradlew.bat | 82 + .../templates/mobile/android/settings.gradle | 5 + .../tauri-cli/templates/mobile/ios/.gitignore | 3 + .../AppIcon.appiconset/AppIcon-20x20@1x.png | Bin 0 -> 1036 bytes .../AppIcon.appiconset/AppIcon-20x20@2x-1.png | Bin 0 -> 2216 bytes .../AppIcon.appiconset/AppIcon-20x20@2x.png | Bin 0 -> 2216 bytes .../AppIcon.appiconset/AppIcon-20x20@3x.png | Bin 0 -> 3512 bytes .../AppIcon.appiconset/AppIcon-29x29@1x.png | Bin 0 -> 1568 bytes .../AppIcon.appiconset/AppIcon-29x29@2x-1.png | Bin 0 -> 3394 bytes .../AppIcon.appiconset/AppIcon-29x29@2x.png | Bin 0 -> 3394 bytes .../AppIcon.appiconset/AppIcon-29x29@3x.png | Bin 0 -> 5228 bytes .../AppIcon.appiconset/AppIcon-40x40@1x.png | Bin 0 -> 2216 bytes .../AppIcon.appiconset/AppIcon-40x40@2x-1.png | Bin 0 -> 4794 bytes .../AppIcon.appiconset/AppIcon-40x40@2x.png | Bin 0 -> 4794 bytes .../AppIcon.appiconset/AppIcon-40x40@3x.png | Bin 0 -> 7288 bytes .../AppIcon.appiconset/AppIcon-512@2x.png | Bin 0 -> 121323 bytes .../AppIcon.appiconset/AppIcon-60x60@2x.png | Bin 0 -> 7288 bytes .../AppIcon.appiconset/AppIcon-60x60@3x.png | Bin 0 -> 11091 bytes .../AppIcon.appiconset/AppIcon-76x76@1x.png | Bin 0 -> 4515 bytes .../AppIcon.appiconset/AppIcon-76x76@2x.png | Bin 0 -> 9257 bytes .../AppIcon-83.5x83.5@2x.png | Bin 0 -> 10219 bytes .../AppIcon.appiconset/Contents.json | 116 + .../mobile/ios/Assets.xcassets/Contents.json | 6 + .../templates/mobile/ios/ExportOptions.plist | 8 + .../mobile/ios/LaunchScreen.storyboard | 30 + crates/tauri-cli/templates/mobile/ios/Podfile | 25 + .../Sources/{{app.name}}/bindings/bindings.h | 8 + .../mobile/ios/Sources/{{app.name}}/main.mm | 6 + .../templates/mobile/ios/project.yml | 174 + .../xcshareddata/WorkspaceSettings.xcsettings | 12 + .../plugin/.github/workflows/audit.yml | 34 + .../plugin/.github/workflows/clippy.yml | 55 + .../plugin/.github/workflows/test.yml | 35 + crates/tauri-cli/templates/plugin/.gitignore | 17 + .../templates/plugin/Cargo.crate-manifest | 17 + crates/tauri-cli/templates/plugin/README.md | 1 + .../plugin/__example-api/tauri-app/.gitignore | 24 + .../tauri-app/.vscode/extensions.json | 7 + .../plugin/__example-api/tauri-app/README.md | 8 + .../plugin/__example-api/tauri-app/index.html | 14 + .../__example-api/tauri-app/jsconfig.json | 34 + .../__example-api/tauri-app/package.json | 22 + .../__example-api/tauri-app/public/svelte.svg | 1 + .../__example-api/tauri-app/public/tauri.svg | 6 + .../__example-api/tauri-app/public/vite.svg | 1 + .../tauri-app/src-tauri/.gitignore | 4 + .../tauri-app/src-tauri/Cargo.crate-manifest | 23 + .../tauri-app/src-tauri/build.rs | 3 + .../src-tauri/capabilities/default.json | 12 + .../tauri-app/src-tauri/icons/128x128.png | Bin 0 -> 11059 bytes .../tauri-app/src-tauri/icons/128x128@2x.png | Bin 0 -> 23137 bytes .../tauri-app/src-tauri/icons/32x32.png | Bin 0 -> 2225 bytes .../tauri-app/src-tauri/icons/icon.ico | Bin 0 -> 37710 bytes .../tauri-app/src-tauri/icons/icon.png | Bin 0 -> 49979 bytes .../tauri-app/src-tauri/src/lib.rs | 14 + .../tauri-app/src-tauri/src/main.rs | 6 + .../tauri-app/src-tauri/tauri.conf.json | 37 + .../__example-api/tauri-app/src/App.svelte | 54 + .../tauri-app/src/lib/Greet.svelte | 22 + .../__example-api/tauri-app/src/main.js | 9 + .../__example-api/tauri-app/src/style.css | 102 + .../__example-api/tauri-app/src/vite-env.d.ts | 2 + .../__example-api/tauri-app/vite.config.js | 24 + .../plugin/__example-basic/vanilla/.gitignore | 1 + .../__example-basic/vanilla/package.json | 14 + .../__example-basic/vanilla/public/index.html | 5 + .../vanilla/src-tauri/.gitignore | 3 + .../vanilla/src-tauri/Cargo.crate-manifest | 22 + .../vanilla/src-tauri/build.rs | 3 + .../src-tauri/capabilities/default.json | 12 + .../vanilla/src-tauri/icons/128x128.png | Bin 0 -> 11059 bytes .../vanilla/src-tauri/icons/128x128@2x.png | Bin 0 -> 23137 bytes .../vanilla/src-tauri/icons/32x32.png | Bin 0 -> 2225 bytes .../vanilla/src-tauri/icons/icon.ico | Bin 0 -> 37710 bytes .../vanilla/src-tauri/icons/icon.png | Bin 0 -> 49979 bytes .../vanilla/src-tauri/rustfmt.toml | 14 + .../vanilla/src-tauri/src/lib.rs | 7 + .../vanilla/src-tauri/src/main.rs | 6 + .../vanilla/src-tauri/tauri.conf.json | 33 + .../templates/plugin/android/.gitignore | 2 + .../templates/plugin/android/build.gradle.kts | 40 + .../plugin/android/consumer-rules.pro | 21 + .../templates/plugin/android/settings.gradle | 31 + .../java/ExampleInstrumentedTest.kt | 24 + .../android/src/main/AndroidManifest.xml | 3 + .../plugin/android/src/main/java/Example.kt | 10 + .../android/src/main/java/ExamplePlugin.kt | 28 + .../android/src/test/java/ExampleUnitTest.kt | 17 + crates/tauri-cli/templates/plugin/build.rs | 8 + .../templates/plugin/guest-js/index.ts | 12 + .../templates/plugin/ios-spm/.gitignore | 10 + .../templates/plugin/ios-spm/Package.swift | 32 + .../templates/plugin/ios-spm/README.md | 3 + .../ios-spm/Sources/ExamplePlugin.swift | 20 + .../Tests/PluginTests/PluginTests.swift | 8 + .../tauri-cli/templates/plugin/package.json | 33 + .../templates/plugin/rollup.config.js | 31 + .../templates/plugin/src/commands.rs | 16 + .../tauri-cli/templates/plugin/src/desktop.rs | 25 + .../tauri-cli/templates/plugin/src/error.rs | 24 + crates/tauri-cli/templates/plugin/src/lib.rs | 51 + .../tauri-cli/templates/plugin/src/mobile.rs | 37 + .../tauri-cli/templates/plugin/src/models.rs | 16 + .../tauri-cli/templates/plugin/tsconfig.json | 15 + crates/tauri-cli/templates/tauri.conf.json | 40 + .../tests/fixtures/pbxproj/project.pbxproj | 467 +++ ...proj__tests__project-modified.pbxproj.snap | 472 +++ ...pers__pbxproj__tests__project.pbxproj.snap | 892 +++++ crates/tauri-cli/tests/init.rs | 54 + crates/tauri-codegen/CHANGELOG.md | 799 ++++ crates/tauri-codegen/Cargo.toml | 46 + crates/tauri-codegen/LICENSE-APACHE-2.0 | 177 + crates/tauri-codegen/LICENSE-MIT | 21 + crates/tauri-codegen/README.md | 43 + crates/tauri-codegen/src/context.rs | 493 +++ crates/tauri-codegen/src/embedded_assets.rs | 452 +++ crates/tauri-codegen/src/image.rs | 208 ++ crates/tauri-codegen/src/lib.rs | 152 + .../src/vendor/blake3_reference.rs | 378 ++ crates/tauri-codegen/src/vendor/mod.rs | 11 + crates/tauri-driver/CHANGELOG.md | 91 + crates/tauri-driver/Cargo.toml | 40 + crates/tauri-driver/LICENSE-APACHE-2.0 | 1 + crates/tauri-driver/LICENSE-MIT | 1 + crates/tauri-driver/LICENSE.spdx | 1 + crates/tauri-driver/README.md | 44 + crates/tauri-driver/src/cli.rs | 63 + crates/tauri-driver/src/main.rs | 52 + crates/tauri-driver/src/server.rs | 245 ++ crates/tauri-driver/src/webdriver.rs | 61 + crates/tauri-macos-sign/CHANGELOG.md | 74 + crates/tauri-macos-sign/Cargo.toml | 32 + crates/tauri-macos-sign/README.md | 3 + crates/tauri-macos-sign/src/certificate.rs | 71 + crates/tauri-macos-sign/src/keychain.rs | 254 ++ .../tauri-macos-sign/src/keychain/identity.rs | 120 + crates/tauri-macos-sign/src/lib.rs | 368 ++ .../src/provisioning_profile.rs | 64 + crates/tauri-macros/CHANGELOG.md | 864 +++++ crates/tauri-macros/Cargo.toml | 34 + crates/tauri-macros/LICENSE-APACHE-2.0 | 177 + crates/tauri-macros/LICENSE-MIT | 21 + crates/tauri-macros/README.md | 42 + crates/tauri-macros/src/command/handler.rs | 185 + crates/tauri-macros/src/command/mod.rs | 24 + crates/tauri-macros/src/command/wrapper.rs | 524 +++ crates/tauri-macros/src/context.rs | 169 + crates/tauri-macros/src/lib.rs | 394 ++ crates/tauri-macros/src/menu.rs | 136 + crates/tauri-macros/src/mobile.rs | 95 + crates/tauri-macros/src/runtime.rs | 105 + crates/tauri-plugin/CHANGELOG.md | 396 ++ crates/tauri-plugin/Cargo.toml | 41 + crates/tauri-plugin/src/build/mobile.rs | 231 ++ crates/tauri-plugin/src/build/mod.rs | 220 ++ crates/tauri-plugin/src/lib.rs | 85 + crates/tauri-plugin/src/runtime.rs | 3 + crates/tauri-runtime-wry/CHANGELOG.md | 1517 ++++++++ crates/tauri-runtime-wry/Cargo.toml | 77 + crates/tauri-runtime-wry/LICENSE-APACHE-2.0 | 177 + crates/tauri-runtime-wry/LICENSE-MIT | 21 + crates/tauri-runtime-wry/README.md | 44 + crates/tauri-runtime-wry/build.rs | 19 + crates/tauri-runtime-wry/src/dialog/mod.rs | 19 + .../tauri-runtime-wry/src/dialog/windows.rs | 124 + crates/tauri-runtime-wry/src/monitor/linux.rs | 18 + crates/tauri-runtime-wry/src/monitor/macos.rs | 36 + crates/tauri-runtime-wry/src/monitor/mod.rs | 37 + .../tauri-runtime-wry/src/monitor/windows.rs | 32 + .../src/undecorated_resizing.rs | 654 ++++ crates/tauri-runtime-wry/src/util.rs | 124 + crates/tauri-runtime-wry/src/webview.rs | 49 + .../src/webview_permissions.rs | 35 + crates/tauri-runtime-wry/src/window/linux.rs | 31 + crates/tauri-runtime-wry/src/window/macos.rs | 43 + crates/tauri-runtime-wry/src/window/mod.rs | 74 + .../tauri-runtime-wry/src/window/windows.rs | 68 + crates/tauri-runtime/CHANGELOG.md | 1101 ++++++ crates/tauri-runtime/Cargo.toml | 74 + crates/tauri-runtime/LICENSE-APACHE-2.0 | 177 + crates/tauri-runtime/LICENSE-MIT | 21 + crates/tauri-runtime/README.md | 44 + crates/tauri-runtime/build.rs | 19 + crates/tauri-runtime/src/dpi.rs | 60 + crates/tauri-runtime/src/lib.rs | 1034 ++++++ crates/tauri-runtime/src/monitor.rs | 21 + crates/tauri-runtime/src/webview.rs | 937 +++++ .../tauri-runtime/src/webview_permissions.rs | 161 + crates/tauri-runtime/src/window.rs | 670 ++++ crates/tauri-schema-generator/Cargo.toml | 15 + crates/tauri-schema-generator/README.md | 3 + crates/tauri-schema-generator/build.rs | 58 + .../schemas/capability.schema.json | 234 ++ .../schemas/permission.schema.json | 205 ++ .../schemas/scope.schema.json | 84 + crates/tauri-schema-generator/src/main.rs | 12 + crates/tauri-schema-worker/.cargo/config.toml | 5 + crates/tauri-schema-worker/.gitignore | 4 + crates/tauri-schema-worker/Cargo.toml | 27 + crates/tauri-schema-worker/README.md | 3 + crates/tauri-schema-worker/package.json | 13 + crates/tauri-schema-worker/src/config.rs | 195 + crates/tauri-schema-worker/src/lib.rs | 27 + crates/tauri-schema-worker/wrangler.toml | 16 + crates/tauri-utils/CHANGELOG.md | 1089 ++++++ crates/tauri-utils/Cargo.toml | 95 + crates/tauri-utils/LICENSE-APACHE-2.0 | 177 + crates/tauri-utils/LICENSE-MIT | 21 + crates/tauri-utils/README.md | 42 + crates/tauri-utils/src/acl/build.rs | 500 +++ crates/tauri-utils/src/acl/capability.rs | 457 +++ crates/tauri-utils/src/acl/identifier.rs | 310 ++ crates/tauri-utils/src/acl/manifest.rs | 190 + crates/tauri-utils/src/acl/mod.rs | 550 +++ crates/tauri-utils/src/acl/resolved.rs | 714 ++++ crates/tauri-utils/src/acl/schema.rs | 405 +++ crates/tauri-utils/src/acl/value.rs | 201 ++ crates/tauri-utils/src/assets.rs | 211 ++ crates/tauri-utils/src/build.rs | 169 + crates/tauri-utils/src/config/parse.rs | 398 ++ crates/tauri-utils/src/config_v1/mod.rs | 3157 ++++++++++++++++ crates/tauri-utils/src/config_v1/parse.rs | 255 ++ crates/tauri-utils/src/html.rs | 442 +++ crates/tauri-utils/src/html2.rs | 335 ++ crates/tauri-utils/src/io.rs | 47 + crates/tauri-utils/src/lib.rs | 407 +++ crates/tauri-utils/src/mime_type.rs | 154 + crates/tauri-utils/src/pattern/isolation.js | 154 + crates/tauri-utils/src/pattern/isolation.rs | 171 + crates/tauri-utils/src/pattern/mod.rs | 7 + crates/tauri-utils/src/platform.rs | 442 +++ .../src/platform/starting_binary.rs | 84 + crates/tauri-utils/src/plugin.rs | 88 + crates/tauri-utils/src/resources.rs | 724 ++++ crates/tauri-utils/src/tokens.rs | 175 + crates/tauri/.scripts/loop_qc.sh | 14 + crates/tauri/Cargo.toml | 278 ++ crates/tauri/LICENSE-APACHE-2.0 | 177 + crates/tauri/LICENSE-MIT | 21 + crates/tauri/README.md | 42 + crates/tauri/build.rs | 500 +++ .../mobile/android-codegen/TauriActivity.kt | 60 + crates/tauri/mobile/android/.gitignore | 1 + crates/tauri/mobile/android/build.gradle.kts | 48 + .../tauri/mobile/android/consumer-rules.pro | 41 + .../java/app/tauri/ExampleInstrumentedTest.kt | 28 + .../android/src/main/AndroidManifest.xml | 3 + .../src/main/java/app/tauri/AppPlugin.kt | 56 + .../src/main/java/app/tauri/FsUtils.kt | 208 ++ .../src/main/java/app/tauri/JniMethod.kt | 8 + .../android/src/main/java/app/tauri/Logger.kt | 85 + .../src/main/java/app/tauri/PathPlugin.kt | 132 + .../main/java/app/tauri/PermissionHelper.kt | 107 + .../main/java/app/tauri/PermissionState.kt | 21 + .../app/tauri/annotation/ActivityCallback.kt | 9 + .../java/app/tauri/annotation/InvokeArg.kt | 9 + .../java/app/tauri/annotation/Permission.kt | 19 + .../tauri/annotation/PermissionCallback.kt | 9 + .../java/app/tauri/annotation/PluginMethod.kt | 8 + .../java/app/tauri/annotation/TauriPlugin.kt | 19 + .../src/main/java/app/tauri/plugin/Channel.kt | 33 + .../plugin/InvalidPluginMethodException.kt | 11 + .../src/main/java/app/tauri/plugin/Invoke.kt | 93 + .../src/main/java/app/tauri/plugin/JSArray.kt | 45 + .../main/java/app/tauri/plugin/JSObject.kt | 152 + .../src/main/java/app/tauri/plugin/Plugin.kt | 536 +++ .../java/app/tauri/plugin/PluginHandle.kt | 178 + .../java/app/tauri/plugin/PluginManager.kt | 245 ++ .../java/app/tauri/plugin/PluginMethodData.kt | 16 + .../java/app/tauri/plugin/PluginResult.kt | 67 + .../test/java/app/tauri/ExampleUnitTest.kt | 21 + crates/tauri/mobile/ios-api/.gitignore | 10 + crates/tauri/mobile/ios-api/Package.swift | 40 + crates/tauri/mobile/ios-api/README.md | 3 + .../ios-api/Sources/Tauri/Channel.swift | 65 + .../mobile/ios-api/Sources/Tauri/Invoke.swift | 118 + .../ios-api/Sources/Tauri/JSTypes.swift | 123 + .../ios-api/Sources/Tauri/JsonValue.swift | 58 + .../mobile/ios-api/Sources/Tauri/Logger.swift | 139 + .../ios-api/Sources/Tauri/Plugin/Plugin.swift | 82 + .../mobile/ios-api/Sources/Tauri/Tauri.swift | 157 + .../ios-api/Sources/Tauri/UiUtils.swift | 15 + .../ios-api/Tests/TauriTests/TauriTests.swift | 15 + crates/tauri/mobile/proguard-tauri.pro | 5 + .../app/autogenerated/reference.md | 440 +++ .../event/autogenerated/reference.md | 124 + .../image/autogenerated/reference.md | 151 + .../menu/autogenerated/reference.md | 610 ++++ .../path/autogenerated/reference.md | 232 ++ .../resources/autogenerated/reference.md | 43 + .../tray/autogenerated/reference.md | 340 ++ .../webview/autogenerated/reference.md | 488 +++ .../window/autogenerated/reference.md | 2098 +++++++++++ crates/tauri/scripts/bundle.global.js | 1 + crates/tauri/scripts/core.js | 114 + crates/tauri/scripts/freeze_prototype.js | 5 + crates/tauri/scripts/init.js | 15 + crates/tauri/scripts/ipc-protocol.js | 91 + crates/tauri/scripts/ipc.js | 210 ++ crates/tauri/scripts/isolation.js | 17 + crates/tauri/scripts/pattern.js | 21 + .../tauri/scripts/process-ipc-message-fn.js | 46 + crates/tauri/src/app.rs | 2842 +++++++++++++++ crates/tauri/src/app/plugin.rs | 158 + crates/tauri/src/async_runtime.rs | 363 ++ crates/tauri/src/error.rs | 198 + crates/tauri/src/event/event-system-spec.md | 34 + crates/tauri/src/event/event_name.rs | 70 + crates/tauri/src/event/init.js | 10 + crates/tauri/src/event/listener.rs | 488 +++ crates/tauri/src/event/mod.rs | 258 ++ crates/tauri/src/event/plugin.rs | 82 + crates/tauri/src/image/mod.rs | 527 +++ crates/tauri/src/image/plugin.rs | 88 + crates/tauri/src/ios.rs | 49 + crates/tauri/src/ipc/authority.rs | 1485 ++++++++ crates/tauri/src/ipc/capability_builder.rs | 170 + crates/tauri/src/ipc/channel.rs | 530 +++ crates/tauri/src/ipc/command.rs | 332 ++ crates/tauri/src/ipc/format_callback.rs | 344 ++ crates/tauri/src/ipc/mod.rs | 606 ++++ crates/tauri/src/ipc/protocol.rs | 753 ++++ crates/tauri/src/lib.rs | 1267 +++++++ crates/tauri/src/manager/menu.rs | 100 + crates/tauri/src/manager/mod.rs | 1040 ++++++ crates/tauri/src/manager/tray.rs | 92 + crates/tauri/src/manager/webview.rs | 793 ++++ crates/tauri/src/manager/window.rs | 285 ++ crates/tauri/src/menu/builders/check.rs | 90 + crates/tauri/src/menu/builders/icon.rs | 130 + crates/tauri/src/menu/builders/menu.rs | 675 ++++ crates/tauri/src/menu/builders/mod.rs | 17 + crates/tauri/src/menu/builders/normal.rs | 68 + crates/tauri/src/menu/check.rs | 129 + crates/tauri/src/menu/icon.rs | 220 ++ crates/tauri/src/menu/menu.rs | 400 ++ crates/tauri/src/menu/mod.rs | 771 ++++ crates/tauri/src/menu/normal.rs | 117 + crates/tauri/src/menu/plugin.rs | 941 +++++ crates/tauri/src/menu/predefined.rs | 360 ++ crates/tauri/src/menu/submenu.rs | 419 +++ crates/tauri/src/path/android.rs | 187 + crates/tauri/src/path/desktop.rs | 296 ++ crates/tauri/src/path/init.js | 10 + crates/tauri/src/path/mod.rs | 386 ++ crates/tauri/src/path/plugin.rs | 315 ++ crates/tauri/src/pattern.rs | 87 + crates/tauri/src/plugin.rs | 1151 ++++++ crates/tauri/src/plugin/mobile.rs | 511 +++ crates/tauri/src/process.rs | 130 + crates/tauri/src/protocol/asset.rs | 309 ++ crates/tauri/src/protocol/isolation.rs | 112 + crates/tauri/src/protocol/mod.rs | 53 + crates/tauri/src/protocol/tauri.rs | 267 ++ crates/tauri/src/resources/mod.rs | 223 ++ crates/tauri/src/resources/plugin.rs | 31 + crates/tauri/src/scope/fs.rs | 739 ++++ crates/tauri/src/scope/mod.rs | 80 + crates/tauri/src/state.rs | 291 ++ crates/tauri/src/test/mock_runtime.rs | 1430 ++++++++ crates/tauri/src/test/mod.rs | 338 ++ crates/tauri/src/tray/mod.rs | 701 ++++ crates/tauri/src/tray/plugin.rs | 253 ++ crates/tauri/src/vibrancy/macos.rs | 120 + crates/tauri/src/vibrancy/mod.rs | 32 + crates/tauri/src/vibrancy/windows.rs | 53 + crates/tauri/src/webview/mod.rs | 2842 +++++++++++++++ crates/tauri/src/webview/plugin.rs | 255 ++ crates/tauri/src/webview/scripts/print.js | 7 + .../src/webview/scripts/toggle-devtools.js | 31 + .../tauri/src/webview/scripts/zoom-hotkey.js | 43 + crates/tauri/src/webview/webview_window.rs | 2879 +++++++++++++++ crates/tauri/src/window/mod.rs | 2625 ++++++++++++++ crates/tauri/src/window/plugin.rs | 351 ++ crates/tauri/src/window/scripts/drag.js | 129 + crates/tauri/test/api/test.txt | 1 + crates/tauri/test/api/test_binary | Bin 0 -> 43 bytes crates/tauri/test/fixture/config.json | 1 + crates/tauri/test/fixture/dist/index.html | 6 + .../test/fixture/isolation/dist/index.html | 6 + .../isolation/isolation-dist/index.html | 10 + .../fixture/isolation/isolation-dist/index.js | 8 + .../isolation/src-tauri/icons/icon.ico | Bin 0 -> 86642 bytes .../isolation/src-tauri/icons/icon.ico~dev | Bin 0 -> 57594 bytes .../isolation/src-tauri/icons/icon.png | Bin 0 -> 974 bytes .../isolation/src-tauri/tauri.conf.json | 27 + .../test/fixture/src-tauri/icons/icon.ico | Bin 0 -> 86642 bytes .../test/fixture/src-tauri/icons/icon.ico~dev | Bin 0 -> 57594 bytes .../test/fixture/src-tauri/icons/icon.png | Bin 0 -> 974 bytes .../test/fixture/src-tauri/tauri.conf.json | 22 + crates/tauri/test/fixture/test.txt | 0 .../fixture/archives/archive.linux.tar.gz | Bin 0 -> 342 bytes .../fixture/archives/archive.linux.tar.gz.sig | 1 + .../fixture/archives/archive.macos.tar.gz.sig | 1 + .../fixture/archives/archive.windows.zip | Bin 0 -> 778 bytes .../fixture/archives/archive.windows.zip.sig | 1 + .../updater/fixture/bad_signature/update.key | 1 + .../fixture/bad_signature/update.key.pub | 1 + .../updater/fixture/good_signature/update.key | 1 + .../fixture/good_signature/update.key.pub | 1 + crates/tests/acl/Cargo.toml | 16 + .../fixtures/capabilities/basic-ping/cap.toml | 4 + .../basic-ping/required-plugins.json | 1 + .../file-explorer-remote/cap.toml | 7 + .../required-plugins.json | 1 + .../capabilities/file-explorer/cap.toml | 4 + .../file-explorer/required-plugins.json | 1 + .../capabilities/multiwebview/cap.toml | 5 + .../multiwebview/required-plugins.json | 1 + .../multiwindow/cap-external.toml | 4 + .../capabilities/multiwindow/cap-main.json | 20 + .../multiwindow/required-plugins.json | 1 + .../platform-specific-permissions/cap.toml | 9 + .../required-plugins.json | 1 + .../capabilities/scope-extended/cap.json | 39 + .../scope-extended/required-plugins.json | 1 + .../acl/fixtures/capabilities/scope/cap.toml | 11 + .../capabilities/scope/required-plugins.json | 1 + .../acl/fixtures/plugins/fs/deny-home.toml | 6 + .../acl/fixtures/plugins/fs/move-tmp.toml | 6 + .../acl/fixtures/plugins/fs/read-dir.toml | 4 + .../plugins/fs/read-download-dir.toml | 4 + .../acl/fixtures/plugins/fs/read-file.toml | 4 + .../fixtures/plugins/fs/read-resources.toml | 8 + .../tests/acl/fixtures/plugins/fs/read.toml | 4 + .../tests/acl/fixtures/plugins/fs/scope.toml | 13 + .../tests/acl/fixtures/plugins/os/linux.toml | 7 + .../tests/acl/fixtures/plugins/os/macos.toml | 7 + .../acl/fixtures/plugins/os/open-browser.toml | 28 + .../acl/fixtures/plugins/os/windows.toml | 6 + .../tests/acl/fixtures/plugins/ping/ping.toml | 4 + .../acl_tests__tests__basic-ping.snap | 40 + ...cl_tests__tests__file-explorer-remote.snap | 411 +++ .../acl_tests__tests__file-explorer.snap | 81 + .../acl_tests__tests__multiwebview.snap | 91 + .../acl_tests__tests__multiwindow.snap | 358 ++ .../acl_tests__tests__scope-extended.snap | 297 ++ .../snapshots/acl_tests__tests__scope.snap | 247 ++ ..._tests__platform-specific-permissions.snap | 95 + ..._tests__platform-specific-permissions.snap | 68 + ..._tests__platform-specific-permissions.snap | 68 + crates/tests/acl/src/lib.rs | 82 + .../frameworks/test.framework/Headers | 1 + .../frameworks/test.framework/Modules | 1 + .../frameworks/test.framework/Resources | 1 + .../test.framework/Versions/A/Headers/test.h | 18 + .../Versions/A/Modules/module.modulemap | 6 + .../Versions/A/Resources/Info.plist | 46 + .../Versions/A/_CodeSignature/CodeResources | 142 + .../frameworks/test.framework/Versions/A/test | Bin 0 -> 35664 bytes .../test.framework/Versions/Current | 1 + crates/tests/restart/Cargo.toml | 15 + crates/tests/restart/LICENSE-APACHE-2.0 | 1 + crates/tests/restart/LICENSE-MIT | 1 + crates/tests/restart/LICENSE.spdx | 1 + crates/tests/restart/build.rs | 5 + crates/tests/restart/src/main.rs | 30 + crates/tests/restart/tests/restart.rs | 125 + dependabot.yml | 105 + examples/.icons/128x128.png | Bin 0 -> 11059 bytes examples/.icons/128x128@2x.png | Bin 0 -> 23137 bytes examples/.icons/32x32.png | Bin 0 -> 2225 bytes examples/.icons/AppIcon.icon/Assets/icon.png | Bin 0 -> 49979 bytes examples/.icons/AppIcon.icon/icon.json | 35 + examples/.icons/Square107x107Logo.png | Bin 0 -> 9202 bytes examples/.icons/Square142x142Logo.png | Bin 0 -> 12530 bytes examples/.icons/Square150x150Logo.png | Bin 0 -> 13032 bytes examples/.icons/Square284x284Logo.png | Bin 0 -> 25943 bytes examples/.icons/Square30x30Logo.png | Bin 0 -> 2078 bytes examples/.icons/Square310x310Logo.png | Bin 0 -> 28507 bytes examples/.icons/Square44x44Logo.png | Bin 0 -> 3419 bytes examples/.icons/Square71x71Logo.png | Bin 0 -> 6027 bytes examples/.icons/Square89x89Logo.png | Bin 0 -> 7551 bytes examples/.icons/StoreLogo.png | Bin 0 -> 3971 bytes examples/.icons/icon.ico | Bin 0 -> 37710 bytes examples/.icons/icon.png | Bin 0 -> 49979 bytes examples/.icons/tray_icon.png | Bin 0 -> 804 bytes .../.icons/tray_icon_with_transparency.ico | Bin 0 -> 4414 bytes .../.icons/tray_icon_with_transparency.png | Bin 0 -> 1015 bytes examples/README.md | 5 + examples/api/.gitignore | 2 + examples/api/.setup-cross.sh | 6 + examples/api/.taurignore | 3 + examples/api/README.md | 29 + examples/api/dist/.gitkeep | 0 examples/api/index.html | 14 + examples/api/isolation-dist/index.html | 10 + examples/api/isolation-dist/index.js | 7 + examples/api/package.json | 26 + examples/api/public/tauri_logo.png | Bin 0 -> 2810 bytes examples/api/src-tauri/.gitignore | 6 + examples/api/src-tauri/.taurignore | 1 + examples/api/src-tauri/Cargo.toml | 51 + examples/api/src-tauri/Cross.toml | 17 + examples/api/src-tauri/Info.plist | 17 + examples/api/src-tauri/build.rs | 56 + .../api/src-tauri/capabilities/.gitignore | 1 + examples/api/src-tauri/capabilities/main.json | 16 + .../api/src-tauri/capabilities/run-app.json | 75 + examples/api/src-tauri/locales/pt-BR.wxl | 6 + .../permissions/app-menu/default.toml | 3 + .../permissions/autogenerated/echo.toml | 11 + .../autogenerated/log_operation.toml | 11 + .../autogenerated/perform_request.toml | 11 + .../permissions/autogenerated/spam.toml | 11 + examples/api/src-tauri/src/cmd.rs | 63 + examples/api/src-tauri/src/lib.rs | 231 ++ examples/api/src-tauri/src/main.rs | 10 + examples/api/src-tauri/src/menu_plugin.rs | 50 + examples/api/src-tauri/src/tray.rs | 126 + .../src-tauri/tauri-plugin-sample/.gitignore | 1 + .../src-tauri/tauri-plugin-sample/Cargo.toml | 18 + .../tauri-plugin-sample/android/.gitignore | 2 + .../android/build.gradle.kts | 40 + .../android/consumer-rules.pro | 21 + .../android/settings.gradle | 31 + .../plugin/sample/ExampleInstrumentedTest.kt | 28 + .../android/src/main/AndroidManifest.xml | 4 + .../main/java/com/plugin/sample/Example.kt | 14 + .../java/com/plugin/sample/ExamplePlugin.kt | 57 + .../java/com/plugin/sample/ExampleUnitTest.kt | 21 + .../src-tauri/tauri-plugin-sample/api-iife.js | 7 + .../src-tauri/tauri-plugin-sample/build.rs | 13 + .../tauri-plugin-sample/ios/.gitignore | 10 + .../tauri-plugin-sample/ios/Package.swift | 35 + .../tauri-plugin-sample/ios/README.md | 3 + .../ios/Sources/ExamplePlugin.swift | 32 + .../ios/Tests/PluginTests/PluginTests.swift | 12 + .../permissions/.gitignore | 1 + .../autogenerated/commands/js_values.toml | 13 + .../autogenerated/commands/ping.toml | 13 + .../permissions/autogenerated/reference.md | 87 + .../permissions/global-scope.toml | 7 + .../permissions/ping-scoped.toml | 8 + .../tauri-plugin-sample/src/desktop.rs | 30 + .../tauri-plugin-sample/src/error.rs | 14 + .../src-tauri/tauri-plugin-sample/src/lib.rs | 101 + .../tauri-plugin-sample/src/mobile.rs | 45 + .../tauri-plugin-sample/src/models.rs | 24 + examples/api/src-tauri/tauri.conf.json | 132 + examples/api/src/App.svelte | 374 ++ examples/api/src/app.css | 86 + .../api/src/components/MenuBuilder.svelte | 293 ++ .../src/components/MenuItemComponent.svelte | 79 + examples/api/src/main.ts | 14 + examples/api/src/views/App.svelte | 43 + examples/api/src/views/Communication.svelte | 70 + examples/api/src/views/Menu.svelte | 126 + examples/api/src/views/Tray.svelte | 134 + examples/api/src/views/WebRTC.svelte | 72 + examples/api/src/views/Welcome.svelte | 62 + examples/api/src/views/Window.svelte | 996 +++++ examples/api/src/vite-env.d.ts | 6 + examples/api/svelte.config.js | 7 + examples/api/tsconfig.json | 15 + examples/api/unocss.config.js | 101 + examples/api/vite.config.js | 44 + examples/commands/README.md | 5 + examples/commands/commands.rs | 32 + examples/commands/index.html | 88 + examples/commands/main.rs | 276 ++ examples/commands/tauri.conf.json | 35 + examples/drag/README.md | 3 + examples/drag/index.html | 418 +++ examples/drag/main.rs | 24 + examples/drag/tauri.conf.json | 35 + examples/file-associations/README.md | 19 + examples/file-associations/index.html | 19 + examples/file-associations/package.json | 13 + .../src-tauri/.license_template | 3 + .../file-associations/src-tauri/Cargo.toml | 19 + .../file-associations/src-tauri/Info.plist | 21 + examples/file-associations/src-tauri/build.rs | 7 + .../file-associations/src-tauri/src/main.rs | 97 + .../src-tauri/tauri.conf.json | 57 + examples/helloworld/README.md | 3 + examples/helloworld/index.html | 34 + examples/helloworld/main.rs | 19 + examples/helloworld/tauri.conf.json | 35 + examples/isolation/README.md | 3 + examples/isolation/dist/index.html | 34 + examples/isolation/dist/linked.js | 1 + examples/isolation/isolation-dist/index.html | 10 + examples/isolation/isolation-dist/index.js | 8 + examples/isolation/main.rs | 19 + examples/isolation/tauri.conf.json | 43 + examples/multiwebview/README.md | 3 + examples/multiwebview/index.html | 11 + examples/multiwebview/main.rs | 62 + examples/multiwebview/tauri.conf.json | 25 + examples/multiwindow/README.md | 5 + examples/multiwindow/index.html | 62 + examples/multiwindow/main.rs | 35 + examples/multiwindow/tauri.conf.json | 46 + examples/resources/README.md | 39 + examples/resources/index.html | 32 + examples/resources/package.json | 7 + examples/resources/src-tauri/Cargo.toml | 16 + examples/resources/src-tauri/assets/index.js | 5 + examples/resources/src-tauri/build.rs | 7 + .../resources/src-tauri/capabilities/app.json | 6 + examples/resources/src-tauri/src/main.rs | 32 + examples/resources/src-tauri/tauri.conf.json | 37 + examples/run-return/README.md | 3 + examples/run-return/index.html | 11 + examples/run-return/main.rs | 21 + examples/run-return/tauri.conf.json | 34 + examples/splashscreen/README.md | 11 + examples/splashscreen/dist/icon.png | Bin 0 -> 14165 bytes examples/splashscreen/dist/index.html | 14 + examples/splashscreen/dist/splashscreen.html | 26 + examples/splashscreen/main.rs | 29 + examples/splashscreen/tauri.conf.json | 43 + examples/state/README.md | 5 + examples/state/index.html | 49 + examples/state/main.rs | 47 + examples/state/tauri.conf.json | 35 + examples/streaming/README.md | 9 + examples/streaming/index.html | 33 + examples/streaming/main.rs | 213 ++ examples/streaming/tauri.conf.json | 36 + package.json | 35 + packages/api-e2e/README.md | 196 + packages/api-e2e/package.json | 33 + packages/api-e2e/tauri.e2e.conf.json | 16 + packages/api-e2e/test/helpers/index.ts | 243 ++ packages/api-e2e/test/specs/app.spec.ts | 64 + packages/api-e2e/test/specs/core.spec.ts | 109 + packages/api-e2e/test/specs/dpi.spec.ts | 54 + packages/api-e2e/test/specs/event.spec.ts | 125 + packages/api-e2e/test/specs/image.spec.ts | 31 + packages/api-e2e/test/specs/menu.spec.ts | 88 + packages/api-e2e/test/specs/path.spec.ts | 62 + packages/api-e2e/test/specs/tray.spec.ts | 43 + packages/api-e2e/test/specs/webview.spec.ts | 44 + .../api-e2e/test/specs/webviewWindow.spec.ts | 64 + packages/api-e2e/test/specs/window.spec.ts | 295 ++ packages/api-e2e/test/types/global.d.ts | 17 + packages/api-e2e/tsconfig.json | 20 + packages/api-e2e/wdio.android.conf.ts | 10 + packages/api-e2e/wdio.conf.ts | 296 ++ packages/api-e2e/wdio.ios.conf.ts | 10 + packages/api-e2e/wdio.mobile.ts | 425 +++ packages/api/.gitignore | 2 + packages/api/CHANGELOG.md | 1056 ++++++ packages/api/LICENSE-APACHE-2.0 | 177 + packages/api/LICENSE-MIT | 21 + packages/api/README.md | 52 + packages/api/eslint.config.js | 61 + packages/api/package.json | 63 + packages/api/rollup.config.ts | 117 + packages/api/src/app.ts | 421 +++ packages/api/src/core.ts | 610 ++++ packages/api/src/dpi.ts | 476 +++ packages/api/src/event.ts | 312 ++ packages/api/src/global.d.ts | 64 + packages/api/src/image.ts | 243 ++ packages/api/src/index.ts | 52 + packages/api/src/menu.ts | 31 + packages/api/src/menu/base.ts | 164 + packages/api/src/menu/checkMenuItem.ts | 86 + packages/api/src/menu/iconMenuItem.ts | 211 ++ packages/api/src/menu/menu.ts | 349 ++ packages/api/src/menu/menuItem.ts | 126 + packages/api/src/menu/predefinedMenuItem.ts | 203 ++ packages/api/src/menu/submenu.ts | 305 ++ packages/api/src/mocks.ts | 380 ++ packages/api/src/path.ts | 853 +++++ packages/api/src/tray.ts | 502 +++ packages/api/src/webview.ts | 1051 ++++++ packages/api/src/webviewWindow.ts | 324 ++ packages/api/src/window.ts | 3215 +++++++++++++++++ packages/api/tsconfig.json | 13 + packages/cli/.cargo/config.toml | 23 + packages/cli/.gitignore | 2 + packages/cli/.npmignore | 14 + packages/cli/Cargo.toml | 24 + packages/cli/LICENSE-APACHE-2.0 | 177 + packages/cli/LICENSE-MIT | 21 + packages/cli/README.md | 52 + .../cli/__tests__/fixtures/empty/.gitignore | 1 + .../__tests__/fixtures/empty/dist/index.html | 6 + .../cli/__tests__/fixtures/empty/package.json | 5 + packages/cli/__tests__/template.spec.ts | 89 + packages/cli/append-headers.js | 15 + packages/cli/build.rs | 7 + packages/cli/index.d.ts | 9 + packages/cli/index.js | 595 +++ packages/cli/main.d.ts | 11 + packages/cli/main.js | 19 + packages/cli/npm/darwin-arm64/README.md | 3 + packages/cli/npm/darwin-arm64/package.json | 33 + packages/cli/npm/darwin-x64/README.md | 3 + packages/cli/npm/darwin-x64/package.json | 33 + .../cli/npm/linux-arm-gnueabihf/README.md | 3 + .../cli/npm/linux-arm-gnueabihf/package.json | 33 + packages/cli/npm/linux-arm64-gnu/README.md | 3 + packages/cli/npm/linux-arm64-gnu/package.json | 36 + packages/cli/npm/linux-arm64-musl/README.md | 3 + .../cli/npm/linux-arm64-musl/package.json | 36 + packages/cli/npm/linux-riscv64-gnu/README.md | 3 + .../cli/npm/linux-riscv64-gnu/package.json | 36 + packages/cli/npm/linux-x64-gnu/README.md | 3 + packages/cli/npm/linux-x64-gnu/package.json | 36 + packages/cli/npm/linux-x64-musl/README.md | 3 + packages/cli/npm/linux-x64-musl/package.json | 36 + packages/cli/npm/win32-arm64-msvc/README.md | 3 + .../cli/npm/win32-arm64-msvc/package.json | 33 + packages/cli/npm/win32-ia32-msvc/README.md | 3 + packages/cli/npm/win32-ia32-msvc/package.json | 33 + packages/cli/npm/win32-x64-msvc/README.md | 3 + packages/cli/npm/win32-x64-msvc/package.json | 33 + packages/cli/package.json | 67 + packages/cli/src/lib.rs | 53 + packages/cli/tauri.js | 57 + pnpm-workspace.yaml | 41 + renovate.json | 60 + rustfmt.toml | 15 + supply-chain/audits.toml | 874 +++++ supply-chain/config.toml | 955 +++++ supply-chain/imports.lock | 2983 +++++++++++++++ 1178 files changed, 156404 insertions(+) create mode 100644 .cargo/audit.toml create mode 100644 .cargo/config.toml create mode 100644 .changes/README.md create mode 100644 .changes/acl-deny-execution-context.md create mode 100644 .changes/additional-watch-folders-alias.md create mode 100644 .changes/allow-crlf-cargo-toml.md create mode 100644 .changes/android-compileroptions-dsl.md create mode 100644 .changes/android-consumer-proguard.md create mode 100644 .changes/android-graceful-exit.md create mode 100644 .changes/android-gradle-9.md create mode 100644 .changes/android-java-gradle-compat-hint.md create mode 100644 .changes/android-json-org-serialization.md create mode 100644 .changes/android-material3.md create mode 100644 .changes/android-plugin-manager-no-activity.md create mode 100644 .changes/android-plugin-manager-relaunch.md create mode 100644 .changes/android-target-sdk-37.md create mode 100644 .changes/android-video-dir.md create mode 100644 .changes/app-bound-domains.md create mode 100644 .changes/app-exit-api.md create mode 100644 .changes/appimage-respect-gdk-backend.md create mode 100644 .changes/asset-protocol-multi-range.md create mode 100644 .changes/asset-protocol-off-main-thread.md create mode 100644 .changes/brotli-9.md create mode 100644 .changes/build-config-path.md create mode 100644 .changes/build-windows-icon-config-root.md create mode 100644 .changes/bundle-vc-runtime.md create mode 100644 .changes/bundler-breaking-changes.md create mode 100644 .changes/bundler-show-sizes.md create mode 100644 .changes/cargo-toml-1.md create mode 100644 .changes/change-pr-15578.md create mode 100644 .changes/change-pr-15593.md create mode 100644 .changes/change-pr-15677.md create mode 100644 .changes/change-pr-15952.md create mode 100644 .changes/clarify-init-frontend-command-prompts.md create mode 100644 .changes/cleanup-js-menu-channel.md create mode 100644 .changes/cli-dev-kill-process-tree.md create mode 100644 .changes/cli-force-noninteractive.md create mode 100644 .changes/cli-gradlew-crlf.md create mode 100644 .changes/cli-help-text-fixes.md create mode 100644 .changes/cli-ios-sim-build-rebuild.md create mode 100644 .changes/config.json create mode 100644 .changes/convert-file-src.md create mode 100644 .changes/ctor-1.md create mode 100644 .changes/default-product-name-warning.md create mode 100644 .changes/disable-binary-patching.md create mode 100644 .changes/dpi-type-wrappers.md create mode 100644 .changes/edition-2024-template.md create mode 100644 .changes/edition-2024.md create mode 100644 .changes/event-listener-cleanup-on-destroy.md create mode 100644 .changes/exit-with-code.md create mode 100644 .changes/explicit-resource-management.md create mode 100644 .changes/expose-set-activate-ignoring-other-apps.md create mode 100644 .changes/fix-appimage-xdg-utils.md create mode 100644 .changes/fix-capabilities-rerun-absolute-path.md create mode 100644 .changes/fix-channel-data-queue-webview-binding.md create mode 100644 .changes/fix-cli-add-npm-frontend-dir.md create mode 100644 .changes/fix-copy-new-resource-files.md create mode 100644 .changes/fix-deterministic-config-serialization.md create mode 100644 .changes/fix-deterministic-embedded-assets-codegen.md create mode 100644 .changes/fix-ico-broken-in-task-manager.md create mode 100644 .changes/fix-ico-largest-entry.md create mode 100644 .changes/fix-js-listeners-leak-on-webview-close.md create mode 100644 .changes/fix-js-set-background-color-arguments.md create mode 100644 .changes/fix-monitor-queries-off-thread.md create mode 100644 .changes/fix-objc-retain-leak.md create mode 100644 .changes/fix-send-sync.md create mode 100644 .changes/fix-unlisten-guard-missing-entry.md create mode 100644 .changes/fix-wix-external-bin-id.md create mode 100644 .changes/fix-wix-resource-target-name.md create mode 100644 .changes/header-source-map-deterministic-display.md create mode 100644 .changes/icon-non-square-fit.md create mode 100644 .changes/image-from-resource.md create mode 100644 .changes/image-icon-zero-copy.md create mode 100644 .changes/image-rgba-perf.md create mode 100644 .changes/image-rgba-uint8array-generic.md create mode 100644 .changes/ios-27.md create mode 100644 .changes/ios-monitor-off-main-thread.md create mode 100644 .changes/js-image-type-alias.md create mode 100644 .changes/lock-minor-versions.md create mode 100644 .changes/macos-app-set-theme-window-theme.md create mode 100644 .changes/macos-liquid-glass.md create mode 100644 .changes/menu-command-panic-on-wrong-input.md create mode 100644 .changes/mobile-init-pnpm-native.md create mode 100644 .changes/mobile-resumed-suspended-event.md create mode 100644 .changes/mock-runtime-run-main-thread-deadlock.md create mode 100644 .changes/msrv-1.90.md create mode 100644 .changes/no-redirection-bitmap-api.md create mode 100644 .changes/no-redirection-bitmap-config.md create mode 100644 .changes/no-redirection-bitmap.md create mode 100644 .changes/nsis-restart-manager.md create mode 100644 .changes/objc2-0.3.2.md create mode 100644 .changes/path-normalize-empty.md create mode 100644 .changes/permission-handler.md create mode 100644 .changes/permissions-policy-header-name.md create mode 100644 .changes/plugin-cleanup-before-exit.md create mode 100644 .changes/plugin-template-ts7.md create mode 100644 .changes/png-0.18.md create mode 100644 .changes/resolved-command-reference-debug-assertion.md create mode 100644 .changes/runtime-handle-monitor-fns.md create mode 100644 .changes/runtime-wry-avoid-clones.md create mode 100644 .changes/runtime-wry-windows-store-debug.md create mode 100644 .changes/set-fullscreen-on-monitor.md create mode 100644 .changes/signing-env-vars.md create mode 100644 .changes/static-vc-runtime.md create mode 100644 .changes/swift-27.md create mode 100644 .changes/target-dir-nightly-out-dir.md create mode 100644 .changes/tauri-build-static-vc-runtime.md create mode 100644 .changes/tauri-info-skip-rust-only-js.md create mode 100644 .changes/unstable-webview-focus.md create mode 100644 .changes/update-dom-query.md create mode 100644 .changes/updater-sign-version.md create mode 100644 .changes/validate-inline-capabilities.md create mode 100644 .changes/wait-async-window-creation.md create mode 100644 .changes/webkitgtk-resize-cursor.md create mode 100644 .changes/webview-focus-on-move.md create mode 100644 .changes/window-effect-non-exhaustive.md create mode 100644 .changes/windows-0.62.md create mode 100644 .changes/windows-multiwebview-cached-focused-states.md create mode 100644 .changes/wry-focused-webview.md create mode 100644 .changes/xcode-27.md create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/README.md create mode 100644 .devcontainer/devcontainer.json create mode 100644 .docker/cross/aarch64.Dockerfile create mode 100755 .docker/cross/cmake.sh create mode 100755 .docker/cross/common.sh create mode 100755 .docker/cross/deny-debian-packages.sh create mode 100755 .docker/cross/dropbear.sh create mode 100644 .docker/cross/lib.sh create mode 100755 .docker/cross/linux-image.sh create mode 100755 .docker/cross/linux-runner create mode 100755 .docker/cross/qemu.sh create mode 100755 .docker/cross/xargo.sh create mode 100644 .editorconfig create mode 100644 .github/CODEOWNERS create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/docs_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/RELEASING.md create mode 100644 .github/icon.png create mode 100644 .github/splash.png create mode 100644 .github/sponsors/crabnebula.svg create mode 100644 .github/workflows/audit.yml create mode 100644 .github/workflows/bench.yml create mode 100644 .github/workflows/check-change-tags.yml create mode 100644 .github/workflows/check-generated-files.yml create mode 100644 .github/workflows/check-license-header.yml create mode 100644 .github/workflows/covector-comment-on-fork.yml create mode 100644 .github/workflows/covector-status.yml create mode 100644 .github/workflows/covector-version-or-publish.yml create mode 100644 .github/workflows/deploy-schema-worker.yml create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/fmt.yml create mode 100644 .github/workflows/lint-js.yml create mode 100644 .github/workflows/lint-rust.yml create mode 100644 .github/workflows/publish-cli-js.yml create mode 100644 .github/workflows/publish-cli-rs.yml create mode 100644 .github/workflows/supply-chain.yml create mode 100644 .github/workflows/test-android.yml create mode 100644 .github/workflows/test-api-e2e-mobile.yml create mode 100644 .github/workflows/test-api-e2e.yml create mode 100644 .github/workflows/test-cli-js.yml create mode 100644 .github/workflows/test-cli-rs.yml create mode 100644 .github/workflows/test-core.yml create mode 100644 .github/workflows/udeps.yml create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .scripts/ci/check-change-tags.js create mode 100644 .scripts/ci/check-license-header.js create mode 100755 .scripts/ci/collect-macos-crash-reports.sh create mode 100755 .scripts/ci/has-diff.sh create mode 100755 .scripts/ci/pack-cli.sh create mode 100644 .scripts/ci/sync-cli-metadata.js create mode 100644 .vscode/extensions.json create mode 100644 ARCHITECTURE.md create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE-2.0 create mode 100644 LICENSE-MIT create mode 100644 LICENSE.spdx create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 bench/Cargo.toml create mode 100644 bench/README.md create mode 100644 bench/src/build_benchmark_jsons.rs create mode 100644 bench/src/run_benchmark.rs create mode 100644 bench/src/utils.rs create mode 100644 bench/tests/cpu_intensive/public/index.css create mode 100644 bench/tests/cpu_intensive/public/index.html create mode 100644 bench/tests/cpu_intensive/public/site.js create mode 100644 bench/tests/cpu_intensive/public/worker.js create mode 100644 bench/tests/cpu_intensive/src-tauri/.gitignore create mode 100644 bench/tests/cpu_intensive/src-tauri/Cargo.toml create mode 100644 bench/tests/cpu_intensive/src-tauri/build.rs create mode 100644 bench/tests/cpu_intensive/src-tauri/src/main.rs create mode 100644 bench/tests/cpu_intensive/src-tauri/tauri.conf.json create mode 100644 bench/tests/files_transfer/public/index.html create mode 100644 bench/tests/files_transfer/src-tauri/.gitignore create mode 100644 bench/tests/files_transfer/src-tauri/Cargo.toml create mode 100644 bench/tests/files_transfer/src-tauri/build.rs create mode 100644 bench/tests/files_transfer/src-tauri/src/main.rs create mode 100644 bench/tests/files_transfer/src-tauri/tauri.conf.json create mode 100644 bench/tests/helloworld/public/index.html create mode 100644 bench/tests/helloworld/src-tauri/.gitignore create mode 100644 bench/tests/helloworld/src-tauri/Cargo.toml create mode 100644 bench/tests/helloworld/src-tauri/build.rs create mode 100644 bench/tests/helloworld/src-tauri/src/main.rs create mode 100644 bench/tests/helloworld/src-tauri/tauri.conf.json create mode 100644 crates/tauri-build/CHANGELOG.md create mode 100644 crates/tauri-build/Cargo.toml create mode 100644 crates/tauri-build/LICENSE-APACHE-2.0 create mode 100644 crates/tauri-build/LICENSE-MIT create mode 100644 crates/tauri-build/README.md create mode 100644 crates/tauri-build/src/acl.rs create mode 100644 crates/tauri-build/src/codegen/context.rs create mode 100644 crates/tauri-build/src/codegen/mod.rs create mode 100644 crates/tauri-build/src/lib.rs create mode 100644 crates/tauri-build/src/manifest.rs create mode 100644 crates/tauri-build/src/mobile.rs create mode 100644 crates/tauri-build/src/static_vcruntime.rs create mode 100644 crates/tauri-build/src/windows-app-manifest.xml create mode 100644 crates/tauri-bundler/CHANGELOG.md create mode 100644 crates/tauri-bundler/Cargo.toml create mode 100644 crates/tauri-bundler/License-Apache.md create mode 100644 crates/tauri-bundler/License-MIT.md create mode 100644 crates/tauri-bundler/README.md create mode 100644 crates/tauri-bundler/src/bundle.rs create mode 100644 crates/tauri-bundler/src/bundle/category.rs create mode 100644 crates/tauri-bundler/src/bundle/kmp/mod.rs create mode 100644 crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy-plugin-gstreamer.sh create mode 100644 crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy-plugin-gtk.sh create mode 100644 crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy.rs create mode 100644 crates/tauri-bundler/src/bundle/linux/appimage/mod.rs create mode 100644 crates/tauri-bundler/src/bundle/linux/debian.rs create mode 100644 crates/tauri-bundler/src/bundle/linux/freedesktop/main.desktop create mode 100644 crates/tauri-bundler/src/bundle/linux/freedesktop/mod.rs create mode 100644 crates/tauri-bundler/src/bundle/linux/mod.rs create mode 100644 crates/tauri-bundler/src/bundle/linux/rpm.rs create mode 100644 crates/tauri-bundler/src/bundle/macos/app.rs create mode 100644 crates/tauri-bundler/src/bundle/macos/dmg/bundle_dmg create mode 100644 crates/tauri-bundler/src/bundle/macos/dmg/eula-resources-template.xml create mode 100644 crates/tauri-bundler/src/bundle/macos/dmg/mod.rs create mode 100644 crates/tauri-bundler/src/bundle/macos/dmg/template.applescript create mode 100644 crates/tauri-bundler/src/bundle/macos/icon.rs create mode 100644 crates/tauri-bundler/src/bundle/macos/ios.rs create mode 100644 crates/tauri-bundler/src/bundle/macos/mod.rs create mode 100644 crates/tauri-bundler/src/bundle/macos/sign.rs create mode 100644 crates/tauri-bundler/src/bundle/platform.rs create mode 100644 crates/tauri-bundler/src/bundle/settings.rs create mode 100644 crates/tauri-bundler/src/bundle/updater_bundle.rs create mode 100644 crates/tauri-bundler/src/bundle/windows/mod.rs create mode 100644 crates/tauri-bundler/src/bundle/windows/msi/default-locale-strings.xml create mode 100644 crates/tauri-bundler/src/bundle/windows/msi/install-task.ps1 create mode 100644 crates/tauri-bundler/src/bundle/windows/msi/languages.json create mode 100644 crates/tauri-bundler/src/bundle/windows/msi/main.wxs create mode 100644 crates/tauri-bundler/src/bundle/windows/msi/mod.rs create mode 100644 crates/tauri-bundler/src/bundle/windows/msi/uninstall-task.ps1 create mode 100644 crates/tauri-bundler/src/bundle/windows/msi/update-task.xml create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/FileAssociation.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Arabic.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Bulgarian.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Dutch.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/English.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/French.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/German.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Hebrew.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Italian.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Japanese.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Korean.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Norwegian.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Persian.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Portuguese.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/PortugueseBR.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Russian.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/SimpChinese.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Spanish.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/SpanishInternational.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Swedish.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/TradChinese.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Turkish.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Ukrainian.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/languages/Vietnamese.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/mod.rs create mode 100644 crates/tauri-bundler/src/bundle/windows/nsis/utils.nsh create mode 100644 crates/tauri-bundler/src/bundle/windows/sign.rs create mode 100644 crates/tauri-bundler/src/bundle/windows/util.rs create mode 100644 crates/tauri-bundler/src/error.rs create mode 100644 crates/tauri-bundler/src/lib.rs create mode 100644 crates/tauri-bundler/src/utils/fs_utils.rs create mode 100644 crates/tauri-bundler/src/utils/http_utils.rs create mode 100644 crates/tauri-bundler/src/utils/mod.rs create mode 100644 crates/tauri-cli/Cargo.toml create mode 100644 crates/tauri-cli/ENVIRONMENT_VARIABLES.md create mode 100644 crates/tauri-cli/LICENSE-APACHE-2.0 create mode 100644 crates/tauri-cli/LICENSE-MIT create mode 100644 crates/tauri-cli/README.md create mode 100644 crates/tauri-cli/build.rs create mode 100644 crates/tauri-cli/metadata-v2.json create mode 100644 crates/tauri-cli/metadata.json create mode 100644 crates/tauri-cli/scripts/kill-children.sh create mode 100644 crates/tauri-cli/src/acl/capability/mod.rs create mode 100644 crates/tauri-cli/src/acl/capability/new.rs create mode 100644 crates/tauri-cli/src/acl/mod.rs create mode 100644 crates/tauri-cli/src/acl/permission/add.rs create mode 100644 crates/tauri-cli/src/acl/permission/ls.rs create mode 100644 crates/tauri-cli/src/acl/permission/mod.rs create mode 100644 crates/tauri-cli/src/acl/permission/new.rs create mode 100644 crates/tauri-cli/src/acl/permission/rm.rs create mode 100644 crates/tauri-cli/src/add.rs create mode 100644 crates/tauri-cli/src/build.rs create mode 100644 crates/tauri-cli/src/bundle.rs create mode 100644 crates/tauri-cli/src/completions.rs create mode 100644 crates/tauri-cli/src/dev.rs create mode 100644 crates/tauri-cli/src/dev/auto-reload.js create mode 100644 crates/tauri-cli/src/dev/builtin_dev_server.rs create mode 100644 crates/tauri-cli/src/error.rs create mode 100644 crates/tauri-cli/src/helpers/app_paths.rs create mode 100644 crates/tauri-cli/src/helpers/cargo.rs create mode 100644 crates/tauri-cli/src/helpers/cargo_manifest.rs create mode 100644 crates/tauri-cli/src/helpers/config.rs create mode 100644 crates/tauri-cli/src/helpers/flock.rs create mode 100644 crates/tauri-cli/src/helpers/framework.rs create mode 100644 crates/tauri-cli/src/helpers/fs.rs create mode 100644 crates/tauri-cli/src/helpers/http.rs create mode 100644 crates/tauri-cli/src/helpers/icns.json create mode 100644 crates/tauri-cli/src/helpers/mod.rs create mode 100644 crates/tauri-cli/src/helpers/npm.rs create mode 100644 crates/tauri-cli/src/helpers/pbxproj.rs create mode 100644 crates/tauri-cli/src/helpers/plist.rs create mode 100644 crates/tauri-cli/src/helpers/plugins.rs create mode 100644 crates/tauri-cli/src/helpers/prompts.rs create mode 100644 crates/tauri-cli/src/helpers/template.rs create mode 100644 crates/tauri-cli/src/helpers/updater_signature.rs create mode 100644 crates/tauri-cli/src/icon.rs create mode 100644 crates/tauri-cli/src/info/app.rs create mode 100644 crates/tauri-cli/src/info/env_nodejs.rs create mode 100644 crates/tauri-cli/src/info/env_rust.rs create mode 100644 crates/tauri-cli/src/info/env_system.rs create mode 100644 crates/tauri-cli/src/info/ios.rs create mode 100644 crates/tauri-cli/src/info/mod.rs create mode 100644 crates/tauri-cli/src/info/packages_nodejs.rs create mode 100644 crates/tauri-cli/src/info/packages_rust.rs create mode 100644 crates/tauri-cli/src/info/plugins.rs create mode 100644 crates/tauri-cli/src/init.rs create mode 100644 crates/tauri-cli/src/inspect.rs create mode 100644 crates/tauri-cli/src/interface/mod.rs create mode 100644 crates/tauri-cli/src/interface/rust.rs create mode 100644 crates/tauri-cli/src/interface/rust/cargo_config.rs create mode 100644 crates/tauri-cli/src/interface/rust/desktop.rs create mode 100644 crates/tauri-cli/src/interface/rust/installation.rs create mode 100644 crates/tauri-cli/src/interface/rust/manifest.rs create mode 100644 crates/tauri-cli/src/lib.rs create mode 100644 crates/tauri-cli/src/main.rs create mode 100644 crates/tauri-cli/src/migrate/migrations/mod.rs create mode 100644 crates/tauri-cli/src/migrate/migrations/v1/config.rs create mode 100644 crates/tauri-cli/src/migrate/migrations/v1/fixtures/api-example.tauri.conf.json create mode 100644 crates/tauri-cli/src/migrate/migrations/v1/fixtures/cli-template.tauri.conf.json create mode 100644 crates/tauri-cli/src/migrate/migrations/v1/frontend.rs create mode 100644 crates/tauri-cli/src/migrate/migrations/v1/frontend/partial_loader/mod.rs create mode 100644 crates/tauri-cli/src/migrate/migrations/v1/frontend/partial_loader/svelte.rs create mode 100644 crates/tauri-cli/src/migrate/migrations/v1/frontend/partial_loader/vue.rs create mode 100644 crates/tauri-cli/src/migrate/migrations/v1/manifest.rs create mode 100644 crates/tauri-cli/src/migrate/migrations/v1/mod.rs create mode 100644 crates/tauri-cli/src/migrate/migrations/v2_beta.rs create mode 100644 crates/tauri-cli/src/migrate/mod.rs create mode 100644 crates/tauri-cli/src/mobile/android/android_studio_script.rs create mode 100644 crates/tauri-cli/src/mobile/android/build.rs create mode 100644 crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs create mode 100644 crates/tauri-cli/src/mobile/android/dev.rs create mode 100644 crates/tauri-cli/src/mobile/android/mod.rs create mode 100644 crates/tauri-cli/src/mobile/android/project.rs create mode 100644 crates/tauri-cli/src/mobile/android/run.rs create mode 100644 crates/tauri-cli/src/mobile/init.rs create mode 100644 crates/tauri-cli/src/mobile/ios/build.rs create mode 100644 crates/tauri-cli/src/mobile/ios/dev.rs create mode 100644 crates/tauri-cli/src/mobile/ios/mod.rs create mode 100644 crates/tauri-cli/src/mobile/ios/project.rs create mode 100644 crates/tauri-cli/src/mobile/ios/run.rs create mode 100644 crates/tauri-cli/src/mobile/ios/xcode_script.rs create mode 100644 crates/tauri-cli/src/mobile/mod.rs create mode 100644 crates/tauri-cli/src/plugin/android.rs create mode 100644 crates/tauri-cli/src/plugin/init.rs create mode 100644 crates/tauri-cli/src/plugin/ios.rs create mode 100644 crates/tauri-cli/src/plugin/mod.rs create mode 100644 crates/tauri-cli/src/plugin/new.rs create mode 100644 crates/tauri-cli/src/remove.rs create mode 100644 crates/tauri-cli/src/signer/generate.rs create mode 100644 crates/tauri-cli/src/signer/mod.rs create mode 100644 crates/tauri-cli/src/signer/sign.rs create mode 100644 crates/tauri-cli/tauri-dev-watcher.gitignore create mode 100644 crates/tauri-cli/tauri.gitignore create mode 100644 crates/tauri-cli/templates/app/src-tauri/.gitignore create mode 100644 crates/tauri-cli/templates/app/src-tauri/Cargo.crate-manifest create mode 100644 crates/tauri-cli/templates/app/src-tauri/build.rs create mode 100644 crates/tauri-cli/templates/app/src-tauri/capabilities/default.json create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/128x128.png create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/128x128@2x.png create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/32x32.png create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/Square107x107Logo.png create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/Square142x142Logo.png create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/Square150x150Logo.png create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/Square284x284Logo.png create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/Square30x30Logo.png create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/Square310x310Logo.png create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/Square44x44Logo.png create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/Square71x71Logo.png create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/Square89x89Logo.png create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/StoreLogo.png create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/icon.ico create mode 100644 crates/tauri-cli/templates/app/src-tauri/icons/icon.png create mode 100644 crates/tauri-cli/templates/app/src-tauri/src/lib.rs create mode 100644 crates/tauri-cli/templates/app/src-tauri/src/main.rs create mode 100644 crates/tauri-cli/templates/app/src-tauri/tauri.conf.json create mode 100644 crates/tauri-cli/templates/mobile/android/.editorconfig create mode 100644 crates/tauri-cli/templates/mobile/android/.gitignore create mode 100644 crates/tauri-cli/templates/mobile/android/app/.gitignore create mode 100644 crates/tauri-cli/templates/mobile/android/app/build.gradle.kts create mode 100644 crates/tauri-cli/templates/mobile/android/app/proguard-rules.pro create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/AndroidManifest.xml create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/MainActivity.kt create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/layout/activity_main.xml create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/values-night/themes.xml create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/values/colors.xml create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/values/strings.xml create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/values/themes.xml create mode 100644 crates/tauri-cli/templates/mobile/android/app/src/main/res/xml/file_paths.xml create mode 100644 crates/tauri-cli/templates/mobile/android/build.gradle.kts create mode 100644 crates/tauri-cli/templates/mobile/android/buildSrc/build.gradle.kts create mode 100644 crates/tauri-cli/templates/mobile/android/buildSrc/src/main/kotlin/BuildTask.kt create mode 100644 crates/tauri-cli/templates/mobile/android/buildSrc/src/main/kotlin/RustPlugin.kt create mode 100644 crates/tauri-cli/templates/mobile/android/gradle.properties create mode 100644 crates/tauri-cli/templates/mobile/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 crates/tauri-cli/templates/mobile/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 crates/tauri-cli/templates/mobile/android/gradlew create mode 100644 crates/tauri-cli/templates/mobile/android/gradlew.bat create mode 100644 crates/tauri-cli/templates/mobile/android/settings.gradle create mode 100644 crates/tauri-cli/templates/mobile/ios/.gitignore create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 crates/tauri-cli/templates/mobile/ios/Assets.xcassets/Contents.json create mode 100644 crates/tauri-cli/templates/mobile/ios/ExportOptions.plist create mode 100644 crates/tauri-cli/templates/mobile/ios/LaunchScreen.storyboard create mode 100644 crates/tauri-cli/templates/mobile/ios/Podfile create mode 100644 crates/tauri-cli/templates/mobile/ios/Sources/{{app.name}}/bindings/bindings.h create mode 100644 crates/tauri-cli/templates/mobile/ios/Sources/{{app.name}}/main.mm create mode 100644 crates/tauri-cli/templates/mobile/ios/project.yml create mode 100644 crates/tauri-cli/templates/mobile/ios/{{app.name}}.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 crates/tauri-cli/templates/plugin/.github/workflows/audit.yml create mode 100644 crates/tauri-cli/templates/plugin/.github/workflows/clippy.yml create mode 100644 crates/tauri-cli/templates/plugin/.github/workflows/test.yml create mode 100644 crates/tauri-cli/templates/plugin/.gitignore create mode 100644 crates/tauri-cli/templates/plugin/Cargo.crate-manifest create mode 100644 crates/tauri-cli/templates/plugin/README.md create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/.gitignore create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/.vscode/extensions.json create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/README.md create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/index.html create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/jsconfig.json create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/package.json create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/public/svelte.svg create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/public/tauri.svg create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/public/vite.svg create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/.gitignore create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/Cargo.crate-manifest create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/build.rs create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/capabilities/default.json create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/icons/128x128.png create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/icons/128x128@2x.png create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/icons/32x32.png create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/icons/icon.ico create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/icons/icon.png create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/src/lib.rs create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/src/main.rs create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/tauri.conf.json create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/App.svelte create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/lib/Greet.svelte create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/main.js create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/style.css create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/vite-env.d.ts create mode 100644 crates/tauri-cli/templates/plugin/__example-api/tauri-app/vite.config.js create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/.gitignore create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/package.json create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/public/index.html create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/.gitignore create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/Cargo.crate-manifest create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/build.rs create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/capabilities/default.json create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/icons/128x128.png create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/icons/128x128@2x.png create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/icons/32x32.png create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/icons/icon.ico create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/icons/icon.png create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/rustfmt.toml create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/src/lib.rs create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/src/main.rs create mode 100644 crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/tauri.conf.json create mode 100644 crates/tauri-cli/templates/plugin/android/.gitignore create mode 100644 crates/tauri-cli/templates/plugin/android/build.gradle.kts create mode 100644 crates/tauri-cli/templates/plugin/android/consumer-rules.pro create mode 100644 crates/tauri-cli/templates/plugin/android/settings.gradle create mode 100644 crates/tauri-cli/templates/plugin/android/src/androidTest/java/ExampleInstrumentedTest.kt create mode 100644 crates/tauri-cli/templates/plugin/android/src/main/AndroidManifest.xml create mode 100644 crates/tauri-cli/templates/plugin/android/src/main/java/Example.kt create mode 100644 crates/tauri-cli/templates/plugin/android/src/main/java/ExamplePlugin.kt create mode 100644 crates/tauri-cli/templates/plugin/android/src/test/java/ExampleUnitTest.kt create mode 100644 crates/tauri-cli/templates/plugin/build.rs create mode 100644 crates/tauri-cli/templates/plugin/guest-js/index.ts create mode 100644 crates/tauri-cli/templates/plugin/ios-spm/.gitignore create mode 100644 crates/tauri-cli/templates/plugin/ios-spm/Package.swift create mode 100644 crates/tauri-cli/templates/plugin/ios-spm/README.md create mode 100644 crates/tauri-cli/templates/plugin/ios-spm/Sources/ExamplePlugin.swift create mode 100644 crates/tauri-cli/templates/plugin/ios-spm/Tests/PluginTests/PluginTests.swift create mode 100644 crates/tauri-cli/templates/plugin/package.json create mode 100644 crates/tauri-cli/templates/plugin/rollup.config.js create mode 100644 crates/tauri-cli/templates/plugin/src/commands.rs create mode 100644 crates/tauri-cli/templates/plugin/src/desktop.rs create mode 100644 crates/tauri-cli/templates/plugin/src/error.rs create mode 100644 crates/tauri-cli/templates/plugin/src/lib.rs create mode 100644 crates/tauri-cli/templates/plugin/src/mobile.rs create mode 100644 crates/tauri-cli/templates/plugin/src/models.rs create mode 100644 crates/tauri-cli/templates/plugin/tsconfig.json create mode 100644 crates/tauri-cli/templates/tauri.conf.json create mode 100644 crates/tauri-cli/tests/fixtures/pbxproj/project.pbxproj create mode 100644 crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project-modified.pbxproj.snap create mode 100644 crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project.pbxproj.snap create mode 100644 crates/tauri-cli/tests/init.rs create mode 100644 crates/tauri-codegen/CHANGELOG.md create mode 100644 crates/tauri-codegen/Cargo.toml create mode 100644 crates/tauri-codegen/LICENSE-APACHE-2.0 create mode 100644 crates/tauri-codegen/LICENSE-MIT create mode 100644 crates/tauri-codegen/README.md create mode 100644 crates/tauri-codegen/src/context.rs create mode 100644 crates/tauri-codegen/src/embedded_assets.rs create mode 100644 crates/tauri-codegen/src/image.rs create mode 100644 crates/tauri-codegen/src/lib.rs create mode 100644 crates/tauri-codegen/src/vendor/blake3_reference.rs create mode 100644 crates/tauri-codegen/src/vendor/mod.rs create mode 100644 crates/tauri-driver/CHANGELOG.md create mode 100644 crates/tauri-driver/Cargo.toml create mode 100644 crates/tauri-driver/LICENSE-APACHE-2.0 create mode 100644 crates/tauri-driver/LICENSE-MIT create mode 100644 crates/tauri-driver/LICENSE.spdx create mode 100644 crates/tauri-driver/README.md create mode 100644 crates/tauri-driver/src/cli.rs create mode 100644 crates/tauri-driver/src/main.rs create mode 100644 crates/tauri-driver/src/server.rs create mode 100644 crates/tauri-driver/src/webdriver.rs create mode 100644 crates/tauri-macos-sign/CHANGELOG.md create mode 100644 crates/tauri-macos-sign/Cargo.toml create mode 100644 crates/tauri-macos-sign/README.md create mode 100644 crates/tauri-macos-sign/src/certificate.rs create mode 100644 crates/tauri-macos-sign/src/keychain.rs create mode 100644 crates/tauri-macos-sign/src/keychain/identity.rs create mode 100644 crates/tauri-macos-sign/src/lib.rs create mode 100644 crates/tauri-macos-sign/src/provisioning_profile.rs create mode 100644 crates/tauri-macros/CHANGELOG.md create mode 100644 crates/tauri-macros/Cargo.toml create mode 100644 crates/tauri-macros/LICENSE-APACHE-2.0 create mode 100644 crates/tauri-macros/LICENSE-MIT create mode 100644 crates/tauri-macros/README.md create mode 100644 crates/tauri-macros/src/command/handler.rs create mode 100644 crates/tauri-macros/src/command/mod.rs create mode 100644 crates/tauri-macros/src/command/wrapper.rs create mode 100644 crates/tauri-macros/src/context.rs create mode 100644 crates/tauri-macros/src/lib.rs create mode 100644 crates/tauri-macros/src/menu.rs create mode 100644 crates/tauri-macros/src/mobile.rs create mode 100644 crates/tauri-macros/src/runtime.rs create mode 100644 crates/tauri-plugin/CHANGELOG.md create mode 100644 crates/tauri-plugin/Cargo.toml create mode 100644 crates/tauri-plugin/src/build/mobile.rs create mode 100644 crates/tauri-plugin/src/build/mod.rs create mode 100644 crates/tauri-plugin/src/lib.rs create mode 100644 crates/tauri-plugin/src/runtime.rs create mode 100644 crates/tauri-runtime-wry/CHANGELOG.md create mode 100644 crates/tauri-runtime-wry/Cargo.toml create mode 100644 crates/tauri-runtime-wry/LICENSE-APACHE-2.0 create mode 100644 crates/tauri-runtime-wry/LICENSE-MIT create mode 100644 crates/tauri-runtime-wry/README.md create mode 100644 crates/tauri-runtime-wry/build.rs create mode 100644 crates/tauri-runtime-wry/src/dialog/mod.rs create mode 100644 crates/tauri-runtime-wry/src/dialog/windows.rs create mode 100644 crates/tauri-runtime-wry/src/monitor/linux.rs create mode 100644 crates/tauri-runtime-wry/src/monitor/macos.rs create mode 100644 crates/tauri-runtime-wry/src/monitor/mod.rs create mode 100644 crates/tauri-runtime-wry/src/monitor/windows.rs create mode 100644 crates/tauri-runtime-wry/src/undecorated_resizing.rs create mode 100644 crates/tauri-runtime-wry/src/util.rs create mode 100644 crates/tauri-runtime-wry/src/webview.rs create mode 100644 crates/tauri-runtime-wry/src/webview_permissions.rs create mode 100644 crates/tauri-runtime-wry/src/window/linux.rs create mode 100644 crates/tauri-runtime-wry/src/window/macos.rs create mode 100644 crates/tauri-runtime-wry/src/window/mod.rs create mode 100644 crates/tauri-runtime-wry/src/window/windows.rs create mode 100644 crates/tauri-runtime/CHANGELOG.md create mode 100644 crates/tauri-runtime/Cargo.toml create mode 100644 crates/tauri-runtime/LICENSE-APACHE-2.0 create mode 100644 crates/tauri-runtime/LICENSE-MIT create mode 100644 crates/tauri-runtime/README.md create mode 100644 crates/tauri-runtime/build.rs create mode 100644 crates/tauri-runtime/src/dpi.rs create mode 100644 crates/tauri-runtime/src/lib.rs create mode 100644 crates/tauri-runtime/src/monitor.rs create mode 100644 crates/tauri-runtime/src/webview.rs create mode 100644 crates/tauri-runtime/src/webview_permissions.rs create mode 100644 crates/tauri-runtime/src/window.rs create mode 100644 crates/tauri-schema-generator/Cargo.toml create mode 100644 crates/tauri-schema-generator/README.md create mode 100644 crates/tauri-schema-generator/build.rs create mode 100644 crates/tauri-schema-generator/schemas/capability.schema.json create mode 100644 crates/tauri-schema-generator/schemas/permission.schema.json create mode 100644 crates/tauri-schema-generator/schemas/scope.schema.json create mode 100644 crates/tauri-schema-generator/src/main.rs create mode 100644 crates/tauri-schema-worker/.cargo/config.toml create mode 100644 crates/tauri-schema-worker/.gitignore create mode 100644 crates/tauri-schema-worker/Cargo.toml create mode 100644 crates/tauri-schema-worker/README.md create mode 100644 crates/tauri-schema-worker/package.json create mode 100644 crates/tauri-schema-worker/src/config.rs create mode 100644 crates/tauri-schema-worker/src/lib.rs create mode 100644 crates/tauri-schema-worker/wrangler.toml create mode 100644 crates/tauri-utils/CHANGELOG.md create mode 100644 crates/tauri-utils/Cargo.toml create mode 100644 crates/tauri-utils/LICENSE-APACHE-2.0 create mode 100644 crates/tauri-utils/LICENSE-MIT create mode 100644 crates/tauri-utils/README.md create mode 100644 crates/tauri-utils/src/acl/build.rs create mode 100644 crates/tauri-utils/src/acl/capability.rs create mode 100644 crates/tauri-utils/src/acl/identifier.rs create mode 100644 crates/tauri-utils/src/acl/manifest.rs create mode 100644 crates/tauri-utils/src/acl/mod.rs create mode 100644 crates/tauri-utils/src/acl/resolved.rs create mode 100644 crates/tauri-utils/src/acl/schema.rs create mode 100644 crates/tauri-utils/src/acl/value.rs create mode 100644 crates/tauri-utils/src/assets.rs create mode 100644 crates/tauri-utils/src/build.rs create mode 100644 crates/tauri-utils/src/config/parse.rs create mode 100644 crates/tauri-utils/src/config_v1/mod.rs create mode 100644 crates/tauri-utils/src/config_v1/parse.rs create mode 100644 crates/tauri-utils/src/html.rs create mode 100644 crates/tauri-utils/src/html2.rs create mode 100644 crates/tauri-utils/src/io.rs create mode 100644 crates/tauri-utils/src/lib.rs create mode 100644 crates/tauri-utils/src/mime_type.rs create mode 100644 crates/tauri-utils/src/pattern/isolation.js create mode 100644 crates/tauri-utils/src/pattern/isolation.rs create mode 100644 crates/tauri-utils/src/pattern/mod.rs create mode 100644 crates/tauri-utils/src/platform.rs create mode 100644 crates/tauri-utils/src/platform/starting_binary.rs create mode 100644 crates/tauri-utils/src/plugin.rs create mode 100644 crates/tauri-utils/src/resources.rs create mode 100644 crates/tauri-utils/src/tokens.rs create mode 100644 crates/tauri/.scripts/loop_qc.sh create mode 100644 crates/tauri/Cargo.toml create mode 100644 crates/tauri/LICENSE-APACHE-2.0 create mode 100644 crates/tauri/LICENSE-MIT create mode 100644 crates/tauri/README.md create mode 100644 crates/tauri/build.rs create mode 100644 crates/tauri/mobile/android-codegen/TauriActivity.kt create mode 100644 crates/tauri/mobile/android/.gitignore create mode 100644 crates/tauri/mobile/android/build.gradle.kts create mode 100644 crates/tauri/mobile/android/consumer-rules.pro create mode 100644 crates/tauri/mobile/android/src/androidTest/java/app/tauri/ExampleInstrumentedTest.kt create mode 100644 crates/tauri/mobile/android/src/main/AndroidManifest.xml create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/FsUtils.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/JniMethod.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/Logger.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/PathPlugin.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/PermissionHelper.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/PermissionState.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/annotation/ActivityCallback.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/annotation/InvokeArg.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/annotation/Permission.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/annotation/PermissionCallback.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/annotation/PluginMethod.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/annotation/TauriPlugin.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Channel.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/plugin/InvalidPluginMethodException.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/plugin/JSArray.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/plugin/JSObject.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginHandle.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginMethodData.kt create mode 100644 crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginResult.kt create mode 100644 crates/tauri/mobile/android/src/test/java/app/tauri/ExampleUnitTest.kt create mode 100644 crates/tauri/mobile/ios-api/.gitignore create mode 100644 crates/tauri/mobile/ios-api/Package.swift create mode 100644 crates/tauri/mobile/ios-api/README.md create mode 100644 crates/tauri/mobile/ios-api/Sources/Tauri/Channel.swift create mode 100644 crates/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift create mode 100644 crates/tauri/mobile/ios-api/Sources/Tauri/JSTypes.swift create mode 100644 crates/tauri/mobile/ios-api/Sources/Tauri/JsonValue.swift create mode 100644 crates/tauri/mobile/ios-api/Sources/Tauri/Logger.swift create mode 100644 crates/tauri/mobile/ios-api/Sources/Tauri/Plugin/Plugin.swift create mode 100644 crates/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift create mode 100644 crates/tauri/mobile/ios-api/Sources/Tauri/UiUtils.swift create mode 100644 crates/tauri/mobile/ios-api/Tests/TauriTests/TauriTests.swift create mode 100644 crates/tauri/mobile/proguard-tauri.pro create mode 100644 crates/tauri/permissions/app/autogenerated/reference.md create mode 100644 crates/tauri/permissions/event/autogenerated/reference.md create mode 100644 crates/tauri/permissions/image/autogenerated/reference.md create mode 100644 crates/tauri/permissions/menu/autogenerated/reference.md create mode 100644 crates/tauri/permissions/path/autogenerated/reference.md create mode 100644 crates/tauri/permissions/resources/autogenerated/reference.md create mode 100644 crates/tauri/permissions/tray/autogenerated/reference.md create mode 100644 crates/tauri/permissions/webview/autogenerated/reference.md create mode 100644 crates/tauri/permissions/window/autogenerated/reference.md create mode 100644 crates/tauri/scripts/bundle.global.js create mode 100644 crates/tauri/scripts/core.js create mode 100644 crates/tauri/scripts/freeze_prototype.js create mode 100644 crates/tauri/scripts/init.js create mode 100644 crates/tauri/scripts/ipc-protocol.js create mode 100644 crates/tauri/scripts/ipc.js create mode 100644 crates/tauri/scripts/isolation.js create mode 100644 crates/tauri/scripts/pattern.js create mode 100644 crates/tauri/scripts/process-ipc-message-fn.js create mode 100644 crates/tauri/src/app.rs create mode 100644 crates/tauri/src/app/plugin.rs create mode 100644 crates/tauri/src/async_runtime.rs create mode 100644 crates/tauri/src/error.rs create mode 100644 crates/tauri/src/event/event-system-spec.md create mode 100644 crates/tauri/src/event/event_name.rs create mode 100644 crates/tauri/src/event/init.js create mode 100644 crates/tauri/src/event/listener.rs create mode 100644 crates/tauri/src/event/mod.rs create mode 100644 crates/tauri/src/event/plugin.rs create mode 100644 crates/tauri/src/image/mod.rs create mode 100644 crates/tauri/src/image/plugin.rs create mode 100644 crates/tauri/src/ios.rs create mode 100644 crates/tauri/src/ipc/authority.rs create mode 100644 crates/tauri/src/ipc/capability_builder.rs create mode 100644 crates/tauri/src/ipc/channel.rs create mode 100644 crates/tauri/src/ipc/command.rs create mode 100644 crates/tauri/src/ipc/format_callback.rs create mode 100644 crates/tauri/src/ipc/mod.rs create mode 100644 crates/tauri/src/ipc/protocol.rs create mode 100644 crates/tauri/src/lib.rs create mode 100644 crates/tauri/src/manager/menu.rs create mode 100644 crates/tauri/src/manager/mod.rs create mode 100644 crates/tauri/src/manager/tray.rs create mode 100644 crates/tauri/src/manager/webview.rs create mode 100644 crates/tauri/src/manager/window.rs create mode 100644 crates/tauri/src/menu/builders/check.rs create mode 100644 crates/tauri/src/menu/builders/icon.rs create mode 100644 crates/tauri/src/menu/builders/menu.rs create mode 100644 crates/tauri/src/menu/builders/mod.rs create mode 100644 crates/tauri/src/menu/builders/normal.rs create mode 100644 crates/tauri/src/menu/check.rs create mode 100644 crates/tauri/src/menu/icon.rs create mode 100644 crates/tauri/src/menu/menu.rs create mode 100644 crates/tauri/src/menu/mod.rs create mode 100644 crates/tauri/src/menu/normal.rs create mode 100644 crates/tauri/src/menu/plugin.rs create mode 100644 crates/tauri/src/menu/predefined.rs create mode 100644 crates/tauri/src/menu/submenu.rs create mode 100644 crates/tauri/src/path/android.rs create mode 100644 crates/tauri/src/path/desktop.rs create mode 100644 crates/tauri/src/path/init.js create mode 100644 crates/tauri/src/path/mod.rs create mode 100644 crates/tauri/src/path/plugin.rs create mode 100644 crates/tauri/src/pattern.rs create mode 100644 crates/tauri/src/plugin.rs create mode 100644 crates/tauri/src/plugin/mobile.rs create mode 100644 crates/tauri/src/process.rs create mode 100644 crates/tauri/src/protocol/asset.rs create mode 100644 crates/tauri/src/protocol/isolation.rs create mode 100644 crates/tauri/src/protocol/mod.rs create mode 100644 crates/tauri/src/protocol/tauri.rs create mode 100644 crates/tauri/src/resources/mod.rs create mode 100644 crates/tauri/src/resources/plugin.rs create mode 100644 crates/tauri/src/scope/fs.rs create mode 100644 crates/tauri/src/scope/mod.rs create mode 100644 crates/tauri/src/state.rs create mode 100644 crates/tauri/src/test/mock_runtime.rs create mode 100644 crates/tauri/src/test/mod.rs create mode 100644 crates/tauri/src/tray/mod.rs create mode 100644 crates/tauri/src/tray/plugin.rs create mode 100644 crates/tauri/src/vibrancy/macos.rs create mode 100644 crates/tauri/src/vibrancy/mod.rs create mode 100644 crates/tauri/src/vibrancy/windows.rs create mode 100644 crates/tauri/src/webview/mod.rs create mode 100644 crates/tauri/src/webview/plugin.rs create mode 100644 crates/tauri/src/webview/scripts/print.js create mode 100644 crates/tauri/src/webview/scripts/toggle-devtools.js create mode 100644 crates/tauri/src/webview/scripts/zoom-hotkey.js create mode 100644 crates/tauri/src/webview/webview_window.rs create mode 100644 crates/tauri/src/window/mod.rs create mode 100644 crates/tauri/src/window/plugin.rs create mode 100644 crates/tauri/src/window/scripts/drag.js create mode 100644 crates/tauri/test/api/test.txt create mode 100644 crates/tauri/test/api/test_binary create mode 100644 crates/tauri/test/fixture/config.json create mode 100644 crates/tauri/test/fixture/dist/index.html create mode 100644 crates/tauri/test/fixture/isolation/dist/index.html create mode 100644 crates/tauri/test/fixture/isolation/isolation-dist/index.html create mode 100644 crates/tauri/test/fixture/isolation/isolation-dist/index.js create mode 100644 crates/tauri/test/fixture/isolation/src-tauri/icons/icon.ico create mode 100644 crates/tauri/test/fixture/isolation/src-tauri/icons/icon.ico~dev create mode 100644 crates/tauri/test/fixture/isolation/src-tauri/icons/icon.png create mode 100644 crates/tauri/test/fixture/isolation/src-tauri/tauri.conf.json create mode 100644 crates/tauri/test/fixture/src-tauri/icons/icon.ico create mode 100644 crates/tauri/test/fixture/src-tauri/icons/icon.ico~dev create mode 100644 crates/tauri/test/fixture/src-tauri/icons/icon.png create mode 100644 crates/tauri/test/fixture/src-tauri/tauri.conf.json create mode 100644 crates/tauri/test/fixture/test.txt create mode 100644 crates/tauri/test/updater/fixture/archives/archive.linux.tar.gz create mode 100644 crates/tauri/test/updater/fixture/archives/archive.linux.tar.gz.sig create mode 100644 crates/tauri/test/updater/fixture/archives/archive.macos.tar.gz.sig create mode 100644 crates/tauri/test/updater/fixture/archives/archive.windows.zip create mode 100644 crates/tauri/test/updater/fixture/archives/archive.windows.zip.sig create mode 100644 crates/tauri/test/updater/fixture/bad_signature/update.key create mode 100644 crates/tauri/test/updater/fixture/bad_signature/update.key.pub create mode 100644 crates/tauri/test/updater/fixture/good_signature/update.key create mode 100644 crates/tauri/test/updater/fixture/good_signature/update.key.pub create mode 100644 crates/tests/acl/Cargo.toml create mode 100644 crates/tests/acl/fixtures/capabilities/basic-ping/cap.toml create mode 100644 crates/tests/acl/fixtures/capabilities/basic-ping/required-plugins.json create mode 100644 crates/tests/acl/fixtures/capabilities/file-explorer-remote/cap.toml create mode 100644 crates/tests/acl/fixtures/capabilities/file-explorer-remote/required-plugins.json create mode 100644 crates/tests/acl/fixtures/capabilities/file-explorer/cap.toml create mode 100644 crates/tests/acl/fixtures/capabilities/file-explorer/required-plugins.json create mode 100644 crates/tests/acl/fixtures/capabilities/multiwebview/cap.toml create mode 100644 crates/tests/acl/fixtures/capabilities/multiwebview/required-plugins.json create mode 100644 crates/tests/acl/fixtures/capabilities/multiwindow/cap-external.toml create mode 100644 crates/tests/acl/fixtures/capabilities/multiwindow/cap-main.json create mode 100644 crates/tests/acl/fixtures/capabilities/multiwindow/required-plugins.json create mode 100644 crates/tests/acl/fixtures/capabilities/platform-specific-permissions/cap.toml create mode 100644 crates/tests/acl/fixtures/capabilities/platform-specific-permissions/required-plugins.json create mode 100644 crates/tests/acl/fixtures/capabilities/scope-extended/cap.json create mode 100644 crates/tests/acl/fixtures/capabilities/scope-extended/required-plugins.json create mode 100644 crates/tests/acl/fixtures/capabilities/scope/cap.toml create mode 100644 crates/tests/acl/fixtures/capabilities/scope/required-plugins.json create mode 100644 crates/tests/acl/fixtures/plugins/fs/deny-home.toml create mode 100644 crates/tests/acl/fixtures/plugins/fs/move-tmp.toml create mode 100644 crates/tests/acl/fixtures/plugins/fs/read-dir.toml create mode 100644 crates/tests/acl/fixtures/plugins/fs/read-download-dir.toml create mode 100644 crates/tests/acl/fixtures/plugins/fs/read-file.toml create mode 100644 crates/tests/acl/fixtures/plugins/fs/read-resources.toml create mode 100644 crates/tests/acl/fixtures/plugins/fs/read.toml create mode 100644 crates/tests/acl/fixtures/plugins/fs/scope.toml create mode 100644 crates/tests/acl/fixtures/plugins/os/linux.toml create mode 100644 crates/tests/acl/fixtures/plugins/os/macos.toml create mode 100644 crates/tests/acl/fixtures/plugins/os/open-browser.toml create mode 100644 crates/tests/acl/fixtures/plugins/os/windows.toml create mode 100644 crates/tests/acl/fixtures/plugins/ping/ping.toml create mode 100644 crates/tests/acl/fixtures/snapshots/acl_tests__tests__basic-ping.snap create mode 100644 crates/tests/acl/fixtures/snapshots/acl_tests__tests__file-explorer-remote.snap create mode 100644 crates/tests/acl/fixtures/snapshots/acl_tests__tests__file-explorer.snap create mode 100644 crates/tests/acl/fixtures/snapshots/acl_tests__tests__multiwebview.snap create mode 100644 crates/tests/acl/fixtures/snapshots/acl_tests__tests__multiwindow.snap create mode 100644 crates/tests/acl/fixtures/snapshots/acl_tests__tests__scope-extended.snap create mode 100644 crates/tests/acl/fixtures/snapshots/acl_tests__tests__scope.snap create mode 100644 crates/tests/acl/fixtures/snapshots/linux/acl_tests__tests__platform-specific-permissions.snap create mode 100644 crates/tests/acl/fixtures/snapshots/macOS/acl_tests__tests__platform-specific-permissions.snap create mode 100644 crates/tests/acl/fixtures/snapshots/windows/acl_tests__tests__platform-specific-permissions.snap create mode 100644 crates/tests/acl/src/lib.rs create mode 100644 crates/tests/app-updater/frameworks/test.framework/Headers create mode 100644 crates/tests/app-updater/frameworks/test.framework/Modules create mode 100644 crates/tests/app-updater/frameworks/test.framework/Resources create mode 100644 crates/tests/app-updater/frameworks/test.framework/Versions/A/Headers/test.h create mode 100644 crates/tests/app-updater/frameworks/test.framework/Versions/A/Modules/module.modulemap create mode 100644 crates/tests/app-updater/frameworks/test.framework/Versions/A/Resources/Info.plist create mode 100644 crates/tests/app-updater/frameworks/test.framework/Versions/A/_CodeSignature/CodeResources create mode 100644 crates/tests/app-updater/frameworks/test.framework/Versions/A/test create mode 100644 crates/tests/app-updater/frameworks/test.framework/Versions/Current create mode 100644 crates/tests/restart/Cargo.toml create mode 100644 crates/tests/restart/LICENSE-APACHE-2.0 create mode 100644 crates/tests/restart/LICENSE-MIT create mode 100644 crates/tests/restart/LICENSE.spdx create mode 100644 crates/tests/restart/build.rs create mode 100644 crates/tests/restart/src/main.rs create mode 100644 crates/tests/restart/tests/restart.rs create mode 100644 dependabot.yml create mode 100644 examples/.icons/128x128.png create mode 100644 examples/.icons/128x128@2x.png create mode 100644 examples/.icons/32x32.png create mode 100644 examples/.icons/AppIcon.icon/Assets/icon.png create mode 100644 examples/.icons/AppIcon.icon/icon.json create mode 100644 examples/.icons/Square107x107Logo.png create mode 100644 examples/.icons/Square142x142Logo.png create mode 100644 examples/.icons/Square150x150Logo.png create mode 100644 examples/.icons/Square284x284Logo.png create mode 100644 examples/.icons/Square30x30Logo.png create mode 100644 examples/.icons/Square310x310Logo.png create mode 100644 examples/.icons/Square44x44Logo.png create mode 100644 examples/.icons/Square71x71Logo.png create mode 100644 examples/.icons/Square89x89Logo.png create mode 100644 examples/.icons/StoreLogo.png create mode 100644 examples/.icons/icon.ico create mode 100644 examples/.icons/icon.png create mode 100644 examples/.icons/tray_icon.png create mode 100644 examples/.icons/tray_icon_with_transparency.ico create mode 100644 examples/.icons/tray_icon_with_transparency.png create mode 100644 examples/README.md create mode 100644 examples/api/.gitignore create mode 100644 examples/api/.setup-cross.sh create mode 100644 examples/api/.taurignore create mode 100644 examples/api/README.md create mode 100644 examples/api/dist/.gitkeep create mode 100644 examples/api/index.html create mode 100644 examples/api/isolation-dist/index.html create mode 100644 examples/api/isolation-dist/index.js create mode 100644 examples/api/package.json create mode 100644 examples/api/public/tauri_logo.png create mode 100644 examples/api/src-tauri/.gitignore create mode 100644 examples/api/src-tauri/.taurignore create mode 100644 examples/api/src-tauri/Cargo.toml create mode 100644 examples/api/src-tauri/Cross.toml create mode 100644 examples/api/src-tauri/Info.plist create mode 100644 examples/api/src-tauri/build.rs create mode 100644 examples/api/src-tauri/capabilities/.gitignore create mode 100644 examples/api/src-tauri/capabilities/main.json create mode 100644 examples/api/src-tauri/capabilities/run-app.json create mode 100644 examples/api/src-tauri/locales/pt-BR.wxl create mode 100644 examples/api/src-tauri/permissions/app-menu/default.toml create mode 100644 examples/api/src-tauri/permissions/autogenerated/echo.toml create mode 100644 examples/api/src-tauri/permissions/autogenerated/log_operation.toml create mode 100644 examples/api/src-tauri/permissions/autogenerated/perform_request.toml create mode 100644 examples/api/src-tauri/permissions/autogenerated/spam.toml create mode 100644 examples/api/src-tauri/src/cmd.rs create mode 100644 examples/api/src-tauri/src/lib.rs create mode 100644 examples/api/src-tauri/src/main.rs create mode 100644 examples/api/src-tauri/src/menu_plugin.rs create mode 100644 examples/api/src-tauri/src/tray.rs create mode 100644 examples/api/src-tauri/tauri-plugin-sample/.gitignore create mode 100644 examples/api/src-tauri/tauri-plugin-sample/Cargo.toml create mode 100644 examples/api/src-tauri/tauri-plugin-sample/android/.gitignore create mode 100644 examples/api/src-tauri/tauri-plugin-sample/android/build.gradle.kts create mode 100644 examples/api/src-tauri/tauri-plugin-sample/android/consumer-rules.pro create mode 100644 examples/api/src-tauri/tauri-plugin-sample/android/settings.gradle create mode 100644 examples/api/src-tauri/tauri-plugin-sample/android/src/androidTest/java/com/plugin/sample/ExampleInstrumentedTest.kt create mode 100644 examples/api/src-tauri/tauri-plugin-sample/android/src/main/AndroidManifest.xml create mode 100644 examples/api/src-tauri/tauri-plugin-sample/android/src/main/java/com/plugin/sample/Example.kt create mode 100644 examples/api/src-tauri/tauri-plugin-sample/android/src/main/java/com/plugin/sample/ExamplePlugin.kt create mode 100644 examples/api/src-tauri/tauri-plugin-sample/android/src/test/java/com/plugin/sample/ExampleUnitTest.kt create mode 100644 examples/api/src-tauri/tauri-plugin-sample/api-iife.js create mode 100644 examples/api/src-tauri/tauri-plugin-sample/build.rs create mode 100644 examples/api/src-tauri/tauri-plugin-sample/ios/.gitignore create mode 100644 examples/api/src-tauri/tauri-plugin-sample/ios/Package.swift create mode 100644 examples/api/src-tauri/tauri-plugin-sample/ios/README.md create mode 100644 examples/api/src-tauri/tauri-plugin-sample/ios/Sources/ExamplePlugin.swift create mode 100644 examples/api/src-tauri/tauri-plugin-sample/ios/Tests/PluginTests/PluginTests.swift create mode 100644 examples/api/src-tauri/tauri-plugin-sample/permissions/.gitignore create mode 100644 examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/commands/js_values.toml create mode 100644 examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/commands/ping.toml create mode 100644 examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/reference.md create mode 100644 examples/api/src-tauri/tauri-plugin-sample/permissions/global-scope.toml create mode 100644 examples/api/src-tauri/tauri-plugin-sample/permissions/ping-scoped.toml create mode 100644 examples/api/src-tauri/tauri-plugin-sample/src/desktop.rs create mode 100644 examples/api/src-tauri/tauri-plugin-sample/src/error.rs create mode 100644 examples/api/src-tauri/tauri-plugin-sample/src/lib.rs create mode 100644 examples/api/src-tauri/tauri-plugin-sample/src/mobile.rs create mode 100644 examples/api/src-tauri/tauri-plugin-sample/src/models.rs create mode 100644 examples/api/src-tauri/tauri.conf.json create mode 100644 examples/api/src/App.svelte create mode 100644 examples/api/src/app.css create mode 100644 examples/api/src/components/MenuBuilder.svelte create mode 100644 examples/api/src/components/MenuItemComponent.svelte create mode 100644 examples/api/src/main.ts create mode 100644 examples/api/src/views/App.svelte create mode 100644 examples/api/src/views/Communication.svelte create mode 100644 examples/api/src/views/Menu.svelte create mode 100644 examples/api/src/views/Tray.svelte create mode 100644 examples/api/src/views/WebRTC.svelte create mode 100644 examples/api/src/views/Welcome.svelte create mode 100644 examples/api/src/views/Window.svelte create mode 100644 examples/api/src/vite-env.d.ts create mode 100644 examples/api/svelte.config.js create mode 100644 examples/api/tsconfig.json create mode 100644 examples/api/unocss.config.js create mode 100644 examples/api/vite.config.js create mode 100644 examples/commands/README.md create mode 100644 examples/commands/commands.rs create mode 100644 examples/commands/index.html create mode 100644 examples/commands/main.rs create mode 100644 examples/commands/tauri.conf.json create mode 100644 examples/drag/README.md create mode 100644 examples/drag/index.html create mode 100644 examples/drag/main.rs create mode 100644 examples/drag/tauri.conf.json create mode 100644 examples/file-associations/README.md create mode 100644 examples/file-associations/index.html create mode 100644 examples/file-associations/package.json create mode 100644 examples/file-associations/src-tauri/.license_template create mode 100644 examples/file-associations/src-tauri/Cargo.toml create mode 100644 examples/file-associations/src-tauri/Info.plist create mode 100644 examples/file-associations/src-tauri/build.rs create mode 100644 examples/file-associations/src-tauri/src/main.rs create mode 100644 examples/file-associations/src-tauri/tauri.conf.json create mode 100644 examples/helloworld/README.md create mode 100644 examples/helloworld/index.html create mode 100644 examples/helloworld/main.rs create mode 100644 examples/helloworld/tauri.conf.json create mode 100644 examples/isolation/README.md create mode 100644 examples/isolation/dist/index.html create mode 100644 examples/isolation/dist/linked.js create mode 100644 examples/isolation/isolation-dist/index.html create mode 100644 examples/isolation/isolation-dist/index.js create mode 100644 examples/isolation/main.rs create mode 100644 examples/isolation/tauri.conf.json create mode 100644 examples/multiwebview/README.md create mode 100644 examples/multiwebview/index.html create mode 100644 examples/multiwebview/main.rs create mode 100644 examples/multiwebview/tauri.conf.json create mode 100644 examples/multiwindow/README.md create mode 100644 examples/multiwindow/index.html create mode 100644 examples/multiwindow/main.rs create mode 100644 examples/multiwindow/tauri.conf.json create mode 100644 examples/resources/README.md create mode 100644 examples/resources/index.html create mode 100644 examples/resources/package.json create mode 100644 examples/resources/src-tauri/Cargo.toml create mode 100644 examples/resources/src-tauri/assets/index.js create mode 100644 examples/resources/src-tauri/build.rs create mode 100644 examples/resources/src-tauri/capabilities/app.json create mode 100644 examples/resources/src-tauri/src/main.rs create mode 100644 examples/resources/src-tauri/tauri.conf.json create mode 100644 examples/run-return/README.md create mode 100644 examples/run-return/index.html create mode 100644 examples/run-return/main.rs create mode 100644 examples/run-return/tauri.conf.json create mode 100644 examples/splashscreen/README.md create mode 100644 examples/splashscreen/dist/icon.png create mode 100644 examples/splashscreen/dist/index.html create mode 100644 examples/splashscreen/dist/splashscreen.html create mode 100644 examples/splashscreen/main.rs create mode 100644 examples/splashscreen/tauri.conf.json create mode 100644 examples/state/README.md create mode 100644 examples/state/index.html create mode 100644 examples/state/main.rs create mode 100644 examples/state/tauri.conf.json create mode 100644 examples/streaming/README.md create mode 100644 examples/streaming/index.html create mode 100644 examples/streaming/main.rs create mode 100644 examples/streaming/tauri.conf.json create mode 100644 package.json create mode 100644 packages/api-e2e/README.md create mode 100644 packages/api-e2e/package.json create mode 100644 packages/api-e2e/tauri.e2e.conf.json create mode 100644 packages/api-e2e/test/helpers/index.ts create mode 100644 packages/api-e2e/test/specs/app.spec.ts create mode 100644 packages/api-e2e/test/specs/core.spec.ts create mode 100644 packages/api-e2e/test/specs/dpi.spec.ts create mode 100644 packages/api-e2e/test/specs/event.spec.ts create mode 100644 packages/api-e2e/test/specs/image.spec.ts create mode 100644 packages/api-e2e/test/specs/menu.spec.ts create mode 100644 packages/api-e2e/test/specs/path.spec.ts create mode 100644 packages/api-e2e/test/specs/tray.spec.ts create mode 100644 packages/api-e2e/test/specs/webview.spec.ts create mode 100644 packages/api-e2e/test/specs/webviewWindow.spec.ts create mode 100644 packages/api-e2e/test/specs/window.spec.ts create mode 100644 packages/api-e2e/test/types/global.d.ts create mode 100644 packages/api-e2e/tsconfig.json create mode 100644 packages/api-e2e/wdio.android.conf.ts create mode 100644 packages/api-e2e/wdio.conf.ts create mode 100644 packages/api-e2e/wdio.ios.conf.ts create mode 100644 packages/api-e2e/wdio.mobile.ts create mode 100644 packages/api/.gitignore create mode 100644 packages/api/CHANGELOG.md create mode 100644 packages/api/LICENSE-APACHE-2.0 create mode 100644 packages/api/LICENSE-MIT create mode 100644 packages/api/README.md create mode 100644 packages/api/eslint.config.js create mode 100644 packages/api/package.json create mode 100644 packages/api/rollup.config.ts create mode 100644 packages/api/src/app.ts create mode 100644 packages/api/src/core.ts create mode 100644 packages/api/src/dpi.ts create mode 100644 packages/api/src/event.ts create mode 100644 packages/api/src/global.d.ts create mode 100644 packages/api/src/image.ts create mode 100644 packages/api/src/index.ts create mode 100644 packages/api/src/menu.ts create mode 100644 packages/api/src/menu/base.ts create mode 100644 packages/api/src/menu/checkMenuItem.ts create mode 100644 packages/api/src/menu/iconMenuItem.ts create mode 100644 packages/api/src/menu/menu.ts create mode 100644 packages/api/src/menu/menuItem.ts create mode 100644 packages/api/src/menu/predefinedMenuItem.ts create mode 100644 packages/api/src/menu/submenu.ts create mode 100644 packages/api/src/mocks.ts create mode 100644 packages/api/src/path.ts create mode 100644 packages/api/src/tray.ts create mode 100644 packages/api/src/webview.ts create mode 100644 packages/api/src/webviewWindow.ts create mode 100644 packages/api/src/window.ts create mode 100644 packages/api/tsconfig.json create mode 100644 packages/cli/.cargo/config.toml create mode 100644 packages/cli/.gitignore create mode 100644 packages/cli/.npmignore create mode 100644 packages/cli/Cargo.toml create mode 100644 packages/cli/LICENSE-APACHE-2.0 create mode 100644 packages/cli/LICENSE-MIT create mode 100644 packages/cli/README.md create mode 100644 packages/cli/__tests__/fixtures/empty/.gitignore create mode 100644 packages/cli/__tests__/fixtures/empty/dist/index.html create mode 100644 packages/cli/__tests__/fixtures/empty/package.json create mode 100644 packages/cli/__tests__/template.spec.ts create mode 100644 packages/cli/append-headers.js create mode 100644 packages/cli/build.rs create mode 100644 packages/cli/index.d.ts create mode 100644 packages/cli/index.js create mode 100644 packages/cli/main.d.ts create mode 100644 packages/cli/main.js create mode 100644 packages/cli/npm/darwin-arm64/README.md create mode 100644 packages/cli/npm/darwin-arm64/package.json create mode 100644 packages/cli/npm/darwin-x64/README.md create mode 100644 packages/cli/npm/darwin-x64/package.json create mode 100644 packages/cli/npm/linux-arm-gnueabihf/README.md create mode 100644 packages/cli/npm/linux-arm-gnueabihf/package.json create mode 100644 packages/cli/npm/linux-arm64-gnu/README.md create mode 100644 packages/cli/npm/linux-arm64-gnu/package.json create mode 100644 packages/cli/npm/linux-arm64-musl/README.md create mode 100644 packages/cli/npm/linux-arm64-musl/package.json create mode 100644 packages/cli/npm/linux-riscv64-gnu/README.md create mode 100644 packages/cli/npm/linux-riscv64-gnu/package.json create mode 100644 packages/cli/npm/linux-x64-gnu/README.md create mode 100644 packages/cli/npm/linux-x64-gnu/package.json create mode 100644 packages/cli/npm/linux-x64-musl/README.md create mode 100644 packages/cli/npm/linux-x64-musl/package.json create mode 100644 packages/cli/npm/win32-arm64-msvc/README.md create mode 100644 packages/cli/npm/win32-arm64-msvc/package.json create mode 100644 packages/cli/npm/win32-ia32-msvc/README.md create mode 100644 packages/cli/npm/win32-ia32-msvc/package.json create mode 100644 packages/cli/npm/win32-x64-msvc/README.md create mode 100644 packages/cli/npm/win32-x64-msvc/package.json create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/lib.rs create mode 100644 packages/cli/tauri.js create mode 100644 pnpm-workspace.yaml create mode 100644 renovate.json create mode 100644 rustfmt.toml create mode 100644 supply-chain/audits.toml create mode 100644 supply-chain/config.toml create mode 100644 supply-chain/imports.lock diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 0000000..b8a4b1e --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,14 @@ +[advisories] +ignore = [ + # rsa Marvin Attack + "RUSTSEC-2023-0071", + # difference is unmaintained + "RUSTSEC-2020-0095", + # proc-macro-error is unmaintained + "RUSTSEC-2024-0370", + # 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", +] diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..4de6d52 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ +[env] +# workaround needed to prevent `STATUS_ENTRYPOINT_NOT_FOUND` error in tests +# see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864 +__TAURI_WORKSPACE__ = "true" diff --git a/.changes/README.md b/.changes/README.md new file mode 100644 index 0000000..318eea0 --- /dev/null +++ b/.changes/README.md @@ -0,0 +1,44 @@ +# Changes + +##### via https://github.com/jbolda/covector + +As you create PRs and make changes that require a version bump, please add a new markdown file in this folder. You do not note the version _number_, but rather the type of bump that you expect: major, minor, or patch. The filename is not important, as long as it is a `.md`, but we recommend that it represents the overall change for organizational purposes. + +When you select the version bump required, you do _not_ need to consider dependencies. Only note the package with the actual change, and any packages that depend on that package will be bumped automatically in the process. + +Use the following format: + +```md +--- +'package-a': 'patch:enhance' +'package-b': 'patch:enhance' +--- + +Change summary goes here +``` + +Summaries do not have a specific character limit, but are text only. These summaries are used within the (future implementation of) changelogs. They will give context to the change and also point back to the original PR if more details and context are needed. + +Changes will be designated as a `major`, `minor` or `patch` as further described in [semver](https://semver.org/). + +Given a version number MAJOR.MINOR.PATCH, increment the: + +- MAJOR version when you make incompatible API changes, +- MINOR version when you add functionality in a backwards compatible manner, and +- PATCH version when you make backwards compatible bug fixes. + +Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format, but will be discussed prior to usage (as extra steps will be necessary in consideration of merging and publishing). + +Additionally you could specify a tag for the change file to group it with other changes by prefixing the bump with `:`, for example: + +```md +--- +'package-a': 'patch:enhance' +--- + +Change summary goes here +``` + +which will group this change file with other changes that specify the `bug` tag. + +For list of available tags, see the `changeTags` key in [./config.json](./config.json) diff --git a/.changes/acl-deny-execution-context.md b/.changes/acl-deny-execution-context.md new file mode 100644 index 0000000..b07ca84 --- /dev/null +++ b/.changes/acl-deny-execution-context.md @@ -0,0 +1,5 @@ +--- +tauri: patch:bug +--- + +Fix the runtime ACL denying a command for every origin when any capability denied it: deny permissions are now correctly scoped to the capability's execution context (local/remote) instead of denying the command for every origin. Previously a capability that denied a command for a remote URL also denied it for the local app (and vice-versa), because the origin match result was discarded. The debug message reporting an explicit denial now also only references the capabilities that actually deny the requesting origin. diff --git a/.changes/additional-watch-folders-alias.md b/.changes/additional-watch-folders-alias.md new file mode 100644 index 0000000..1fa7684 --- /dev/null +++ b/.changes/additional-watch-folders-alias.md @@ -0,0 +1,5 @@ +--- +"tauri-utils": patch:bug +--- + +Added `additional-watch-folders` as an alias for the `build > additionalWatchFolders` configuration value, so the kebab-case spelling that matches the camelCase key is accepted in `Tauri.toml`. The previous `additional-watch-directories` alias keeps working. diff --git a/.changes/allow-crlf-cargo-toml.md b/.changes/allow-crlf-cargo-toml.md new file mode 100644 index 0000000..d1afc8b --- /dev/null +++ b/.changes/allow-crlf-cargo-toml.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:enhance +"@tauri-apps/cli": patch:enhance +--- + +Don't always rewrite `Cargo.toml` file from CRLF line endings to LF diff --git a/.changes/android-compileroptions-dsl.md b/.changes/android-compileroptions-dsl.md new file mode 100644 index 0000000..96c2c86 --- /dev/null +++ b/.changes/android-compileroptions-dsl.md @@ -0,0 +1,9 @@ +--- +"tauri": patch:enhance +"tauri-cli": patch:enhance +"@tauri-apps/cli": patch:enhance +--- + +Migrate the Android Gradle scripts from the deprecated `kotlinOptions` DSL to `compilerOptions`, which is accepted by both Kotlin Gradle Plugin 1.9.x and 2.x. This lets projects move to Kotlin 2.x without hitting the hard error that 2.3+ raises on the old DSL. + +This increased the minimum supported Gradle version to 8.13, if your `gradle` is on an earlier version, delete `src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties` and re-run `tauri android init` to update it. diff --git a/.changes/android-consumer-proguard.md b/.changes/android-consumer-proguard.md new file mode 100644 index 0000000..b320eab --- /dev/null +++ b/.changes/android-consumer-proguard.md @@ -0,0 +1,23 @@ +--- +tauri: minor:deps +tauri-cli: minor:deps +"@tauri-apps/cli": minor:deps +--- + +On Android, fix missing `consumer-rules.pro` file in the template. + +**IMPORTANT**: For plugin authors, update your `build.gradle.kts` file to remove the + +```kotlin + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } +``` + +section and rename your `proguard-rules.pro` to `consumer-rules.pro` to match the `consumerProguardFiles("consumer-rules.pro")` in the template. diff --git a/.changes/android-graceful-exit.md b/.changes/android-graceful-exit.md new file mode 100644 index 0000000..9e3fcbb --- /dev/null +++ b/.changes/android-graceful-exit.md @@ -0,0 +1,5 @@ +--- +'tauri': 'patch:bug' +--- + +On Android, `AppHandle::exit` (and the process plugin `exit` command) now finishes the activity instead of exiting the process directly, so the app closes with the system transition instead of flashing a blank screen. diff --git a/.changes/android-gradle-9.md b/.changes/android-gradle-9.md new file mode 100644 index 0000000..0d38c05 --- /dev/null +++ b/.changes/android-gradle-9.md @@ -0,0 +1,8 @@ +--- +tauri: minor:deps +tauri-build: minor:deps +tauri-cli: minor:deps +"@tauri-apps/cli": minor:deps +--- + +On Android, updated the template to use Gradle v9.6.1 (`com.android.tools.build:gradle` v9.3.1) and Kotlin v2.2. Use `tauri android init` to apply the change. diff --git a/.changes/android-java-gradle-compat-hint.md b/.changes/android-java-gradle-compat-hint.md new file mode 100644 index 0000000..e7314dd --- /dev/null +++ b/.changes/android-java-gradle-compat-hint.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:enhance +"@tauri-apps/cli": patch:enhance +--- + +Warn during Android commands (`init`/`dev`/`build`) when the active Java version is too new for the Gradle version the project uses (e.g. Java 27 against the Gradle 9.6.1 the template ships, or Java 25 against a project still on Gradle 8.14), instead of letting the build fail later with a cryptic error. The warning points to the Gradle/Java compatibility matrix and suggests a supported JDK. diff --git a/.changes/android-json-org-serialization.md b/.changes/android-json-org-serialization.md new file mode 100644 index 0000000..9d9976c --- /dev/null +++ b/.changes/android-json-org-serialization.md @@ -0,0 +1,5 @@ +--- +tauri: patch:bug +--- + +On Android, `Invoke.resolveObject` and the other values serialized with the plugin JSON mapper now write `JSObject` and `JSArray` as JSON objects and arrays. They were serialized through their private fields, so a `JSArray` reached the frontend as `{ "values": [...] }` and a `JSObject` as `{ "nameValuePairs": {...} }`. diff --git a/.changes/android-material3.md b/.changes/android-material3.md new file mode 100644 index 0000000..1f013f7 --- /dev/null +++ b/.changes/android-material3.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:enhance +"@tauri-apps/cli": patch:enhance +--- + +Use `Theme.Material3.DayNight.NoActionBar` instead of `Theme.MaterialComponents.DayNight.NoActionBar` when running `tauri android init` diff --git a/.changes/android-plugin-manager-no-activity.md b/.changes/android-plugin-manager-no-activity.md new file mode 100644 index 0000000..90bc3bd --- /dev/null +++ b/.changes/android-plugin-manager-no-activity.md @@ -0,0 +1,5 @@ +--- +tauri: patch:bug +--- + +On Android, fixed requesting permission in a plugin fails after the first activity closed. diff --git a/.changes/android-plugin-manager-relaunch.md b/.changes/android-plugin-manager-relaunch.md new file mode 100644 index 0000000..eeb27f3 --- /dev/null +++ b/.changes/android-plugin-manager-relaunch.md @@ -0,0 +1,5 @@ +--- +tauri: patch:bug +--- + +On Android, fixed a crash when an activity is destroyed while another one is already running — installing an APK over the running app is the common way to hit it. The plugin manager tried to move its activity result launchers to the surviving activity, and `registerForActivityResult` rejects that with `IllegalStateException: LifecycleOwner ... is attempting to register while current state is RESUMED`. Each activity now registers its own launchers when it is created. diff --git a/.changes/android-target-sdk-37.md b/.changes/android-target-sdk-37.md new file mode 100644 index 0000000..beac94f --- /dev/null +++ b/.changes/android-target-sdk-37.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:enhance +"@tauri-apps/cli": patch:enhance +--- + +Update template to use `targetSdk = 37` diff --git a/.changes/android-video-dir.md b/.changes/android-video-dir.md new file mode 100644 index 0000000..31aff2f --- /dev/null +++ b/.changes/android-video-dir.md @@ -0,0 +1,7 @@ +--- +tauri: minor:breaking +--- + +On Android, `$VIDEO` and `video_dir()` now resolve to the app-specific Movies directory instead of external cache storage. + +**Migration:** Files previously written to the old location (`.../cache`) will not be discovered at the new location (`.../files/Movies`). Migrate existing files or update path assumptions accordingly. diff --git a/.changes/app-bound-domains.md b/.changes/app-bound-domains.md new file mode 100644 index 0000000..ea5a24f --- /dev/null +++ b/.changes/app-bound-domains.md @@ -0,0 +1,8 @@ +--- +"tauri": minor:feat +"tauri-runtime": minor:feat +"tauri-runtime-wry": minor:feat +"tauri-utils": minor:feat +--- + +Add `WebviewBuilder::limit_navigations_to_app_bound_domains`, `WebviewWindowBuilder::limit_navigations_to_app_bound_domains`, and limitNavigationsToAppBoundDomains to tauri.config.json. diff --git a/.changes/app-exit-api.md b/.changes/app-exit-api.md new file mode 100644 index 0000000..cf5fe16 --- /dev/null +++ b/.changes/app-exit-api.md @@ -0,0 +1,6 @@ +--- +'tauri': 'minor:feat' +'@tauri-apps/api': 'minor:feat' +--- + +Added the `exit` function to `@tauri-apps/api/app`, backed by the new `plugin:app|exit` command (`core:app:allow-exit` permission), to exit the app without requiring the `@tauri-apps/plugin-process` plugin. diff --git a/.changes/appimage-respect-gdk-backend.md b/.changes/appimage-respect-gdk-backend.md new file mode 100644 index 0000000..4464422 --- /dev/null +++ b/.changes/appimage-respect-gdk-backend.md @@ -0,0 +1,5 @@ +--- +"tauri-bundler": "patch:bug" +--- + +Respect an explicitly configured `GDK_BACKEND` in AppImage GTK hooks while retaining `x11` as the default. diff --git a/.changes/asset-protocol-multi-range.md b/.changes/asset-protocol-multi-range.md new file mode 100644 index 0000000..da071fa --- /dev/null +++ b/.changes/asset-protocol-multi-range.md @@ -0,0 +1,5 @@ +--- +tauri: patch:bug +--- + +Fix malformed `asset://` multi-range responses: the multipart body now ends with the closing boundary, `Content-Type` is only `multipart/byteranges`, and the status is `206 Partial Content`. diff --git a/.changes/asset-protocol-off-main-thread.md b/.changes/asset-protocol-off-main-thread.md new file mode 100644 index 0000000..1294c1b --- /dev/null +++ b/.changes/asset-protocol-off-main-thread.md @@ -0,0 +1,5 @@ +--- +tauri: patch:bug +--- + +Load `asset://` files asynchronously instead of on the event loop thread, so a slow or unreachable path no longer freezes every window. diff --git a/.changes/brotli-9.md b/.changes/brotli-9.md new file mode 100644 index 0000000..8fe43b4 --- /dev/null +++ b/.changes/brotli-9.md @@ -0,0 +1,6 @@ +--- +"tauri-codegen": patch:deps +"tauri-utils": patch:deps +--- + +Updated brotli to v9 diff --git a/.changes/build-config-path.md b/.changes/build-config-path.md new file mode 100644 index 0000000..a86e359 --- /dev/null +++ b/.changes/build-config-path.md @@ -0,0 +1,5 @@ +--- +"tauri-build": minor:feat +--- + +Added `Attributes::config_path` to customize config path, deprecated `CodegenContext::config_path` in favor of this diff --git a/.changes/build-windows-icon-config-root.md b/.changes/build-windows-icon-config-root.md new file mode 100644 index 0000000..8cc8182 --- /dev/null +++ b/.changes/build-windows-icon-config-root.md @@ -0,0 +1,5 @@ +--- +"tauri-build": patch:bug +--- + +The `.ico` from `bundle > icon` (or the `icons/icon.ico` fallback) embedded as the Windows application icon is now resolved relative to the config file directory instead of the build script's working directory, so it works with `Attributes::config_path`. diff --git a/.changes/bundle-vc-runtime.md b/.changes/bundle-vc-runtime.md new file mode 100644 index 0000000..97291e2 --- /dev/null +++ b/.changes/bundle-vc-runtime.md @@ -0,0 +1,8 @@ +--- +"tauri-bundler": "minor:feat" +"tauri-cli": "minor:feat" +"@tauri-apps/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/bundler-breaking-changes.md b/.changes/bundler-breaking-changes.md new file mode 100644 index 0000000..dba5f2b --- /dev/null +++ b/.changes/bundler-breaking-changes.md @@ -0,0 +1,5 @@ +--- +tauri-bundler: patch:deps +--- + +**Breaking Change:** Updated various dependencies and removed `goblin`. Some of these dependencies are part of the public API which makes this a breaking change. Added a warning about `tauri-bundler`'s API stability in reflection to past regular breaking struct changes. diff --git a/.changes/bundler-show-sizes.md b/.changes/bundler-show-sizes.md new file mode 100644 index 0000000..6525c80 --- /dev/null +++ b/.changes/bundler-show-sizes.md @@ -0,0 +1,7 @@ +--- +"tauri-bundler": "minor:feat" +"tauri-cli": "minor:feat" +"@tauri-apps/cli": "minor:feat" +--- + +The bundler now prints the size of each generated bundle next to its path in the `Finished N bundles at:` output (directories such as macOS `.app` bundles are measured recursively). diff --git a/.changes/cargo-toml-1.md b/.changes/cargo-toml-1.md new file mode 100644 index 0000000..11fc6f1 --- /dev/null +++ b/.changes/cargo-toml-1.md @@ -0,0 +1,6 @@ +--- +tauri: patch:deps +tauri-build: patch:deps +--- + +Updated `cargo_toml` crate to `1` diff --git a/.changes/change-pr-15578.md b/.changes/change-pr-15578.md new file mode 100644 index 0000000..21d45a2 --- /dev/null +++ b/.changes/change-pr-15578.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:enhance +--- + +`State` had `Send` and `Sync` trait bounds that were already implied, remove them from the struct definition. diff --git a/.changes/change-pr-15593.md b/.changes/change-pr-15593.md new file mode 100644 index 0000000..712620a --- /dev/null +++ b/.changes/change-pr-15593.md @@ -0,0 +1,5 @@ +--- +"tauri-macos-sign": patch:deps +--- + +Updated dependency `apple-codesign` to `0.29`, no user facing changes. diff --git a/.changes/change-pr-15677.md b/.changes/change-pr-15677.md new file mode 100644 index 0000000..46399a3 --- /dev/null +++ b/.changes/change-pr-15677.md @@ -0,0 +1,5 @@ +--- +"tauri-utils": patch:deps +--- + +Update `serial_test` to 3.5 to pull in the updated `scc` and fix RUSTSEC advisory diff --git a/.changes/change-pr-15952.md b/.changes/change-pr-15952.md new file mode 100644 index 0000000..6c47a33 --- /dev/null +++ b/.changes/change-pr-15952.md @@ -0,0 +1,6 @@ +--- +"tauri": patch:changes +--- + +Internal refactors removing an `Arc` and a `Mutex`. +Deprecate `InvokeMessage::state` and `InvokeMessage::state_ref` that should accidentally were made public. diff --git a/.changes/clarify-init-frontend-command-prompts.md b/.changes/clarify-init-frontend-command-prompts.md new file mode 100644 index 0000000..91d0dfc --- /dev/null +++ b/.changes/clarify-init-frontend-command-prompts.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": "patch:enhance" +"@tauri-apps/cli": "patch:enhance" +--- + +Clarify that the `tauri init` frontend commands run before `tauri dev` and `tauri build`, and can be left empty when they are not needed. diff --git a/.changes/cleanup-js-menu-channel.md b/.changes/cleanup-js-menu-channel.md new file mode 100644 index 0000000..bfd36b5 --- /dev/null +++ b/.changes/cleanup-js-menu-channel.md @@ -0,0 +1,5 @@ +--- +"tauri": "patch:bug" +--- + +Remove the `Channel` used to send event to JavaScript side on dropping the menu diff --git a/.changes/cli-dev-kill-process-tree.md b/.changes/cli-dev-kill-process-tree.md new file mode 100644 index 0000000..7f47727 --- /dev/null +++ b/.changes/cli-dev-kill-process-tree.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:bug +"@tauri-apps/cli": patch:bug +--- + +`tauri dev` now kills the whole process tree of the running app when it restarts or exits, so processes spawned by the app (e.g. sidecars) are no longer left orphaned. diff --git a/.changes/cli-force-noninteractive.md b/.changes/cli-force-noninteractive.md new file mode 100644 index 0000000..370590d --- /dev/null +++ b/.changes/cli-force-noninteractive.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'patch:enhance' +'@tauri-apps/cli': 'patch:enhance' +--- + +When stdin is not a terminal, `tauri init` now automatically skips prompts, avoiding IO errors in CI and scripts. This eliminates the need to pass `--ci` explicitly in non-interactive environments. diff --git a/.changes/cli-gradlew-crlf.md b/.changes/cli-gradlew-crlf.md new file mode 100644 index 0000000..4bcca6b --- /dev/null +++ b/.changes/cli-gradlew-crlf.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:bug +"@tauri-apps/cli": patch:bug +--- + +Normalize `gen/android/gradlew` CRLF line endings to LF on all host platforms, not only Unix. A `gradlew` checked out with CRLF broke `sh ./gradlew` on Windows hosts using Git Bash. The rewrite only runs when a CRLF is actually present. A failure to rewrite aborts on Unix, where the script is executed directly; on Windows the CLI invokes `gradlew.bat`, so failures there only warn. diff --git a/.changes/cli-help-text-fixes.md b/.changes/cli-help-text-fixes.md new file mode 100644 index 0000000..0bf5eb2 --- /dev/null +++ b/.changes/cli-help-text-fixes.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": "patch:bug" +"@tauri-apps/cli": "patch:bug" +--- + +Fix the `tauri capability new` command description, which said "Create a new permission file", and the `--skip-stapling` help text on `tauri build` and `tauri bundle`, whose first line described the opposite of what the flag does. diff --git a/.changes/cli-ios-sim-build-rebuild.md b/.changes/cli-ios-sim-build-rebuild.md new file mode 100644 index 0000000..446de27 --- /dev/null +++ b/.changes/cli-ios-sim-build-rebuild.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:bug +"@tauri-apps/cli": patch:bug +--- + +`tauri ios build` for a simulator target no longer fails with "failed to rename app: Directory not empty" when the output `.app` from a previous build exists. diff --git a/.changes/config.json b/.changes/config.json new file mode 100644 index 0000000..97a6917 --- /dev/null +++ b/.changes/config.json @@ -0,0 +1,224 @@ +{ + "gitSiteUrl": "https://www.github.com/tauri-apps/tauri/", + "changeTags": { + "feat": "New Features", + "enhance": "Enhancements", + "bug": "Bug Fixes", + "perf": "Performance Improvements", + "changes": "What's Changed", + "sec": "Security fixes", + "deps": "Dependencies", + "breaking": "Breaking Changes" + }, + "defaultChangeTag": "changes", + "pkgManagers": { + "rust": { + "version": true, + "getPublishedVersion": { + "use": "fetch:check", + "options": { + "url": "https://crates.io/api/v1/crates/${ pkg.pkgFile.pkg.package.name }/${ pkg.pkgFile.version }" + } + }, + "prepublish": [ + "cargo install cargo-audit --features=fix", + { + "command": "echo '
\n

Cargo Audit

\n\n```'", + "dryRunCommand": true, + "pipe": true + }, + { + "command": "cargo audit ${ process.env.CARGO_AUDIT_OPTIONS || '' }", + "dryRunCommand": true, + "runFromRoot": true, + "pipe": true + }, + { + "command": "echo '```\n\n
\n'", + "dryRunCommand": true, + "pipe": true + } + ], + "publish": [ + { + "command": "echo '
\n

Cargo Publish

\n\n```'", + "dryRunCommand": true, + "pipe": true + }, + { + "command": "cargo publish", + "dryRunCommand": "cargo publish --dry-run", + "pipe": true + }, + { + "command": "echo '```\n\n
\n'", + "dryRunCommand": true, + "pipe": true + } + ], + "postpublish": { + "use": "fetch:check", + "options": { + "url": "https://crates.io/api/v1/crates/${ pkg.pkgFile.pkg.package.name }/${ pkg.pkgFile.version }" + }, + "retries": [5000, 5000, 5000] + } + }, + "javascript": { + "version": true, + "getPublishedVersion": { + "use": "fetch:check", + "options": { + "url": "https://registry.npmjs.com/${ pkg.pkgFile.pkg.name }/${ pkg.pkgFile.version }" + } + }, + "prepublish": [ + { + "command": "echo '
\n

PNPM Audit

\n\n```'", + "dryRunCommand": true, + "pipe": true + }, + { + "command": "pnpm i --frozen-lockfile", + "dryRunCommand": true + }, + { + "command": "pnpm audit", + "dryRunCommand": true, + "runFromRoot": true, + "pipe": true + }, + { + "command": "echo '```\n\n
\n'", + "dryRunCommand": true, + "pipe": true + } + ], + "publish": [ + { + "command": "echo '
\n

PNPM Publish

\n\n```'", + "dryRunCommand": true, + "pipe": true + }, + { + "command": "pnpm publish --access public --loglevel silly --no-git-checks", + "dryRunCommand": "npm publish --dry-run --access public --no-git-checks", + "pipe": true + }, + { + "command": "echo '```\n\n
\n'", + "dryRunCommand": true, + "pipe": true + } + ], + "postpublish": { + "use": "fetch:check", + "options": { + "url": "https://registry.npmjs.com/${ pkg.pkgFile.pkg.name }/${ pkg.pkgFile.version }" + }, + "retries": [5000, 5000, 5000] + } + } + }, + "packages": { + "@tauri-apps/api": { + "path": "./packages/api", + "manager": "javascript", + "publish": [ + { + "command": "echo '
\n

PNPM Publish

\n\n```'", + "dryRunCommand": true, + "pipe": true + }, + { + "command": "pnpm npm-publish", + "dryRunCommand": true, + "pipe": true + }, + { + "command": "echo '```\n\n
\n'", + "dryRunCommand": true, + "pipe": true + } + ] + }, + "tauri-utils": { + "path": "./crates/tauri-utils", + "manager": "rust" + }, + "tauri-macos-sign": { + "path": "./crates/tauri-macos-sign", + "manager": "rust" + }, + "tauri-bundler": { + "path": "./crates/tauri-bundler", + "manager": "rust", + "dependencies": ["tauri-utils", "tauri-macos-sign"] + }, + "tauri-runtime": { + "path": "./crates/tauri-runtime", + "manager": "rust", + "dependencies": ["tauri-utils"] + }, + "tauri-runtime-wry": { + "path": "./crates/tauri-runtime-wry", + "manager": "rust", + "dependencies": ["tauri-utils", "tauri-runtime"] + }, + "tauri-codegen": { + "path": "./crates/tauri-codegen", + "manager": "rust", + "dependencies": ["tauri-utils"] + }, + "tauri-macros": { + "path": "./crates/tauri-macros", + "manager": "rust", + "dependencies": ["tauri-codegen", "tauri-utils"] + }, + "tauri-plugin": { + "path": "./crates/tauri-plugin", + "manager": "rust", + "dependencies": ["tauri-utils"], + "postversion": "node ../../.scripts/ci/sync-cli-metadata.js ${ pkg.pkg } ${ release.type }" + }, + "tauri-build": { + "path": "./crates/tauri-build", + "manager": "rust", + "dependencies": ["tauri-codegen", "tauri-utils"], + "postversion": "node ../../.scripts/ci/sync-cli-metadata.js ${ pkg.pkg } ${ release.type }" + }, + "tauri": { + "path": "./crates/tauri", + "manager": "rust", + "dependencies": [ + "tauri-macros", + "tauri-utils", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-build" + ], + "postversion": [ + "node ../../.scripts/ci/sync-cli-metadata.js ${ pkg.pkg } ${ release.type }", + "cargo build --manifest-path ../tauri-schema-generator/Cargo.toml" + ] + }, + "@tauri-apps/cli": { + "path": "./packages/cli", + "manager": "javascript", + "dependencies": ["tauri-cli"], + "postversion": "node ../../.scripts/ci/sync-cli-metadata.js ${ pkg.pkg } ${ release.type }", + "prepublish": [], + "publish": [], + "postpublish": [] + }, + "tauri-cli": { + "path": "./crates/tauri-cli", + "manager": "rust", + "dependencies": ["tauri-bundler", "tauri-utils", "tauri-macos-sign"] + }, + "tauri-driver": { + "path": "./crates/tauri-driver", + "manager": "rust" + } + } +} diff --git a/.changes/convert-file-src.md b/.changes/convert-file-src.md new file mode 100644 index 0000000..898e7bb --- /dev/null +++ b/.changes/convert-file-src.md @@ -0,0 +1,5 @@ +--- +"tauri": minor:feat +--- + +Add `Webview::convert_file_src` and `WebviewWindow::convert_file_src`, the Rust equivalent of the JavaScript `convertFileSrc` function. The returned URL uses the scheme the webview was configured with (`useHttpsScheme`), so it always matches what the webview can load. diff --git a/.changes/ctor-1.md b/.changes/ctor-1.md new file mode 100644 index 0000000..c7ad9e6 --- /dev/null +++ b/.changes/ctor-1.md @@ -0,0 +1,5 @@ +--- +tauri-utils: patch:deps +--- + +Updated `ctor` crate to `1` diff --git a/.changes/default-product-name-warning.md b/.changes/default-product-name-warning.md new file mode 100644 index 0000000..eb4bca1 --- /dev/null +++ b/.changes/default-product-name-warning.md @@ -0,0 +1,7 @@ +--- +"tauri-cli": patch:enhance +"@tauri-apps/cli": patch:enhance +"tauri-utils": patch:enhance +--- + +`tauri build` now warns when `productName` is still set to the default `tauri-app`, since it names the generated bundles and is written into install paths and metadata that are expected to be unique to your application. The config documentation for `productName` now lists what the field controls on each platform, and `identifier`'s documentation notes that the default value is rejected. diff --git a/.changes/disable-binary-patching.md b/.changes/disable-binary-patching.md new file mode 100644 index 0000000..8a911bc --- /dev/null +++ b/.changes/disable-binary-patching.md @@ -0,0 +1,7 @@ +--- +"tauri-bundler": "minor:feat" +"tauri-cli": "minor:feat" +"@tauri-apps/cli": "minor:feat" +--- + +Add a `--no-binary-patching` flag to `tauri build` and `tauri bundle`. When set, the bundler skips patching the main executable with bundle type information (and the subsequent re-signing), leaving an already-signed binary untouched. Patching is only required when shipping multiple bundle types per platform that should each update with their own installer format. diff --git a/.changes/dpi-type-wrappers.md b/.changes/dpi-type-wrappers.md new file mode 100644 index 0000000..93b0c18 --- /dev/null +++ b/.changes/dpi-type-wrappers.md @@ -0,0 +1,5 @@ +--- +"tauri-runtime-wry": "minor:breaking" +--- + +Removed unused dpi wrapper types like `PhysicalPositionWrapper` diff --git a/.changes/edition-2024-template.md b/.changes/edition-2024-template.md new file mode 100644 index 0000000..e859524 --- /dev/null +++ b/.changes/edition-2024-template.md @@ -0,0 +1,6 @@ +--- +tauri-cli: minor:changes +"@tauri-apps/cli": minor:changes +--- + +Moved to edition 2024 for the `tauri init`/`tauri plugin init` templates diff --git a/.changes/edition-2024.md b/.changes/edition-2024.md new file mode 100644 index 0000000..808eb26 --- /dev/null +++ b/.changes/edition-2024.md @@ -0,0 +1,16 @@ +--- +"tauri": minor:changes +"tauri-utils": minor:changes +"tauri-runtime": minor:changes +"tauri-runtime-wry": minor:changes +"tauri-codegen": minor:changes +"tauri-macros": minor:changes +"tauri-plugin": minor:changes +"tauri-build": minor:changes +"tauri-cli": minor:changes +"tauri-macos-sign": minor:changes +"tauri-bundler": minor:changes +"tauri-driver": minor:changes +--- + +Moved to edition 2024 diff --git a/.changes/event-listener-cleanup-on-destroy.md b/.changes/event-listener-cleanup-on-destroy.md new file mode 100644 index 0000000..e096256 --- /dev/null +++ b/.changes/event-listener-cleanup-on-destroy.md @@ -0,0 +1,5 @@ +--- +"tauri": "patch:bug" +--- + +Remove Rust-side event listeners bound to a window or webview when that target is destroyed, so `listen`/`once` handlers registered on a `Window`, `Webview` or `WebviewWindow` no longer leak after it is closed. diff --git a/.changes/exit-with-code.md b/.changes/exit-with-code.md new file mode 100644 index 0000000..dc405b2 --- /dev/null +++ b/.changes/exit-with-code.md @@ -0,0 +1,6 @@ +--- +'tauri': 'patch:bug' +'tauri-runtime-wry': 'patch:bug' +--- + +Transfer the exit code from the `window.app_handle().exit(1)` call to the `run_return()` result instead of always returning 0. diff --git a/.changes/explicit-resource-management.md b/.changes/explicit-resource-management.md new file mode 100644 index 0000000..6888ba2 --- /dev/null +++ b/.changes/explicit-resource-management.md @@ -0,0 +1,15 @@ +--- +"@tauri-apps/api": minor:enhance +--- + +Add ECMAScript Explicit Resource Management to Resource. You can now use the `using` syntax in supported browsers or with polyfills: + +```javascript +import { create, BaseDirectory } from "@tauri-apps/plugin-fs" +... +{ + await using file = await create("foo/bar.txt", { baseDir: BaseDirectory.AppConfig }); + await file.write(new TextEncoder().encode("Hello world")); + // Before `file` goes out of scope, it is disposed by calling `file[Symbol.asyncDispose]()` and awaited. +} +``` diff --git a/.changes/expose-set-activate-ignoring-other-apps.md b/.changes/expose-set-activate-ignoring-other-apps.md new file mode 100644 index 0000000..ec4db3f --- /dev/null +++ b/.changes/expose-set-activate-ignoring-other-apps.md @@ -0,0 +1,7 @@ +--- +"tauri-runtime-wry": minor:feat +"tauri-runtime": minor:feat +"tauri": minor:feat +--- + +Add `Builder::activate_ignoring_other_apps` on macOS to control whether the app activates at launch when another app is already active. diff --git a/.changes/fix-appimage-xdg-utils.md b/.changes/fix-appimage-xdg-utils.md new file mode 100644 index 0000000..94aa65a --- /dev/null +++ b/.changes/fix-appimage-xdg-utils.md @@ -0,0 +1,7 @@ +--- +tauri-bundler: patch:bug +tauri-cli: patch:bug +"@tauri-apps/cli": patch:bug +--- + +On Linux, do not bundle xdg-open and xdg-utils in the AppImage anymore. This rarely worked and usually requires host system support anyway. diff --git a/.changes/fix-capabilities-rerun-absolute-path.md b/.changes/fix-capabilities-rerun-absolute-path.md new file mode 100644 index 0000000..2921b74 --- /dev/null +++ b/.changes/fix-capabilities-rerun-absolute-path.md @@ -0,0 +1,5 @@ +--- +'tauri-build': 'patch:bug' +--- + +Emit an absolute path for the capabilities directory `cargo:rerun-if-changed`. Cargo resolves a relative watch path against the package owning the build script, while the capabilities glob is resolved against the process working directory, so callers that change the current directory before `try_build`/`try_build_context` ended up watching a non-existent directory — which is always dirty and re-ran the build script (and recompiled everything downstream) on every build. diff --git a/.changes/fix-channel-data-queue-webview-binding.md b/.changes/fix-channel-data-queue-webview-binding.md new file mode 100644 index 0000000..e21fe49 --- /dev/null +++ b/.changes/fix-channel-data-queue-webview-binding.md @@ -0,0 +1,7 @@ +--- +"tauri": patch:sec +--- + +Bind channel data IPC queue entries to the webview they were created for and scope their ids per webview. +Queued channel payloads and large invoke responses can no longer be read by other webviews, and entries are purged when the owning webview is closed. +Fixes GHSA-w28w-mhc8-qvjv. diff --git a/.changes/fix-cli-add-npm-frontend-dir.md b/.changes/fix-cli-add-npm-frontend-dir.md new file mode 100644 index 0000000..2cbc625 --- /dev/null +++ b/.changes/fix-cli-add-npm-frontend-dir.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:bug +"@tauri-apps/cli": patch:bug +--- + +Fix `tauri add` / `tauri remove` installing npm plugin packages into `src-tauri` instead of the frontend directory. \ No newline at end of file diff --git a/.changes/fix-copy-new-resource-files.md b/.changes/fix-copy-new-resource-files.md new file mode 100644 index 0000000..757b5ae --- /dev/null +++ b/.changes/fix-copy-new-resource-files.md @@ -0,0 +1,6 @@ +--- +'tauri-build': 'patch:bug' +'tauri-utils': 'patch:enhance' +--- + +Emit a `cargo:rerun-if-changed` for each resource directory (and glob base directory), so that adding or removing a file inside a resource directory re-runs the build script and copies the new files. Previously only the individual files present at build time were watched, so newly added files were silently ignored until an unrelated rebuild. diff --git a/.changes/fix-deterministic-config-serialization.md b/.changes/fix-deterministic-config-serialization.md new file mode 100644 index 0000000..7363bfa --- /dev/null +++ b/.changes/fix-deterministic-config-serialization.md @@ -0,0 +1,5 @@ +--- +'tauri-utils': 'patch:bug' +--- + +Serialize the CSP directive map, header source maps and plugin config with sorted keys so writing the processed config (e.g. the `tauri.conf.json` embedded in Android/iOS projects) is deterministic across builds. diff --git a/.changes/fix-deterministic-embedded-assets-codegen.md b/.changes/fix-deterministic-embedded-assets-codegen.md new file mode 100644 index 0000000..d66d3d6 --- /dev/null +++ b/.changes/fix-deterministic-embedded-assets-codegen.md @@ -0,0 +1,5 @@ +--- +'tauri-codegen': 'patch:bug' +--- + +Emit embedded assets and CSP script/style hashes in sorted order so `generate_context!` output no longer depends on the filesystem walk order, which varies across machines and broke reproducible builds. diff --git a/.changes/fix-ico-broken-in-task-manager.md b/.changes/fix-ico-broken-in-task-manager.md new file mode 100644 index 0000000..21916ef --- /dev/null +++ b/.changes/fix-ico-broken-in-task-manager.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:bug +"@tauri-apps/cli": patch:bug +--- + +Fix broken child window icons in Task Manager, you'll need to re-run `tauri icon` to generate the fixed icons diff --git a/.changes/fix-ico-largest-entry.md b/.changes/fix-ico-largest-entry.md new file mode 100644 index 0000000..96722ad --- /dev/null +++ b/.changes/fix-ico-largest-entry.md @@ -0,0 +1,5 @@ +--- +"tauri-codegen": patch:bug +--- + +Select the largest entry of an ICO file instead of the first one when embedding it with `include_image!` or as the tray icon, so the OS downscales a high-resolution source instead of upscaling the 16x16 entry. diff --git a/.changes/fix-js-listeners-leak-on-webview-close.md b/.changes/fix-js-listeners-leak-on-webview-close.md new file mode 100644 index 0000000..dbe95ba --- /dev/null +++ b/.changes/fix-js-listeners-leak-on-webview-close.md @@ -0,0 +1,5 @@ +--- +'tauri': 'patch:bug' +--- + +Remove a webview's JS event listeners from the backend `Listeners` map when that webview is destroyed. Previously the entries keyed by the source webview label lingered after the webview was dropped, so they could never be delivered and leaked until the app exited — forcing apps to manually `unlisten()` before closing a window. diff --git a/.changes/fix-js-set-background-color-arguments.md b/.changes/fix-js-set-background-color-arguments.md new file mode 100644 index 0000000..7a193a5 --- /dev/null +++ b/.changes/fix-js-set-background-color-arguments.md @@ -0,0 +1,5 @@ +--- +"@tauri-apps/api": patch:bug +--- + +Fix `Window.setBackgroundColor`, `Webview.setBackgroundColor` and `WebviewWindow.setBackgroundColor` sending a `color` argument while the `plugin:window|set_background_color` and `plugin:webview|set_webview_background_color` commands expect `label` and `value`. The color was always deserialized as `None`, so instead of applying the given color the call cleared the background of the calling window/webview. diff --git a/.changes/fix-monitor-queries-off-thread.md b/.changes/fix-monitor-queries-off-thread.md new file mode 100644 index 0000000..30f4f48 --- /dev/null +++ b/.changes/fix-monitor-queries-off-thread.md @@ -0,0 +1,6 @@ +--- +'tauri': 'patch:bug' +'tauri-runtime-wry': 'patch:bug' +--- + +Query monitor information (`primary_monitor`, `monitor_from_point`, `available_monitors`) on the main thread from the app-level runtime handle instead of touching the event loop's window target directly. diff --git a/.changes/fix-objc-retain-leak.md b/.changes/fix-objc-retain-leak.md new file mode 100644 index 0000000..d24a616 --- /dev/null +++ b/.changes/fix-objc-retain-leak.md @@ -0,0 +1,6 @@ +--- +"tauri": patch:bug +"tauri-runtime-wry": patch:bug +--- + +Avoid leaking Objective-C objects in `WebviewMessage::WithWebview` on Apple targets by replacing `Retained::into_raw` with scoped retained bindings and `Retained::as_ptr` pointer handoff. diff --git a/.changes/fix-send-sync.md b/.changes/fix-send-sync.md new file mode 100644 index 0000000..200a6f4 --- /dev/null +++ b/.changes/fix-send-sync.md @@ -0,0 +1,5 @@ +--- +"tauri-runtime-wry": minor:breaking +--- + +`WindowsStore` and `DispatcherMainThreadContext` are no longer `Send` and `Sync`, and unsafe impl has been moved to `Context` directly. diff --git a/.changes/fix-unlisten-guard-missing-entry.md b/.changes/fix-unlisten-guard-missing-entry.md new file mode 100644 index 0000000..77a3d06 --- /dev/null +++ b/.changes/fix-unlisten-guard-missing-entry.md @@ -0,0 +1,5 @@ +--- +'tauri': 'patch:bug' +--- + +Guard the handler lookup in the generated unlisten script. When the unlisten function ran before its listener registration eval reached the webview, the entry was still missing and reading its `handlerId` threw, which aborted `_unlisten` before it sent the backend `plugin:event|unlisten` and left the listener registered. diff --git a/.changes/fix-wix-external-bin-id.md b/.changes/fix-wix-external-bin-id.md new file mode 100644 index 0000000..2a9edd1 --- /dev/null +++ b/.changes/fix-wix-external-bin-id.md @@ -0,0 +1,7 @@ +--- +"tauri-bundler": patch:bug +"tauri-cli": patch:bug +"@tauri-apps/cli": patch:bug +--- + +Fix MSI bundling when an external binary filename starts with a digit. diff --git a/.changes/fix-wix-resource-target-name.md b/.changes/fix-wix-resource-target-name.md new file mode 100644 index 0000000..9ce87bb --- /dev/null +++ b/.changes/fix-wix-resource-target-name.md @@ -0,0 +1,8 @@ +--- +"tauri-bundler": patch:bug +"tauri-cli": patch:bug +"@tauri-apps/cli": patch:bug +--- + + +Fix WiX bundler doesn't respect the resource's target file name. diff --git a/.changes/header-source-map-deterministic-display.md b/.changes/header-source-map-deterministic-display.md new file mode 100644 index 0000000..712ed4c --- /dev/null +++ b/.changes/header-source-map-deterministic-display.md @@ -0,0 +1,5 @@ +--- +"tauri-utils": patch:bug +--- + +Fixed header values configured with an object in `app > security > headers` being serialized in a random order, which made the resulting header value differ between runs. The `key value` pairs are now always sorted by key, matching the ordering already used when the configuration is serialized. diff --git a/.changes/icon-non-square-fit.md b/.changes/icon-non-square-fit.md new file mode 100644 index 0000000..826afbe --- /dev/null +++ b/.changes/icon-non-square-fit.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": "minor:feat" +"@tauri-apps/cli": "minor:feat" +--- + +Add a `--fit` option to `tauri icon` to accept non-square source images. `--fit cover` center-crops the source to a square (clipping the longer side) and `--fit contain` pads the shorter side with transparency. Non-square sources without `--fit` keep erroring, now with a hint pointing to the flag. diff --git a/.changes/image-from-resource.md b/.changes/image-from-resource.md new file mode 100644 index 0000000..bb6f5bd --- /dev/null +++ b/.changes/image-from-resource.md @@ -0,0 +1,7 @@ +--- +"tauri": minor:feat +"tauri-codegen": minor:feat +"tauri-utils": minor:feat +--- + +Added `Image::from_app_icon_resource` and `Image::from_icon_resource` on Windows for loading images from icon resources embedded in the executable (identified by an `IconResource` id or name), and the default `default_window_icon` from `tauri::generate_context` macro is now loaded using `from_app_icon_resource`. The resource id `tauri-build` embeds the application icon with is exposed as `tauri_utils::platform::WINDOWS_APP_ICON_RESOURCE_ID`. diff --git a/.changes/image-icon-zero-copy.md b/.changes/image-icon-zero-copy.md new file mode 100644 index 0000000..7a634df --- /dev/null +++ b/.changes/image-icon-zero-copy.md @@ -0,0 +1,5 @@ +--- +tauri: patch:perf +--- + +Skip redundant rgba buffer copies in icon conversions (`muda::Icon`/`tray_icon::Icon`), `Image::from_bytes`, and submenu icon construction. diff --git a/.changes/image-rgba-perf.md b/.changes/image-rgba-perf.md new file mode 100644 index 0000000..a958aee --- /dev/null +++ b/.changes/image-rgba-perf.md @@ -0,0 +1,5 @@ +--- +tauri: patch:perf +--- + +Skip serializing to JSON for `Image::rgba` diff --git a/.changes/image-rgba-uint8array-generic.md b/.changes/image-rgba-uint8array-generic.md new file mode 100644 index 0000000..3981180 --- /dev/null +++ b/.changes/image-rgba-uint8array-generic.md @@ -0,0 +1,5 @@ +--- +"@tauri-apps/api": patch:bug +--- + +`Image.rgba` now returns a more specific type `Promise>` instead of the default `Promise` diff --git a/.changes/ios-27.md b/.changes/ios-27.md new file mode 100644 index 0000000..e8228d5 --- /dev/null +++ b/.changes/ios-27.md @@ -0,0 +1,6 @@ +--- +tauri-cli: minor:deps +"@tauri-apps/cli": minor:deps +--- + +Update the generated iOS Xcode project to build with Xcode 27. The scenes lifecycle is **not** enabled by default: to opt in, add the `UIApplicationSceneManifest` key to your `src-tauri/Info.ios.plist`. diff --git a/.changes/ios-monitor-off-main-thread.md b/.changes/ios-monitor-off-main-thread.md new file mode 100644 index 0000000..0ce370c --- /dev/null +++ b/.changes/ios-monitor-off-main-thread.md @@ -0,0 +1,5 @@ +--- +"tauri-runtime-wry": patch:bug +--- + +Fix a crash on iOS when the monitor APIs (`Window::current_monitor`, `primary_monitor`, `available_monitors`, `monitor_from_point` and the `AppHandle` equivalents) are called off the main thread, e.g. from a command: the monitor is now read on the main thread, where iOS requires it. diff --git a/.changes/js-image-type-alias.md b/.changes/js-image-type-alias.md new file mode 100644 index 0000000..7d2bb89 --- /dev/null +++ b/.changes/js-image-type-alias.md @@ -0,0 +1,5 @@ +--- +'@tauri-apps/api': 'minor:feat' +--- + +Added `JsImage` type alias and documented its relationship to the Rust `JsImage` diff --git a/.changes/lock-minor-versions.md b/.changes/lock-minor-versions.md new file mode 100644 index 0000000..8984167 --- /dev/null +++ b/.changes/lock-minor-versions.md @@ -0,0 +1,12 @@ +--- +"tauri": minor:changes +"tauri-runtime": minor:changes +"tauri-runtime-wry": minor:changes +"tauri-build": minor:changes +"tauri-macros": minor:changes +"tauri-codegen": minor:changes +"tauri-cli": minor:changes +"@tauri-apps/cli": minor:changes +--- + +Lock unstable tauri crates to minor versions. diff --git a/.changes/macos-app-set-theme-window-theme.md b/.changes/macos-app-set-theme-window-theme.md new file mode 100644 index 0000000..3482d9f --- /dev/null +++ b/.changes/macos-app-set-theme-window-theme.md @@ -0,0 +1,5 @@ +--- +"tauri-runtime-wry": patch:bug +--- + +On macOS, `App::set_theme` now also updates the theme each window reports, so `Window::theme` (and `getCurrentWindow().theme()` in JavaScript) reflects the app-level theme instead of keeping the previous value until the system appearance changes. diff --git a/.changes/macos-liquid-glass.md b/.changes/macos-liquid-glass.md new file mode 100644 index 0000000..fc2aa82 --- /dev/null +++ b/.changes/macos-liquid-glass.md @@ -0,0 +1,7 @@ +--- +tauri: minor:enhance +tauri-utils: minor:enhance +"@tauri-apps/api": minor:enhance +--- + +Added `Regular` and `Clear` Liquid Glass window effects, and the `interactive` window effects option (macOS 27.0+) that enables the glass' visual response to user interactions. diff --git a/.changes/menu-command-panic-on-wrong-input.md b/.changes/menu-command-panic-on-wrong-input.md new file mode 100644 index 0000000..5b65a1b --- /dev/null +++ b/.changes/menu-command-panic-on-wrong-input.md @@ -0,0 +1,9 @@ +--- +tauri: minor:bug +tauri-macros: minor:bug +--- + +Fix menu related commands can panic if called with invalid menu types through `invoke` directly. + +- The internal `do_menu_item!` macro now returns `Err(crate::Error::UnexpectedMenuKind)` instead of `unreachable!()` +- Added a new error type `tauri::Error::UnexpectedMenuKind` diff --git a/.changes/mobile-init-pnpm-native.md b/.changes/mobile-init-pnpm-native.md new file mode 100644 index 0000000..3ae4f92 --- /dev/null +++ b/.changes/mobile-init-pnpm-native.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:bug +"@tauri-apps/cli": patch:bug +--- + +Detect pnpm from `npm_config_user_agent` when generating the mobile projects. pnpm's native binary (pnpm 11+) runs package scripts without setting `PNPM_PACKAGE_NAME`, and when installed through corepack the binary is named `pnpm-native`, so `tauri android init` recorded `pnpm-native` as the command for Gradle to run and the Android build failed with "A problem occurred starting process 'command 'pnpm-native''". diff --git a/.changes/mobile-resumed-suspended-event.md b/.changes/mobile-resumed-suspended-event.md new file mode 100644 index 0000000..a7e8d89 --- /dev/null +++ b/.changes/mobile-resumed-suspended-event.md @@ -0,0 +1,6 @@ +--- +"tauri": minor:changes +"tauri-runtime-wry": minor:changes +--- + +`WindowEvent::Resumed` and `WindowEvent::Suspended` are now only fired for the matching activity window instead of every window. diff --git a/.changes/mock-runtime-run-main-thread-deadlock.md b/.changes/mock-runtime-run-main-thread-deadlock.md new file mode 100644 index 0000000..e031486 --- /dev/null +++ b/.changes/mock-runtime-run-main-thread-deadlock.md @@ -0,0 +1,5 @@ +--- +tauri: patch:bug +--- + +Fix `run_main_thread!` macro used by things like `MenuItem` and some other APIs deadlock when called on main thread with `MockRuntime` diff --git a/.changes/msrv-1.90.md b/.changes/msrv-1.90.md new file mode 100644 index 0000000..af2f247 --- /dev/null +++ b/.changes/msrv-1.90.md @@ -0,0 +1,16 @@ +--- +"tauri": minor:changes +"tauri-utils": minor:changes +"tauri-runtime": minor:changes +"tauri-runtime-wry": minor:changes +"tauri-codegen": minor:changes +"tauri-macros": minor:changes +"tauri-plugin": minor:changes +"tauri-build": minor:changes +"tauri-cli": minor:changes +"tauri-macos-sign": minor:changes +"tauri-bundler": minor:changes +"tauri-driver": minor:changes +--- + +Set MSRV to 1.90. diff --git a/.changes/no-redirection-bitmap-api.md b/.changes/no-redirection-bitmap-api.md new file mode 100644 index 0000000..23aa252 --- /dev/null +++ b/.changes/no-redirection-bitmap-api.md @@ -0,0 +1,5 @@ +--- +'@tauri-apps/api': 'minor:feat' +--- + +Added `noRedirectionBitmap` option to the `Window` and `WebviewWindow` constructors on Windows. diff --git a/.changes/no-redirection-bitmap-config.md b/.changes/no-redirection-bitmap-config.md new file mode 100644 index 0000000..5264ba4 --- /dev/null +++ b/.changes/no-redirection-bitmap-config.md @@ -0,0 +1,6 @@ +--- +'tauri': 'minor:feat' +'tauri-utils': 'minor:feat' +--- + +Added `app > windows > noRedirectionBitmap` config option to disable the window redirection bitmap on Windows. diff --git a/.changes/no-redirection-bitmap.md b/.changes/no-redirection-bitmap.md new file mode 100644 index 0000000..230cb4b --- /dev/null +++ b/.changes/no-redirection-bitmap.md @@ -0,0 +1,7 @@ +--- +'tauri': 'minor:feat' +'tauri-runtime': 'minor:feat' +'tauri-runtime-wry': 'minor:feat' +--- + +Added `WindowBuilder/WebviewWindowBuilder::no_redirection_bitmap` method to disable the window redirection bitmap on Windows. diff --git a/.changes/nsis-restart-manager.md b/.changes/nsis-restart-manager.md new file mode 100644 index 0000000..7e0c351 --- /dev/null +++ b/.changes/nsis-restart-manager.md @@ -0,0 +1,5 @@ +--- +"tauri-bundler": minor:enhance +--- + +Switch to use restart manager to close running app, this makes it so that we send a `WM_ENDSESSION` signal to the app for it to gracefully shutdown diff --git a/.changes/objc2-0.3.2.md b/.changes/objc2-0.3.2.md new file mode 100644 index 0000000..83c6cff --- /dev/null +++ b/.changes/objc2-0.3.2.md @@ -0,0 +1,7 @@ +--- +"tauri": patch:deps +"tauri-runtime": patch:deps +"tauri-runtime-wry": patch:deps +--- + +On macOS, updated `objc2-*` dependencies to 0.3.2 diff --git a/.changes/path-normalize-empty.md b/.changes/path-normalize-empty.md new file mode 100644 index 0000000..bb8261b --- /dev/null +++ b/.changes/path-normalize-empty.md @@ -0,0 +1,5 @@ +--- +tauri: patch:bug +--- + +Fix `path.normalize("")` returning an empty string instead of `"."`, matching Node.js and the existing `path.join("")` behavior. diff --git a/.changes/permission-handler.md b/.changes/permission-handler.md new file mode 100644 index 0000000..00da137 --- /dev/null +++ b/.changes/permission-handler.md @@ -0,0 +1,9 @@ +--- +"tauri": "minor:feat" +"tauri-runtime": "minor:feat" +"tauri-runtime-wry": "minor:feat" +--- + +Expose the `wry` permission handler API through Tauri. +This includes support for permission types such as `DisplayCapture`, `Midi`, `Sensors`, `MediaKeySystemAccess`, `LocalFonts`, `WindowManagement`, `PointerLock`, `AutomaticDownloads`, `FileSystemAccess`, and `Autoplay`. +Added `PermissionResponse::{Allow, Deny, Default}` for runtime permission decisions. diff --git a/.changes/permissions-policy-header-name.md b/.changes/permissions-policy-header-name.md new file mode 100644 index 0000000..f96cc96 --- /dev/null +++ b/.changes/permissions-policy-header-name.md @@ -0,0 +1,5 @@ +--- +"tauri-utils": patch:bug +--- + +Fixed the `app > security > headers > Permissions-Policy` configuration being sent as a header named `Permission-Policy`, which is not a real HTTP header, so the policy had no effect. The header is now correctly named `Permissions-Policy`. diff --git a/.changes/plugin-cleanup-before-exit.md b/.changes/plugin-cleanup-before-exit.md new file mode 100644 index 0000000..6418b3e --- /dev/null +++ b/.changes/plugin-cleanup-before-exit.md @@ -0,0 +1,5 @@ +--- +"tauri": minor:feat +--- + +Added the `Plugin::cleanup_before_exit` hook and `plugin::Builder::on_cleanup_before_exit`, invoked by `App::cleanup_before_exit` right before the process exits so plugins can release resources such as sidecar processes. Unlike `RunEvent::Exit`, it also runs on exit paths that bypass the event loop such as `AppHandle::restart` on the main thread. diff --git a/.changes/plugin-template-ts7.md b/.changes/plugin-template-ts7.md new file mode 100644 index 0000000..2889169 --- /dev/null +++ b/.changes/plugin-template-ts7.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:deps +"@tauri-apps/cli": patch:deps +--- + +Update typescript to v7 in `tauri plugin init` template. Also fixes the default `rootDir` in the template `tsconfig.json` diff --git a/.changes/png-0.18.md b/.changes/png-0.18.md new file mode 100644 index 0000000..73bdce2 --- /dev/null +++ b/.changes/png-0.18.md @@ -0,0 +1,5 @@ +--- +tauri-codegen: patch:deps +--- + +Updated `png` crate to `0.18` diff --git a/.changes/resolved-command-reference-debug-assertion.md b/.changes/resolved-command-reference-debug-assertion.md new file mode 100644 index 0000000..40e1442 --- /dev/null +++ b/.changes/resolved-command-reference-debug-assertion.md @@ -0,0 +1,8 @@ +--- +tauri: minor:enhance +tauri-macros: minor:enhance +tauri-utils: minor:enhance +tauri-build: minor:enhance +--- + +Fix different build and runtime debug assertion profiles on the tauri-utils crate can resulting in compilation errors. diff --git a/.changes/runtime-handle-monitor-fns.md b/.changes/runtime-handle-monitor-fns.md new file mode 100644 index 0000000..36e6ccf --- /dev/null +++ b/.changes/runtime-handle-monitor-fns.md @@ -0,0 +1,6 @@ +--- +'tauri-runtime': 'minor:breaking' +'tauri-runtime-wry': 'minor:breaking' +--- + +Changed `RuntimeHandle::primary_monitor`, `RuntimeHandle::monitor_from_point`, `RuntimeHandle::available_monitors` to return `Result`s diff --git a/.changes/runtime-wry-avoid-clones.md b/.changes/runtime-wry-avoid-clones.md new file mode 100644 index 0000000..afa03ac --- /dev/null +++ b/.changes/runtime-wry-avoid-clones.md @@ -0,0 +1,5 @@ +--- +"tauri-runtime-wry": "minor:changes" +--- + +`EventLoopIterationContext` and `UserMessageContext` now takes references to avoid clones diff --git a/.changes/runtime-wry-windows-store-debug.md b/.changes/runtime-wry-windows-store-debug.md new file mode 100644 index 0000000..6283d84 --- /dev/null +++ b/.changes/runtime-wry-windows-store-debug.md @@ -0,0 +1,5 @@ +--- +'tauri-runtime-wry': 'patch:bug' +--- + +`WindowsStore` and `ActiveTraceSpanStore` now implement `Debug` manually without borrowing the inner `RefCell`, so formatting them re-entrantly while the store is already borrowed no longer panics with "already borrowed". diff --git a/.changes/set-fullscreen-on-monitor.md b/.changes/set-fullscreen-on-monitor.md new file mode 100644 index 0000000..26f66aa --- /dev/null +++ b/.changes/set-fullscreen-on-monitor.md @@ -0,0 +1,8 @@ +--- +"tauri": minor:feat +"tauri-runtime": minor:feat +"tauri-runtime-wry": minor:feat +"@tauri-apps/api": minor:feat +--- + +Added `Window::set_fullscreen_on_monitor`, `WebviewWindow::set_fullscreen_on_monitor` and the `setFullscreenOnMonitor` JavaScript API to make a window fullscreen on the monitor containing a given physical position, along with the `core:window:allow-set-fullscreen-on-monitor` permission. diff --git a/.changes/signing-env-vars.md b/.changes/signing-env-vars.md new file mode 100644 index 0000000..d9cff76 --- /dev/null +++ b/.changes/signing-env-vars.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:enhance +"@tauri-apps/cli": patch:enhance +--- + +Document the `TAURI_SIGNING_PRIVATE_KEY_PATH` environment variable and clarify that `TAURI_SIGNING_PRIVATE_KEY` accepts a string or a path for the `build` and `bundle` command but must be the literal key string for the `signer sign` command, both in `ENVIRONMENT_VARIABLES.md` and in the `signer generate` command output. diff --git a/.changes/static-vc-runtime.md b/.changes/static-vc-runtime.md new file mode 100644 index 0000000..3bed5b7 --- /dev/null +++ b/.changes/static-vc-runtime.md @@ -0,0 +1,7 @@ +--- +"tauri-cli": "minor:feat" +"@tauri-apps/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/swift-27.md b/.changes/swift-27.md new file mode 100644 index 0000000..f1e6593 --- /dev/null +++ b/.changes/swift-27.md @@ -0,0 +1,6 @@ +--- +tauri: minor:deps +tauri-utils: minor:deps +--- + +Update swift-rs to support builds using Xcode 27. diff --git a/.changes/target-dir-nightly-out-dir.md b/.changes/target-dir-nightly-out-dir.md new file mode 100644 index 0000000..7eade8e --- /dev/null +++ b/.changes/target-dir-nightly-out-dir.md @@ -0,0 +1,5 @@ +--- +'tauri-build': 'patch:bug' +--- + +Resolve the target directory by walking up from `OUT_DIR` to the `build` directory instead of assuming it is exactly three levels up. Recent nightly toolchains add another level to `OUT_DIR`, which made sidecars and resources land in `target/debug/build` instead of `target/debug`. diff --git a/.changes/tauri-build-static-vc-runtime.md b/.changes/tauri-build-static-vc-runtime.md new file mode 100644 index 0000000..98f0d05 --- /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/.changes/tauri-info-skip-rust-only-js.md b/.changes/tauri-info-skip-rust-only-js.md new file mode 100644 index 0000000..5464a0c --- /dev/null +++ b/.changes/tauri-info-skip-rust-only-js.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": "patch:bug" +"@tauri-apps/cli": "patch:bug" +--- + +Fix `tauri info` reporting the Rust-only plugins (`localhost`, `persisted-scope` and `single-instance`) as missing their `@tauri-apps/plugin-*` JavaScript package, which is never published for those plugins. diff --git a/.changes/unstable-webview-focus.md b/.changes/unstable-webview-focus.md new file mode 100644 index 0000000..6854f71 --- /dev/null +++ b/.changes/unstable-webview-focus.md @@ -0,0 +1,6 @@ +--- +tauri: minor:bug +tauri-runtime-wry: minor:bug +--- + +Fix webview don't get focus when Alt-Tab back to the window if `unstable` feature is enabled on Windows diff --git a/.changes/update-dom-query.md b/.changes/update-dom-query.md new file mode 100644 index 0000000..edfe935 --- /dev/null +++ b/.changes/update-dom-query.md @@ -0,0 +1,5 @@ +--- +"tauri-utils": minor:deps +--- + +Updated `dom_query` dependency to 0.28.0 diff --git a/.changes/updater-sign-version.md b/.changes/updater-sign-version.md new file mode 100644 index 0000000..f27315a --- /dev/null +++ b/.changes/updater-sign-version.md @@ -0,0 +1,10 @@ +--- +"tauri-cli": "minor:feat" +"@tauri-apps/cli": "minor:feat" +--- + +Record the app version in the trusted comment of updater signatures, so a signed artifact is bound to the version it was released as. + +An update endpoint response is not signed, and the signature only covers the downloaded artifact, so the announced `version` on its own does not prove which release the `url` and `signature` point at. minisign covers the trusted comment with its global signature, which lets the updater plugin compare the two and reject a response that pairs a version number with a different release. Enable `requireSignedVersion` in the updater plugin configuration to enforce this. + +`tauri build` fills the version in automatically, and `tauri plugin add updater` now enables `requireSignedVersion` for the project it is adding the plugin to. `tauri signer sign` gains an `--app-version` flag for signing updater artifacts by hand, and warns when it is omitted. diff --git a/.changes/validate-inline-capabilities.md b/.changes/validate-inline-capabilities.md new file mode 100644 index 0000000..90868f2 --- /dev/null +++ b/.changes/validate-inline-capabilities.md @@ -0,0 +1,5 @@ +--- +"tauri-build": patch:bug +--- + +Validate the capabilities inlined in the `app > security > capabilities` configuration value, which previously skipped the build script validation and only failed later with a generic `failed to resolve ACL` panic when a permission identifier was unknown. diff --git a/.changes/wait-async-window-creation.md b/.changes/wait-async-window-creation.md new file mode 100644 index 0000000..f1d5252 --- /dev/null +++ b/.changes/wait-async-window-creation.md @@ -0,0 +1,5 @@ +--- +"tauri-runtime-wry": patch:bug +--- + +`create_window` and `create_webview` should wait for the window creation to complete before returning even if it's off main thread, it should also return the error if it failed diff --git a/.changes/webkitgtk-resize-cursor.md b/.changes/webkitgtk-resize-cursor.md new file mode 100644 index 0000000..89aee8d --- /dev/null +++ b/.changes/webkitgtk-resize-cursor.md @@ -0,0 +1,6 @@ +--- +'tauri-runtime-wry': 'patch:bug' +'tauri': 'patch:bug' +--- + +On Linux, fix resize cursor for undecorated Window not being set correctly when mouse is over a resize edge. diff --git a/.changes/webview-focus-on-move.md b/.changes/webview-focus-on-move.md new file mode 100644 index 0000000..fb5ccc5 --- /dev/null +++ b/.changes/webview-focus-on-move.md @@ -0,0 +1,6 @@ +--- +tauri: minor:bug +tauri-runtime-wry: minor:bug +--- + +Fix `WindowEvent::Focused` events emitted when dragging the window on Windows diff --git a/.changes/window-effect-non-exhaustive.md b/.changes/window-effect-non-exhaustive.md new file mode 100644 index 0000000..cabea6a --- /dev/null +++ b/.changes/window-effect-non-exhaustive.md @@ -0,0 +1,5 @@ +--- +tauri-utils: minor:breaking +--- + +The `WindowEffect` enum is now `#[non_exhaustive]` so new effects can be added without a breaking change. Exhaustive `match` statements on it must add a wildcard arm. diff --git a/.changes/windows-0.62.md b/.changes/windows-0.62.md new file mode 100644 index 0000000..9e7f42e --- /dev/null +++ b/.changes/windows-0.62.md @@ -0,0 +1,7 @@ +--- +"tauri": minor:deps +"tauri-runtime": minor:deps +"tauri-runtime-wry": minor:deps +--- + +Updated `windows` to `0.62` and `webview2-com` to `0.39`, this drops Windows 7 support, see https://github.com/microsoft/windows-rs/issues/3808. diff --git a/.changes/windows-multiwebview-cached-focused-states.md b/.changes/windows-multiwebview-cached-focused-states.md new file mode 100644 index 0000000..22e8afc --- /dev/null +++ b/.changes/windows-multiwebview-cached-focused-states.md @@ -0,0 +1,6 @@ +--- +tauri: patch:bug +tauri-runtime-wry: patch:bug +--- + +On Windows, fixed `Window::is_focused` always returns `false` in multi-webview mode diff --git a/.changes/wry-focused-webview.md b/.changes/wry-focused-webview.md new file mode 100644 index 0000000..45a1642 --- /dev/null +++ b/.changes/wry-focused-webview.md @@ -0,0 +1,5 @@ +--- +tauri-runtime-wry: minor:breaking +--- + +`CreateWebviewOptions::focused_webview` now takes `Arc>` instead of `Arc>>` diff --git a/.changes/xcode-27.md b/.changes/xcode-27.md new file mode 100644 index 0000000..7dc1313 --- /dev/null +++ b/.changes/xcode-27.md @@ -0,0 +1,6 @@ +--- +tauri-cli: minor:deps +"@tauri-apps/cli": minor:deps +--- + +Update cargo-mobile2 and change the default minimum iOS version to 15.0 to support Xcode 27. diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..f80ffd8 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,12 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/master/containers/ubuntu/.devcontainer/base.Dockerfile +# [Choice] Ubuntu version (use jammy or bionic on local arm64/Apple Silicon): jammy, focal, bionic +ARG VARIANT="jammy" +FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT} + +# Derived from Tauri contribution and setup guides: +# See: https://github.com/tauri-apps/tauri/blob/dev/.github/CONTRIBUTING.md#development-guide +# See: https://v2.tauri.app/start/prerequisites/ +ARG TAURI_BUILD_DEPS="build-essential curl libappindicator3-dev libgtk-3-dev librsvg2-dev libssl-dev libwebkit2gtk-4.1-dev wget" + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get install -y --no-install-recommends $TAURI_BUILD_DEPS diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 0000000..e72d611 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,39 @@ +# VS Code Devcontainer for Tauri + +## Overview + +Please note that most of these instructions are derived from Microsoft's VS Code documentation: [Developing inside a Container](https://code.visualstudio.com/docs/remote/containers). Check the official documentation if you encounter problems and submit a PR with any corrections you find for the instructions below. + +The development container included in this repository is derived from [Microsoft's default Ubuntu development container](https://github.com/microsoft/vscode-dev-containers/tree/master/containers/ubuntu). Contents of the Ubuntu Docker image can be in the [VS Code devcontainer Ubuntu base Dockerfile](https://github.com/microsoft/vscode-dev-containers/blob/main/containers/ubuntu/.devcontainer/base.Dockerfile). The contents of the container used for development can be found in the [Dockerfile](./Dockerfile) located in the same directory as this README. + +## Usage + +1. Ensure you have all [Devcontainer Prerequisites](#devcontainer-prerequisites) +2. Open the directory containing your [`tauri-apps/tauri`](https://github.com/tauri-apps/tauri) code. +3. Install the [Remote Development](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack) extension pack for VS Code. This will be included if you install recommended workspace extensions upon opening this repository. +4. Ensure Docker is running +5. [Open your workspace in the provided devcontainer](https://code.visualstudio.com/docs/remote/containers#_open-an-existing-workspace-in-a-container): Open this repository in VS Code and run **Remote-Containers: Reopen in Container...** from the Command Palette (F1). + +### Devcontainer Prerequisites + +Prerequisites are mainly derived from VS Code's instructions for usage of development containers, documented here: [Developing inside a Container: Getting Started](https://code.visualstudio.com/docs/remote/containers#_getting-started). + +1. Docker (Docker Desktop recommended) +2. VS Code +3. X window host - required if you want to be able to interact with a GUI from your Docker host + +### A note on filesystem performance + +Due to limitations in how Docker shares files between the Docker host and a container, it's also recommended that developers [clone Tauri source code into a container volume](https://code.visualstudio.com/remote/advancedcontainers/improve-performance#_use-clone-repository-in-container-volume). This is optional, but highly advised as many filesystem/IO heavy operations (`cargo build`, `pnpm install`, etc) will be very slow if they operate on directories shared with a Docker container from the Docker host. + +To do this, open your project with VS Code and run **Remote-Containers: Clone Repository in Container Volume...** from the Command Palette (F1). + +### Accessing a Tauri application running you the devcontainer + +Docker Desktop provides facilities for [allowing the development container to connect to a service on the Docker host](https://docs.docker.com/desktop/windows/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host). So long as you have an X window server running on your Docker host, the devcontainer can connect to it and expose your Tauri GUI via an X window. + +**Export the `DISPLAY` variable within the devcontainer terminal you launch your Tauri application from to expose your GUI outside of the devcontainer**. + +```bash +export DISPLAY="host.docker.internal:0" +``` diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..a7a9805 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,30 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/ubuntu +{ + "name": "Ubuntu", + "build": { + "dockerfile": "Dockerfile", + // Update 'VARIANT' to pick an Ubuntu version: jammy / ubuntu-22.04, focal / ubuntu-20.04, bionic /ubuntu-18.04 + // Use ubuntu-22.04 or ubuntu-18.04 on local arm64/Apple Silicon. + "args": { "VARIANT": "ubuntu-22.04" } + }, + + // Set *default* container specific settings.json values on container create. + "settings": {}, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "uname -a", + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "features": { + "node": "lts", + "rust": "latest" + } +} diff --git a/.docker/cross/aarch64.Dockerfile b/.docker/cross/aarch64.Dockerfile new file mode 100644 index 0000000..c57ec56 --- /dev/null +++ b/.docker/cross/aarch64.Dockerfile @@ -0,0 +1,44 @@ +FROM ubuntu:22.04 +ARG DEBIAN_FRONTEND=noninteractive + +COPY common.sh lib.sh / +RUN /common.sh + +COPY cmake.sh / +RUN /cmake.sh + +COPY xargo.sh / +RUN /xargo.sh + +RUN apt-get update && apt-get install --assume-yes --no-install-recommends \ + g++-aarch64-linux-gnu \ + libc6-dev-arm64-cross + +COPY deny-debian-packages.sh / +RUN TARGET_ARCH=arm64 /deny-debian-packages.sh \ + binutils \ + binutils-aarch64-linux-gnu + +COPY qemu.sh / +RUN /qemu.sh aarch64 softmmu + +COPY dropbear.sh / +RUN /dropbear.sh + +COPY linux-image.sh / +RUN /linux-image.sh aarch64 + +COPY linux-runner / + +ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \ + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUNNER="/linux-runner aarch64" \ + CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc \ + CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ \ + BINDGEN_EXTRA_CLANG_ARGS_aarch64_unknown_linux_gnu="--sysroot=/usr/aarch64-linux-gnu" \ + QEMU_LD_PREFIX=/usr/aarch64-linux-gnu \ + RUST_TEST_THREADS=1 \ + PKG_CONFIG_PATH="/usr/lib/aarch64-linux-gnu/pkgconfig/:${PKG_CONFIG_PATH}" + +RUN dpkg --add-architecture arm64 +RUN apt-get update +RUN apt-get install --assume-yes --no-install-recommends libssl-dev:arm64 libdbus-1-dev:arm64 libsoup2.4-dev:arm64 libssl-dev:arm64 libgtk-3-dev:arm64 webkit2gtk-4.1-dev:arm64 libappindicator3-1:arm64 librsvg2-dev:arm64 patchelf:arm64 diff --git a/.docker/cross/cmake.sh b/.docker/cross/cmake.sh new file mode 100755 index 0000000..9ed4ee6 --- /dev/null +++ b/.docker/cross/cmake.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -x +set -euo pipefail + +# shellcheck disable=SC1091 +. lib.sh + +main() { + local version=3.23.1 + + install_packages curl + + local td + td="$(mktemp -d)" + + pushd "${td}" + + curl --retry 3 -sSfL "https://github.com/Kitware/CMake/releases/download/v${version}/cmake-${version}-linux-x86_64.sh" -o cmake.sh + sh cmake.sh --skip-license --prefix=/usr/local + + popd + + purge_packages + + rm -rf "${td}" + rm -rf /var/lib/apt/lists/* + rm "${0}" +} + +main "${@}" diff --git a/.docker/cross/common.sh b/.docker/cross/common.sh new file mode 100755 index 0000000..24d5619 --- /dev/null +++ b/.docker/cross/common.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +set -x +set -euo pipefail + +# shellcheck disable=SC1091 +. lib.sh + +# For architectures except amd64 and i386, look for packages on ports.ubuntu.com instead. +# This is important if you enable additional architectures so you can install libraries to cross-compile against. +# Look for 'dpkg --add-architecture' in the README for more details. +if grep -i ubuntu /etc/os-release >/dev/null; then + sed 's/http:\/\/\(.*\).ubuntu.com\/ubuntu\//[arch-=amd64,i386] http:\/\/ports.ubuntu.com\/ubuntu-ports\//g' /etc/apt/sources.list > /etc/apt/sources.list.d/ports.list + sed -i 's/http:\/\/\(.*\).ubuntu.com\/ubuntu\//[arch=amd64,i386] http:\/\/\1.archive.ubuntu.com\/ubuntu\//g' /etc/apt/sources.list +fi + +install_packages \ + autoconf \ + automake \ + binutils \ + ca-certificates \ + curl \ + file \ + gcc \ + git \ + libtool \ + m4 \ + make + +if_centos install_packages \ + clang-devel \ + gcc-c++ \ + glibc-devel \ + pkgconfig + +if_ubuntu install_packages \ + g++ \ + libc6-dev \ + libclang-dev \ + pkg-config diff --git a/.docker/cross/deny-debian-packages.sh b/.docker/cross/deny-debian-packages.sh new file mode 100755 index 0000000..3193333 --- /dev/null +++ b/.docker/cross/deny-debian-packages.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -x +set -euo pipefail + +main() { + local package + + for package in "${@}"; do + echo "Package: ${package}:${TARGET_ARCH} +Pin: release * +Pin-Priority: -1" > "/etc/apt/preferences.d/${package}" + echo "${package}" + done + + rm "${0}" +} + +main "${@}" diff --git a/.docker/cross/dropbear.sh b/.docker/cross/dropbear.sh new file mode 100755 index 0000000..097814e --- /dev/null +++ b/.docker/cross/dropbear.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +set -x +set -euo pipefail + +# shellcheck disable=SC1091 +. lib.sh + +main() { + local version=2022.82 + + install_packages \ + autoconf \ + automake \ + bzip2 \ + curl \ + make + + if_centos install_packages zlib-devel + if_ubuntu install_packages zlib1g-dev + + local td + td="$(mktemp -d)" + + pushd "${td}" + + curl --retry 3 -sSfL "https://matt.ucc.asn.au/dropbear/releases/dropbear-${version}.tar.bz2" -O + tar --strip-components=1 -xjf "dropbear-${version}.tar.bz2" + + # Remove some unwanted message + sed -i '/skipping hostkey/d' cli-kex.c + sed -i '/failed to identify current user/d' cli-runopts.c + + ./configure \ + --disable-syslog \ + --disable-shadow \ + --disable-lastlog \ + --disable-utmp \ + --disable-utmpx \ + --disable-wtmp \ + --disable-wtmpx \ + --disable-pututline \ + --disable-pututxline + + make "-j$(nproc)" PROGRAMS=dbclient + cp dbclient /usr/local/bin/ + + purge_packages + + popd + + rm -rf "${td}" + rm "${0}" +} + +main "${@}" diff --git a/.docker/cross/lib.sh b/.docker/cross/lib.sh new file mode 100644 index 0000000..0d299b8 --- /dev/null +++ b/.docker/cross/lib.sh @@ -0,0 +1,45 @@ +purge_list=() + +install_packages() { + if grep -i ubuntu /etc/os-release; then + apt-get update + + for pkg in "${@}"; do + if ! dpkg -L "${pkg}" >/dev/null 2>/dev/null; then + apt-get install --assume-yes --no-install-recommends "${pkg}" + + purge_list+=( "${pkg}" ) + fi + done + else + for pkg in "${@}"; do + if ! yum list installed "${pkg}" >/dev/null 2>/dev/null; then + yum install -y "${pkg}" + + purge_list+=( "${pkg}" ) + fi + done + fi +} + +purge_packages() { + if (( ${#purge_list[@]} )); then + if grep -i ubuntu /etc/os-release; then + apt-get purge --assume-yes --auto-remove "${purge_list[@]}" + else + yum remove -y "${purge_list[@]}" + fi + fi +} + +if_centos() { + if grep -q -i centos /etc/os-release; then + eval "${@}" + fi +} + +if_ubuntu() { + if grep -q -i ubuntu /etc/os-release; then + eval "${@}" + fi +} diff --git a/.docker/cross/linux-image.sh b/.docker/cross/linux-image.sh new file mode 100755 index 0000000..dc85ed4 --- /dev/null +++ b/.docker/cross/linux-image.sh @@ -0,0 +1,274 @@ +#!/usr/bin/env bash + +set -x +set -euo pipefail + +# shellcheck disable=SC1091 +. lib.sh + +main() { + # arch in the rust target + local arch="${1}" \ + kversion=4.19.0-20 + + local debsource="deb http://http.debian.net/debian/ buster main" + debsource="${debsource}\ndeb http://security.debian.org/ buster/updates main" + + local dropbear="dropbear-bin" + + local -a deps + local kernel= + local libgcc="libgcc1" + + # select debian arch and kernel version + case "${arch}" in + aarch64) + arch=arm64 + kernel="${kversion}-arm64" + ;; + armv7) + arch=armhf + kernel="${kversion}-armmp" + ;; + i686) + arch=i386 + kernel="${kversion}-686" + ;; + mips|mipsel) + kernel="${kversion}-4kc-malta" + ;; + mips64el) + kernel="${kversion}-5kc-malta" + ;; + powerpc) + # there is no buster powerpc port, so we use jessie + # use a more recent kernel from backports + kversion='4.9.0-0.bpo.6' + kernel="${kversion}-powerpc" + debsource="deb http://archive.debian.org/debian jessie main" + debsource="${debsource}\ndeb http://archive.debian.org/debian jessie-backports main" + debsource="${debsource}\ndeb http://ftp.ports.debian.org/debian-ports unstable main" + debsource="${debsource}\ndeb http://ftp.ports.debian.org/debian-ports unreleased main" + + # archive.debian.org Release files are expired. + echo "Acquire::Check-Valid-Until false;" | tee -a /etc/apt/apt.conf.d/10-nocheckvalid + echo "APT::Get::AllowUnauthenticated true;" | tee -a /etc/apt/apt.conf.d/10-nocheckvalid + + dropbear="dropbear" + deps=(libcrypt1:"${arch}") + ;; + powerpc64) + # there is no stable port + arch=ppc64 + # https://packages.debian.org/en/sid/linux-image-powerpc64 + kversion='5.*' + kernel="${kversion}-powerpc64" + libgcc="libgcc-s1" + debsource="deb http://ftp.ports.debian.org/debian-ports unstable main" + debsource="${debsource}\ndeb http://ftp.ports.debian.org/debian-ports unreleased main" + # sid version of dropbear requires these dependencies + deps=(libcrypt1:"${arch}") + ;; + powerpc64le) + arch=ppc64el + kernel="${kversion}-powerpc64le" + ;; + s390x) + arch=s390x + kernel="${kversion}-s390x" + ;; + sparc64) + # there is no stable port + # https://packages.debian.org/en/sid/linux-image-sparc64 + kernel='*-sparc64' + libgcc="libgcc-s1" + debsource="deb http://ftp.ports.debian.org/debian-ports unstable main" + debsource="${debsource}\ndeb http://ftp.ports.debian.org/debian-ports unreleased main" + # sid version of dropbear requires these dependencies + deps=(libcrypt1:"${arch}") + ;; + x86_64) + arch=amd64 + kernel="${kversion}-amd64" + ;; + *) + echo "Invalid arch: ${arch}" + exit 1 + ;; + esac + + install_packages ca-certificates \ + curl \ + cpio \ + sharutils \ + gnupg + + # Download packages + mv /etc/apt/sources.list /etc/apt/sources.list.bak + echo -e "${debsource}" > /etc/apt/sources.list + + # Old ubuntu does not support --add-architecture, so we directly change multiarch file + if [ -f /etc/dpkg/dpkg.cfg.d/multiarch ]; then + cp /etc/dpkg/dpkg.cfg.d/multiarch /etc/dpkg/dpkg.cfg.d/multiarch.bak + fi + dpkg --add-architecture "${arch}" || echo "foreign-architecture ${arch}" > /etc/dpkg/dpkg.cfg.d/multiarch + + # Add Debian keys. + curl --retry 3 -sSfL 'https://ftp-master.debian.org/keys/archive-key-{7.0,8,9,10}.asc' -O + curl --retry 3 -sSfL 'https://ftp-master.debian.org/keys/archive-key-{8,9,10}-security.asc' -O + curl --retry 3 -sSfL 'https://ftp-master.debian.org/keys/release-{7,8,9,10}.asc' -O + curl --retry 3 -sSfL 'https://www.ports.debian.org/archive_{2020,2021,2022}.key' -O + + for key in *.asc *.key; do + apt-key add "${key}" + rm "${key}" + done + + # allow apt-get to retry downloads + echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80-retries + + apt-get update + + mkdir -p "/qemu/${arch}" + chmod 777 /qemu "/qemu/${arch}" + + cd "/qemu/${arch}" + apt-get -d --no-install-recommends download \ + ${deps[@]+"${deps[@]}"} \ + "busybox:${arch}" \ + "${dropbear}:${arch}" \ + "libtommath1:${arch}" \ + "libtomcrypt1:${arch}" \ + "libgmp10:${arch}" \ + "libc6:${arch}" \ + "${libgcc}:${arch}" \ + "libstdc++6:${arch}" \ + "linux-image-${kernel}:${arch}" \ + ncurses-base \ + "zlib1g:${arch}" + cd /qemu + + # Install packages + root="root-${arch}" + mkdir -p "${root}"/{bin,etc/dropbear,root,sys,dev,proc,sbin,tmp,usr/{bin,sbin},var/log} + for deb in "${arch}"/*deb; do + dpkg -x "${deb}" "${root}"/ + done + + cp "${root}/boot/vmlinu"* kernel + + # initrd + mkdir -p "${root}/modules" + cp -v \ + "${root}/lib/modules"/*/kernel/drivers/net/net_failover.ko \ + "${root}/lib/modules"/*/kernel/drivers/net/virtio_net.ko \ + "${root}/lib/modules"/*/kernel/drivers/virtio/* \ + "${root}/lib/modules"/*/kernel/fs/netfs/netfs.ko \ + "${root}/lib/modules"/*/kernel/fs/9p/9p.ko \ + "${root}/lib/modules"/*/kernel/fs/fscache/fscache.ko \ + "${root}/lib/modules"/*/kernel/net/9p/9pnet.ko \ + "${root}/lib/modules"/*/kernel/net/9p/9pnet_virtio.ko \ + "${root}/lib/modules"/*/kernel/net/core/failover.ko \ + "${root}/modules" || true # some file may not exist + rm -rf "${root:?}/boot" + rm -rf "${root:?}/lib/modules" + + cat << 'EOF' > "${root}/etc/hosts" +127.0.0.1 localhost qemu +EOF + + cat << 'EOF' > $root/etc/hostname +qemu +EOF + + cat << 'EOF' > $root/etc/passwd +root::0:0:root:/root:/bin/sh +EOF + +cat << 'EOF' | uudecode -o $root/etc/dropbear/dropbear_rsa_host_key +begin 600 dropbear_rsa_host_key +M````!W-S:"UR+3[X=QMH,B*4$RYULV,V3X6]K:7@Q?80"#WXGGQZNFN6CZ7LTDX(F6J[\]F5<0`HEOF:Z +MX;^53`L'4I/A```!``$L:$Z*#6<^3@+O%.[-#/5H+.C'3\#QQZN[1;J>L`8I +MZ_&T'!"J'/Y+?R?55G:M^=]R*-&I3TOJYZA8@&H51ZOAF59'1_>>Z@?E4#)$ +MQU)X/RWH51ZB5KSDWJS:D'7GD(!?NAY`C'7\)I:_4)J")QBV/P"RJQGHG'%B +M1BT2LE6676>`1K,0\NIMZTKQNB(IC+88<7#8%_-=P<&6<"9LH>60TSS?3?-C +MN`T36YB/3^<(Q;`N1NT>I9EZS`BAC^-?.:,R\7EL"<4>7E=]^1]B\K9])AQU +MBM\]M;4V(S(6KH-I.4[6>9E+@\UEM.J6:[2LUEEJDG:G:+:/EVF^Y75@(S$` +M``"!`.O+KW=&*CBCHL"11&SVO4/K]$R-]7MV7,3RR)Q[X'0;6.?4JHW!3VR6 +M*FGBY--37ZD-+UV.8_+"$6PH9)(/E.\G19#G0K`LRM?JWS!58&;D0C1```` +M@0"\[@NYWSTW(?Q@:_A*1Y3/AKYO5?S=0"<2>#V-AH6W-NCSDTSRP=2D79FS +M"D?[;.)V>8'#9&I3"MU@+:2\Z%$0-MG0+J'(0>T1_C6?*C=4U0I$DI<=@D]1 +H_&DE8Y(OT%%EPG]!$H&5HX*),_D1A2\P=R.7G'`0L%YM-79Y"T">$0`` +` +end +EOF + + # dropbear complains when this file is missing + touch "${root}/var/log/lastlog" + + cat << 'EOF' > $root/init +#!/bin/busybox sh + +set -e + +/bin/busybox --install + +mount -t devtmpfs devtmpfs /dev +mount -t proc none /proc +mount -t sysfs none /sys +mkdir /dev/pts +mount -t devpts none /dev/pts/ + +# some archs does not have virtio modules +insmod /modules/failover.ko || true +insmod /modules/net_failover.ko || true +insmod /modules/virtio.ko || true +insmod /modules/virtio_ring.ko || true +insmod /modules/virtio_mmio.ko || true +insmod /modules/virtio_pci_legacy_dev.ko || true +insmod /modules/virtio_pci_modern_dev.ko || true +insmod /modules/virtio_pci.ko || true +insmod /modules/virtio_net.ko || true +insmod /modules/netfs.ko || true +insmod /modules/fscache.ko +insmod /modules/9pnet.ko +insmod /modules/9pnet_virtio.ko || true +insmod /modules/9p.ko + +ifconfig lo 127.0.0.1 +ifconfig eth0 10.0.2.15 +route add default gw 10.0.2.2 eth0 + +mkdir /target +mount -t 9p -o trans=virtio target /target -oversion=9p2000.u || true + +exec dropbear -F -E -B +EOF + + chmod +x "${root}/init" + cd "${root}" + find . | cpio --create --format='newc' --quiet | gzip > ../initrd.gz + cd - + + # Clean up + rm -rf "/qemu/${root}" "/qemu/${arch}" + mv -f /etc/apt/sources.list.bak /etc/apt/sources.list + if [ -f /etc/dpkg/dpkg.cfg.d/multiarch.bak ]; then + mv /etc/dpkg/dpkg.cfg.d/multiarch.bak /etc/dpkg/dpkg.cfg.d/multiarch + fi + # can fail if arch is used (amd64 and/or i386) + dpkg --remove-architecture "${arch}" || true + apt-get update + + purge_packages + + ls -lh /qemu +} + +main "${@}" diff --git a/.docker/cross/linux-runner b/.docker/cross/linux-runner new file mode 100755 index 0000000..2ef0efc --- /dev/null +++ b/.docker/cross/linux-runner @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +set -e + +LOG=/tmp/qemu.log +LOCK=/tmp/qemu.lock + +if [ -n "${CROSS_DEBUG}" ]; then + set -x +fi + +# arch in the rust target +arch="${1}" +shift + +if [ "${CROSS_RUNNER}" = "" ]; then + if [[ "${arch}" == i?86 ]] || [[ "${arch}" == x86_64 ]]; then + CROSS_RUNNER=native + else + CROSS_RUNNER=qemu-user + fi +fi + +# select qemu arch +qarch="${arch}" +case "${arch}" in + armv7) + qarch="arm" + ;; + i686) + qarch="i386" + ;; + powerpc) + qarch="ppc" + ;; + powerpc64) + qarch="ppc64" + ;; + powerpc64le) + if [ "${CROSS_RUNNER}" = "qemu-user" ]; then + qarch="ppc64le" + else + qarch="ppc64" + fi + ;; +esac + +case "${CROSS_RUNNER}" in + native) + exec "${@}" + ;; + qemu-user) + exec "qemu-${qarch}" "${@}" + ;; + qemu-system) + true + ;; + *) + echo "Invalid runner: \"${CROSS_RUNNER}\""; + echo "Valid runners are: native, qemu-user and qemu-system" + exit 1 + ;; +esac + +n="$(nproc)" +memory=1G +driver9p="virtio-9p-pci" +drivernet="virtio-net-pci" + +# select qemu parameters +case "${arch}" in + aarch64) + # 8 is the max number of cpu supported by qemu-aarch64 + n=$(( n > 8 ? 8 : n )) + opt="-machine virt -cpu cortex-a57" + ;; + armv7) + opt="-machine virt" + driver9p="virtio-9p-device" + drivernet="virtio-net-device" + ;; + i686) + opt="-append console=ttyS0" + ;; + mips|mipsel) + # avoid kernel error + # https://blahcat.github.io/2017/07/14/building-a-debian-stretch-qemu-image-for-mipsel/ + opt="-append nokaslr" + n=1 + ;; + mips64el) + # avoid kernel error + # https://blahcat.github.io/2017/07/14/building-a-debian-stretch-qemu-image-for-mipsel/ + opt="-append nokaslr -cpu MIPS64R2-generic" + n=1 + ;; + powerpc) + opt="-append console=ttyPZ0" + n=1 + ;; + powerpc64|powerpc64le) + opt="-append console=hvc0 --nodefaults -serial stdio" + ;; + s390x) + n=1 + driver9p="virtio-9p-ccw" + drivernet="virtio-net-ccw" + ;; + sparc64) + n=1 + driver9p+=",bus=pciB" + drivernet+=",bus=pciB" + ;; + x86_64) + opt="-append console=ttyS0" + ;; +esac + +( + flock -n 200 || exit 0 + + echo Booting QEMU virtual machine with $n cpus... + + QEMU_CMD="qemu-system-${qarch} \ + -m ${memory} \ + -smp ${n} \ + -nographic \ + -monitor none \ + -netdev user,id=net0,hostfwd=tcp::10022-:22 \ + -device ${drivernet},netdev=net0 \ + -kernel /qemu/kernel \ + -initrd /qemu/initrd.gz \ + ${opt} \ + -fsdev local,id=fs0,path=/target,security_model=mapped \ + -device ${driver9p},fsdev=fs0,mount_tag=target" + + touch "${LOG}" + if [[ -n "${CROSS_DEBUG}" ]]; then + (${QEMU_CMD} 2>&1 | tee -a "${LOG}") & + else + ${QEMU_CMD} >> "${LOG}" 2>&1 & + fi + + # wait for dropbear + for _ in $(seq 240); do + if grep -q "Not backgrounding" "${LOG}"; then + READY=1 + break + fi + sleep 0.5s + done + + if [ -z "${READY}" ]; then + if [ -n "${CROSS_DEBUG}" ]; then + echo "Not ready but continuing because CROSS_DEBUG is set" + else + echo "Qemu is not ready after ${SECONDS} seconds..." + echo "Set the environment variable CROSS_DEBUG=1 to debug" + echo "Last 100 lines of qemu output:" + tail -n 100 "${LOG}" + exit 1 + fi + fi + + echo "Booted in ${SECONDS} seconds" + +) 200>"${LOCK}" + +if [[ -t 1 ]] && [[ -t 2 ]]; then + tty_flag='-t' +fi + +exec dbclient ${tty_flag} -p 10022 -y -y root@localhost "${@}" diff --git a/.docker/cross/qemu.sh b/.docker/cross/qemu.sh new file mode 100755 index 0000000..75626c5 --- /dev/null +++ b/.docker/cross/qemu.sh @@ -0,0 +1,233 @@ +#!/usr/bin/env bash + +set -x +set -euo pipefail + +# shellcheck disable=SC1091 +. lib.sh + +build_static_libffi () { + local version=3.0.13 + + local td + td="$(mktemp -d)" + + pushd "${td}" + + + curl --retry 3 -sSfL "https://github.com/libffi/libffi/archive/refs/tags/v${version}.tar.gz" -O -L + tar --strip-components=1 -xzf "v${version}.tar.gz" + ./configure --prefix="$td"/lib --disable-builddir --disable-shared --enable-static + make "-j$(nproc)" + install -m 644 ./.libs/libffi.a /usr/lib64/ + + popd + + rm -rf "${td}" +} + +build_static_libmount () { + local version_spec=2.23.2 + local version=2.23 + local td + td="$(mktemp -d)" + + pushd "${td}" + + curl --retry 3 -sSfL "https://kernel.org/pub/linux/utils/util-linux/v${version}/util-linux-${version_spec}.tar.xz" -O -L + tar --strip-components=1 -xJf "util-linux-${version_spec}.tar.xz" + ./configure --disable-shared --enable-static --without-ncurses + make "-j$(nproc)" mount blkid + install -m 644 ./.libs/*.a /usr/lib64/ + + popd + + rm -rf "${td}" +} + + +build_static_libattr() { + local version=2.4.46 + + local td + td="$(mktemp -d)" + + pushd "${td}" + + yum install -y gettext + + curl --retry 3 -sSfL "https://download.savannah.nongnu.org/releases/attr/attr-${version}.src.tar.gz" -O + tar --strip-components=1 -xzf "attr-${version}.src.tar.gz" + cp /usr/share/automake*/config.* . + + ./configure + make "-j$(nproc)" + install -m 644 ./libattr/.libs/libattr.a /usr/lib64/ + + yum remove -y gettext + + popd + + rm -rf "${td}" +} + +build_static_libcap() { + local version=2.22 + + local td + td="$(mktemp -d)" + + pushd "${td}" + + curl --retry 3 -sSfL "https://www.kernel.org/pub/linux/libs/security/linux-privs/libcap2/libcap-${version}.tar.xz" -O + tar --strip-components=1 -xJf "libcap-${version}.tar.xz" + make "-j$(nproc)" + install -m 644 libcap/libcap.a /usr/lib64/ + + popd + + rm -rf "${td}" +} + +build_static_pixman() { + local version=0.34.0 + + local td + td="$(mktemp -d)" + + pushd "${td}" + + curl --retry 3 -sSfL "https://www.cairographics.org/releases/pixman-${version}.tar.gz" -O + tar --strip-components=1 -xzf "pixman-${version}.tar.gz" + ./configure + make "-j$(nproc)" + install -m 644 ./pixman/.libs/libpixman-1.a /usr/lib64/ + + popd + + rm -rf "${td}" +} + +main() { + local version=5.1.0 + + if_centos version=4.2.1 + + local arch="${1}" \ + softmmu="${2:-}" + + install_packages \ + autoconf \ + automake \ + bison \ + bzip2 \ + curl \ + flex \ + libtool \ + make \ + patch \ + python3 \ + + if_centos install_packages \ + gcc-c++ \ + pkgconfig \ + xz \ + glib2-devel \ + glib2-static \ + glibc-static \ + libattr-devel \ + libcap-devel \ + libfdt-devel \ + pcre-static \ + pixman-devel \ + libselinux-devel \ + libselinux-static \ + libffi \ + libuuid-devel \ + libblkid-devel \ + libmount-devel \ + zlib-devel \ + zlib-static + + if_centos 'curl --retry 3 -sSfL "https://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD" -o /usr/share/automake*/config.guess' + if_centos 'curl --retry 3 -sSfL "https://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD" -o /usr/share/automake*/config.sub' + + # these are not packaged as static libraries in centos; build them manually + if_centos build_static_libffi + if_centos build_static_libmount + if_centos build_static_libattr + if_centos build_static_libcap + if_centos build_static_pixman + + if_ubuntu install_packages \ + g++ \ + pkg-config \ + xz-utils \ + libattr1-dev \ + libcap-ng-dev \ + libffi-dev \ + libglib2.0-dev \ + libpixman-1-dev \ + libselinux1-dev \ + zlib1g-dev + + # if we have python3.6+, we can install qemu 6.1.0, which needs ninja-build + # ubuntu 16.04 only provides python3.5, so remove when we have a newer qemu. + is_ge_python36=$(python3 -c "import sys; print(int(sys.version_info >= (3, 6)))") + if [[ "${is_ge_python36}" == "1" ]]; then + if_ubuntu version=6.1.0 + if_ubuntu install_packages ninja-build + fi + + local td + td="$(mktemp -d)" + + pushd "${td}" + + curl --retry 3 -sSfL "https://download.qemu.org/qemu-${version}.tar.xz" -O + tar --strip-components=1 -xJf "qemu-${version}.tar.xz" + + local targets="${arch}-linux-user" + local virtfs="" + case "${softmmu}" in + softmmu) + if [ "${arch}" = "ppc64le" ]; then + targets="${targets},ppc64-softmmu" + else + targets="${targets},${arch}-softmmu" + fi + virtfs="--enable-virtfs" + ;; + "") + true + ;; + *) + echo "Invalid softmmu option: ${softmmu}" + exit 1 + ;; + esac + + ./configure \ + --disable-kvm \ + --disable-vnc \ + --disable-guest-agent \ + --enable-linux-user \ + --static \ + ${virtfs} \ + --target-list="${targets}" + make "-j$(nproc)" + make install + + # HACK the binfmt_misc interpreter we'll use expects the QEMU binary to be + # in /usr/bin. Create an appropriate symlink + ln -s "/usr/local/bin/qemu-${arch}" "/usr/bin/qemu-${arch}-static" + + purge_packages + + popd + + rm -rf "${td}" + rm "${0}" +} + +main "${@}" diff --git a/.docker/cross/xargo.sh b/.docker/cross/xargo.sh new file mode 100755 index 0000000..5f7e2dc --- /dev/null +++ b/.docker/cross/xargo.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -x +set -euo pipefail + +# shellcheck disable=SC1091 +. lib.sh + +main() { + install_packages ca-certificates curl + + export RUSTUP_HOME=/tmp/rustup + export CARGO_HOME=/tmp/cargo + + curl --retry 3 -sSfL https://sh.rustup.rs -o rustup-init.sh + sh rustup-init.sh -y --no-modify-path --profile minimal + rm rustup-init.sh + + PATH="${CARGO_HOME}/bin:${PATH}" cargo install xargo --root /usr/local + + rm -r "${RUSTUP_HOME}" "${CARGO_HOME}" + + purge_packages + + rm "${0}" +} + +main "${@}" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9d08a1a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..a5296b4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,8 @@ +# Current WG Code Sub Teams: +# @tauri-apps/wg-tauri +# @tauri-apps/wg-devops + +# Order is important; the last matching pattern takes the most precedence. +* @tauri-apps/wg-tauri + +.github @tauri-apps/wg-devops diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..aa35f0f --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[contact@tauri.app](mailto:contact@tauri.app). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..a30e677 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,120 @@ +# Tauri Contributing Guide + +Hi! We, the maintainers, are really excited that you are interested in contributing to Tauri. Before submitting your contribution though, please make sure to take a moment and read through the [Code of Conduct](/.github/CODE_OF_CONDUCT.md), as well as the appropriate section for the contribution you intend to make: + +- [Issue Reporting Guidelines](#issue-reporting-guidelines) +- [Pull Request Guidelines](#pull-request-guidelines) +- [Development Guide](#development-guide) +- [AI Tool Policy](#ai-tool-policy) + +## Issue Reporting Guidelines + +- The issue list of this repo is **exclusively** for bug reports and feature requests. Non-conforming issues will be closed immediately. + +- If you have a question, you can get quick answers from the [Tauri Discord chat](https://discord.gg/SpmNs4S). + +- Try to search for your issue, it may have already been answered or even fixed in the development branch (`dev`). + +- Check if the issue is reproducible with the latest stable version of Tauri. If you are using a pre-release, please indicate the specific version you are using. + +- It is **required** that you clearly describe the steps necessary to reproduce the issue you are running into. Although we would love to help our users as much as possible, diagnosing issues without clear reproduction steps is extremely time-consuming and simply not sustainable. + +- Use only the minimum amount of code necessary to reproduce the unexpected behavior. A good bug report should isolate specific methods that exhibit unexpected behavior and precisely define how expectations were violated. What did you expect the method or methods to do, and how did the observed behavior differ? The more precisely you isolate the issue, the faster we can investigate. + +- Issues with no clear repro steps will not be triaged. If an issue labeled "need repro" receives no further input from the issue author for more than 5 days, it will be closed. + +- If your issue is resolved but still open, don't hesitate to close it. In case you found a solution by yourself, it could be helpful to explain how you fixed it. + +- Most importantly, we beg your patience: the team must balance your request against many other responsibilities — fixing other bugs, answering other questions, new features, new documentation, etc. The issue list is not paid support and we cannot make guarantees about how fast your issue can be resolved. + +## Pull Request Guidelines + +- You have to [sign your commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). + +- It's OK to have multiple small commits as you work on the PR - we will let GitHub automatically squash it before merging. + +- If adding new feature: + - Provide convincing reason to add this feature. Ideally you should open a suggestion issue first and have it greenlighted before working on it. + +- If fixing a bug: + - If you are resolving a special issue, add `(fix: #xxxx[,#xxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `fix: update entities encoding/decoding (fix #3899)`. + - Provide detailed description of the bug in the PR, or link to an issue that does. + +- If the PR is meant to be released, follow the instructions in `.changes/readme.md` to log your changes. ie. [readme.md](https://github.com/tauri-apps/tauri/blob/dev/.changes/README.md) + +## Development Guide + +**NOTE: If you have any question don't hesitate to ask in our Discord server. We try to keep this guide to up guide, but if something doesn't work let us know.** + +### General Setup + +First, [join our Discord server](https://discord.gg/SpmNs4S) and let us know that you want to contribute. This way we can point you in the right direction and help ensure your contribution will be as helpful as possible. + +To set up your machine for development, follow the [Tauri setup guide](https://v2.tauri.app/start/prerequisites/) to get all the tools you need to develop Tauri apps. The only additional tool you may need is [PNPM](https://pnpm.io/), it is only required if you are developing the Node CLI or API packages (`packages/cli` and `packages/api`). + +Next, [fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) and clone [this repository](https://github.com/tauri-apps). +The development process varies depending on what part of Tauri you are contributing to, see the guides below for per-package instructions. + +Some Tauri packages will be automatically built when running one of the examples. Others, however, will need to be built beforehand. To initialize, execute these commands in the repository root: + +```bash +pnpm install +pnpm build +``` + +### Overview + +See [Architecture](/ARCHITECTURE.md#major-components) for an overview of the packages in this repository. + +### Developing Tauri Core and Related Components (Rust API, Macros, Codegen, and Utils) + +The code for the Rust crates, including the Core, Macros, Utils, WRY runtime, and a few more are located in the [main Tauri repository](https://github.com/tauri-apps/tauri/tree/dev/crates). + +The easiest way to test your changes is to use the [helloworld](https://github.com/tauri-apps/tauri/tree/dev/examples/helloworld) example app. It automatically rebuilds and uses your local copy of the Tauri core packages. Just run `cargo run --example helloworld` after making changes to test them out. + +To test local changes against your own application simply point the Tauri create to your local repository. In `src-tauri/Cargo.toml` file change: + +`tauri = { version = "2.1.1" }` + +to: + +`tauri = { path = "path/to/local/tauri/crates/tauri" }` + +If any other crates depend on Tauri you will have to point them to the local repo as well. + +### Developing Tauri Bundler and Rust CLI + +The code for the bundler is located in [crates/tauri-bundler](https://github.com/tauri-apps/tauri/tree/dev/crates/tauri-bundler), and the code for the Rust CLI is located in [tauri-cli](https://github.com/tauri-apps/tauri/tree/dev/crates/tauri-cli). +Running `cargo install --path .` in the Rust CLI directory will allow you to run `cargo tauri build` and `cargo tauri dev` anywhere, using the updated copy of the bundler and cli. You will have to run this command each time you make a change in either package. +You can use `cargo install --path . --debug` to speed up test builds. + +### Developing The Node.js CLI (`@tauri-apps/cli`) + +[`@tauri-apps/cli`](https://github.com/tauri-apps/tauri/tree/dev/packages/cli) is a small wrapper around `tauri-cli` so most changes should be happen in the Rust CLI (see above). + +#### Building the documentation locally + +You can build the Rust documentation locally running the following script: + +```bash +$ cargo +nightly doc --all-features --open +``` + +### Developing the JS API + +The JS API provides bindings between the developer's JS in the Webview and the built-in Tauri APIs, written in Rust. Its code is located in [/packages/api](https://github.com/tauri-apps/tauri/tree/dev/packages/api). +After making changes to the code, run `pnpm build` to build it. To test your changes, we recommend using the API example app, located in [/examples/api](https://github.com/tauri-apps/tauri/tree/dev/examples/api). It will automatically use your local copy of the JS API and provides a helpful UI to test the various commands. + +## AI Tool Policy + +It takes a lot of time to review a Pull Request while it's very easy to make a nonsensical but plausible looking one using AI tools. +It is unfair for other contributors and the reviewers to spend much of the time dealing with this, hence these rules: + +1. Review and test all LLM-generated content before submitting, you're the one responsible for it, not the AI. +2. Don't use AI to respond to review comments (except for translations). + +We will close the Pull Request with an `ai-slop` tag if you fail to follow these rules. + +## Financial Contribution + +Tauri is an MIT-licensed open source project. Its ongoing development can be supported via [GitHub Sponsors](https://github.com/sponsors/tauri-apps) or [Open Collective](https://opencollective.com/tauri). We prefer GitHub Sponsors as donations made are doubled through the matching fund program. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..062d6b9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# Copyright 2019-2024 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +# These are supported funding model platforms + +github: tauri-apps +patreon: # +open_collective: tauri +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: # Replace with a single custom sponsorship URL diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..6e51eff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,63 @@ +# Copyright 2019-2024 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: 🐞 Bug Report +title: '[bug] ' +description: Report a bug +labels: ['type: bug', 'status: needs triage'] + +body: + - type: markdown + attributes: + value: | + ## First of all + 1. Please search for [existing issues](https://github.com/tauri-apps/tauri/issues?q=is%3Aissue) about this problem first. + 2. Make sure `rustc` and all relevant Tauri packages are up to date. + 3. Make sure it's an issue with Tauri and not something else you are using. + 4. Remember to follow our community guidelines and be friendly. + + - type: textarea + id: description + attributes: + label: Describe the bug + description: A clear description of what the bug is. Include screenshots if applicable. + placeholder: Bug description + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: A link to a reproduction repo or steps to reproduce the behaviour. + placeholder: | + Please provide a minimal reproduction or steps to reproduce, see this guide https://stackoverflow.com/help/minimal-reproducible-example + Why reproduction is required? see this article https://antfu.me/posts/why-reproductions-are-required + + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: A clear description of what you expected to happen. + + - type: textarea + id: info + attributes: + label: Full `tauri info` output + description: 'Output of `npm run tauri info` or `cargo tauri info`' + render: text + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Stack trace + render: text + + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..dabd032 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +# Copyright 2019-2024 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +contact_links: + - name: 💬 Discord Chat + url: https://discord.com/invite/tauri + about: Ask questions and talk to other Tauri users and the maintainers diff --git a/.github/ISSUE_TEMPLATE/docs_report.md b/.github/ISSUE_TEMPLATE/docs_report.md new file mode 100644 index 0000000..27df742 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/docs_report.md @@ -0,0 +1,7 @@ +--- +name: 📚 Docs Report +about: Create a report to help us improve the docs +title: '[docs] ' +labels: 'type: documentation' +assignees: '' +--- diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..266eb27 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,39 @@ +# Copyright 2019-2024 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: 💡 Feature Request +title: '[feat] ' +description: Suggest an idea +labels: ['type: feature request'] + +body: + - type: textarea + id: problem + attributes: + label: Describe the problem + description: A clear description of the problem this feature would solve + placeholder: "I'm always frustrated when..." + validations: + required: true + + - type: textarea + id: solution + attributes: + label: "Describe the solution you'd like" + description: A clear description of what change you would like + placeholder: 'I would like to...' + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: "Any alternative solutions you've considered" + + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context about the problem here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..bd297e8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ + diff --git a/.github/RELEASING.md b/.github/RELEASING.md new file mode 100644 index 0000000..0528809 --- /dev/null +++ b/.github/RELEASING.md @@ -0,0 +1,57 @@ +# Tauri Releasing Handbook + +This handbook contains information about our release pipeline and how to deal with common issues. +This document is mainly intended for team members responsible for maintaining the project. + +- [Covector](#covector) +- [Version Pull Request](#version-pull-request) +- [Releasing and Publishing](#releasing-and-publishing) +- [Publishing failed, what to do?](#publishing-failed-what-to-do) + +## Covector + +We use [`covector`](https://github.com/jbolda/covector) to manage our version bumps and release pipeline. +It can be configured in [`.changes/config.json`](../.changes/config.json) which includes how each package should be published step by step. + +Some packages can't be published directly using `covector` as it requires to be built on a matrix of platforms +such as `tauri-cli` prebuilt binaries which is published using [publish-cli-rs.yml](./workflows/publish-cli-rs.yml) +and `@tauri-apps/cli` native Node.js modules which is published using [publish-cli-js.yml](./workflows/publish-cli-js.yml) +both of which are triggered after `covector` has created a github release for both of them, see `Trigger @tauri-apps/cli publishing workflow` +and `Trigger tauri-cli publishing workflow` steps in [covector-version-or-publish.yml](./workflows/covector-version-or-publish.yml) + +## Version Pull Request + +On each pull request merged, [covector-version-or-publish.yml](./workflows/covector-version-or-publish.yml) workflow will run, and: + +When there're change files inside `.changes` folder and they're not all included in `pre.json` (usually this is only when we are in `-alpha` to `-rc` phase), it will open/update an `Apply Version Updates From Current Changes` PR (https://github.com/tauri-apps/tauri/pull/11029 for example) that bumps all packages based on current existing change files and generate `CHANGELOG.md` entries. see `Create Pull Request With Versions Bumped` step in [covector-version-or-publish.yml](./workflows/covector-version-or-publish.yml). + +Otherwise, covector will start to publish packages configured in [`.changes/config.json`](../.changes/config.json). + +## Releasing and Publishing + +Releasing can be as easy as merging the version pull request but here is a checklist to follow: + +- [ ] Double check that every package is bumped correctly and there are no accidental major or minor being released unless that is indeed the intention. +- [ ] Make sure that there are no pending or unfinished [covector-version-or-publish.yml](./workflows/covector-version-or-publish.yml) workflow runs. +- [ ] Approve and merge the version pull request + +## Publishing failed, what to do? + +It is possible and due to many factors that one or many packages release can fail to release, there is no reason to panic, we can fix this. + +Did all of the packages fail to release? + +- yes? + - [ ] `git checkout -b revert-branch` + - [ ] `git revert HEAD~1` +- no? + - [ ] `git checkout -b revert-branch` + - [ ] `git revert HEAD~1 --no-commit` + - [ ] Edit the commit and revert only changes related to packages that failed to publish + - [ ] `git revert --continue` + +Then: + +- [ ] Make a pull request with reverted changes, get it approved and merged +- [ ] Fix the issue that caused releases to fail in another PR, get it approved and merged +- [ ] Repeat the release process again. diff --git a/.github/icon.png b/.github/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f71d543ce36d92ae0f6ac89ca04d8ec72b79c2e8 GIT binary patch literal 10955 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 z zL5pdrh5>Vp!R<>>O(dH0w%8mRNp4Bhyj<<~Yf)~0W06S9!PKx{_I2@|I!rEVI;b#YIKD zervwDdtaAiZKG7ZFLM4b+JCCdt9PiT?ps$8v%kI|W)rL_^UY7GJ|pk)1wG2WL^wIK zA}NO6+5J3k7zsqU0&aWoZM$9&QPSO$B*h(kve@Fr*{t~x35`6sx2xgc)emI zm7c5&vFxIEU_7>5kJ!R?NGt-^_L!t5pmfBV{{@iZ2S~1>YP0v($)_?1$60p%q2yto zzH>G-h?S!
+

Welcome to Tauri!

+ +
+ +

Click on the Tauri, Vite, and React logos to learn more.

+ +
{ + e.preventDefault(); + greet(); + }} + > + setName(e.currentTarget.value)} + placeholder="Enter a name..." + /> + +
+ +

{greetMsg}

+
+ ); +} + +export default App; +"#; + + let expected = r#" +import { useState } from "react"; +import reactLogo from "./assets/react.svg"; +import { invoke, } from "@tauri-apps/api"; +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; +import { convertFileSrc } from "@tauri-apps/api/core"; +import { open } from "@tauri-apps/plugin-dialog"; +import { register } from "@tauri-apps/plugin-global-shortcut"; +import clipboard from "@tauri-apps/plugin-clipboard-manager"; +import * as fs from "@tauri-apps/plugin-fs"; +import { Store } from "@tauri-apps/plugin-store"; +import Database from "@tauri-apps/plugin-sql"; +import "./App.css"; +import * as dialog from "@tauri-apps/plugin-dialog" +import * as superCli from "@tauri-apps/plugin-cli" +const appWindow = getCurrentWebviewWindow() + +function App() { + const [greetMsg, setGreetMsg] = useState(""); + const [name, setName] = useState(""); + + async function greet() { + // Learn more about Tauri commands at https://v2.tauri.app/develop/calling-rust/#commands + setGreetMsg(await invoke("greet", { name })); + await open(); + await dialog.save(); + await convertFileSrc(""); + const a = appWindow.label; + superCli.getMatches(); + clipboard.readText(); + fs.exists(""); + } + + return ( +
+

Welcome to Tauri!

+ + + +

Click on the Tauri, Vite, and React logos to learn more.

+ +
{ + e.preventDefault(); + greet(); + }} + > + setName(e.currentTarget.value)} + placeholder="Enter a name..." + /> + +
+ +

{greetMsg}

+
+ ); +} + +export default App; +"#; + + let mut new_plugins = Vec::new(); + let mut npm_packages_to_remove = Vec::new(); + + let migrated = migrate_imports( + Path::new("file.js"), + input, + &mut new_plugins, + &mut npm_packages_to_remove, + ) + .unwrap(); + + assert_eq!(migrated, expected); + assert_migrated_output_parses(Path::new("file.js"), &migrated); + + assert_eq!( + new_plugins, + vec![ + "dialog", + "cli", + "dialog", + "global-shortcut", + "clipboard-manager", + "fs", + "store", + "sql" + ] + ); + assert_eq!( + npm_packages_to_remove, + vec!["tauri-plugin-store-api", "tauri-plugin-sql-api"] + ); + } +} diff --git a/crates/tauri-cli/src/migrate/migrations/v1/frontend/partial_loader/mod.rs b/crates/tauri-cli/src/migrate/migrations/v1/frontend/partial_loader/mod.rs new file mode 100644 index 0000000..bb95b33 --- /dev/null +++ b/crates/tauri-cli/src/migrate/migrations/v1/frontend/partial_loader/mod.rs @@ -0,0 +1,69 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// taken from https://github.com/oxc-project/oxc/blob/main/crates/oxc_linter/src/partial_loader/mod.rs + +mod svelte; +mod vue; + +use oxc_span::SourceType; + +pub use self::{svelte::SveltePartialLoader, vue::VuePartialLoader}; + +const SCRIPT_START: &str = " { + pub source_text: &'a str, + pub source_type: SourceType, + /// The javascript source could be embedded in some file, + /// use `start` to record start offset of js block in the original file. + pub start: usize, +} + +impl<'a> JavaScriptSource<'a> { + pub fn new(source_text: &'a str, source_type: SourceType, start: usize) -> Self { + Self { + source_text, + source_type, + start, + } + } +} + +pub struct PartialLoader; + +impl PartialLoader { + /// Extract js section of specifial files. + /// Returns `None` if the specifial file does not have a js section. + pub fn parse<'a>(ext: &str, source_text: &'a str) -> Option>> { + match ext { + "vue" => Some(VuePartialLoader::new(source_text).parse()), + "svelte" => Some(SveltePartialLoader::new(source_text).parse()), + _ => None, + } + } +} + +/// Find closing angle for situations where there is another `>` in between. +/// e.g. `" + let offset = script_end_finder.find(&self.source_text.as_bytes()[pointer..])?; + let js_end = pointer + offset; + + let source_text = &self.source_text[js_start..js_end]; + let source_type = SourceType::default() + .with_module(true) + .with_typescript(is_ts); + Some(JavaScriptSource::new(source_text, source_type, js_start)) + } +} + +#[cfg(test)] +mod test { + use super::{JavaScriptSource, SveltePartialLoader}; + + fn parse_svelte(source_text: &str) -> JavaScriptSource<'_> { + let sources = SveltePartialLoader::new(source_text).parse(); + *sources.first().unwrap() + } + + #[test] + fn test_parse_svelte() { + let source_text = r#" + +

Hello World

+ "#; + + let result = parse_svelte(source_text); + assert_eq!(result.source_text.trim(), r#"console.log("hi");"#); + } + + #[test] + fn test_parse_svelte_ts_with_generic() { + let source_text = r#" + +

Hello World

+ "#; + + let result = parse_svelte(source_text); + assert_eq!(result.source_text.trim(), r#"console.log("hi");"#); + } +} diff --git a/crates/tauri-cli/src/migrate/migrations/v1/frontend/partial_loader/vue.rs b/crates/tauri-cli/src/migrate/migrations/v1/frontend/partial_loader/vue.rs new file mode 100644 index 0000000..d1723b6 --- /dev/null +++ b/crates/tauri-cli/src/migrate/migrations/v1/frontend/partial_loader/vue.rs @@ -0,0 +1,246 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// taken from https://github.com/oxc-project/oxc/blob/main/crates/oxc_linter/src/partial_loader/vue.rs + +use memchr::memmem::Finder; +use oxc_span::SourceType; + +use super::{JavaScriptSource, SCRIPT_END, SCRIPT_START, find_script_closing_angle}; + +pub struct VuePartialLoader<'a> { + source_text: &'a str, +} + +impl<'a> VuePartialLoader<'a> { + pub fn new(source_text: &'a str) -> Self { + Self { source_text } + } + + pub fn parse(self) -> Vec> { + self.parse_scripts() + } + + /// Each *.vue file can contain at most + /// * one `" + let offset = script_end_finder.find(&self.source_text.as_bytes()[*pointer..])?; + let js_end = *pointer + offset; + *pointer += offset + SCRIPT_END.len(); + + let source_text = &self.source_text[js_start..js_end]; + let source_type = SourceType::default() + .with_module(true) + .with_typescript(is_ts) + .with_jsx(is_jsx); + Some(JavaScriptSource::new(source_text, source_type, js_start)) + } +} + +#[cfg(test)] +mod test { + use super::{JavaScriptSource, VuePartialLoader}; + + fn parse_vue(source_text: &str) -> JavaScriptSource<'_> { + let sources = VuePartialLoader::new(source_text).parse(); + *sources.first().unwrap() + } + + #[test] + fn test_parse_vue_one_line() { + let source_text = r#" + + + "#; + + let result = parse_vue(source_text); + assert_eq!(result.source_text, r#" console.log("hi") "#); + } + + #[test] + fn test_build_vue_with_ts_flag_1() { + let source_text = r#" + + "#; + + let result = parse_vue(source_text); + assert!(result.source_type.is_typescript()); + assert_eq!(result.source_text.trim(), "1/1"); + } + + #[test] + fn test_build_vue_with_ts_flag_2() { + let source_text = r" + + "; + + let result = parse_vue(source_text); + assert!(result.source_type.is_typescript()); + assert_eq!(result.source_text.trim(), "1/1"); + } + + #[test] + fn test_build_vue_with_ts_flag_3() { + let source_text = r" + + "; + + let result = parse_vue(source_text); + assert!(result.source_type.is_typescript()); + assert_eq!(result.source_text.trim(), "1/1"); + } + + #[test] + fn test_build_vue_with_tsx_flag() { + let source_text = r" + + "; + + let result = parse_vue(source_text); + assert!(result.source_type.is_jsx()); + assert!(result.source_type.is_typescript()); + assert_eq!(result.source_text.trim(), "1/1"); + } + + #[test] + fn test_build_vue_with_escape_string() { + let source_text = r" + + + "; + + let result = parse_vue(source_text); + assert!(!result.source_type.is_typescript()); + assert_eq!(result.source_text.trim(), r"a.replace(/'/g, '\''))"); + } + + #[test] + fn test_multi_level_template_literal() { + let source_text = r" + + "; + + let result = parse_vue(source_text); + assert_eq!(result.source_text.trim(), r"`a${b( `c \`${d}\``)}`"); + } + + #[test] + fn test_brace_with_regex_in_template_literal() { + let source_text = r" + + "; + + let result = parse_vue(source_text); + assert_eq!(result.source_text.trim(), r"`${/{/}`"); + } + + #[test] + fn test_no_script() { + let source_text = r" + + "; + + let sources = VuePartialLoader::new(source_text).parse(); + assert!(sources.is_empty()); + } + + #[test] + fn test_syntax_error() { + let source_text = r" + + + "; + let sources = VuePartialLoader::new(source_text).parse(); + assert_eq!(sources.len(), 2); + assert_eq!(sources[0].source_text, "a"); + assert_eq!(sources[1].source_text, "b"); + } + + #[test] + fn test_unicode() { + let source_text = r" + + "; + + let result = parse_vue(source_text); + assert_eq!( + result.source_text.trim(), + "let 日历 = '2000年'; + const t = useTranslate({ + 'zh-CN': { + calendar: '日历', + tiledDisplay: '平铺展示', + }, + });" + .trim() + ); + } +} diff --git a/crates/tauri-cli/src/migrate/migrations/v1/manifest.rs b/crates/tauri-cli/src/migrate/migrations/v1/manifest.rs new file mode 100644 index 0000000..e0d8c2e --- /dev/null +++ b/crates/tauri-cli/src/migrate/migrations/v1/manifest.rs @@ -0,0 +1,415 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + Result, + error::ErrorExt, + interface::rust::manifest::{read_manifest, serialize_manifest}, +}; + +use tauri_utils::config_v1::Allowlist; +use toml_edit::{DocumentMut, Entry, Item, TableLike, Value}; + +use std::path::Path; + +const CRATE_TYPES: [&str; 3] = ["lib", "staticlib", "cdylib"]; + +pub fn migrate(tauri_dir: &Path) -> Result<()> { + let manifest_path = tauri_dir.join("Cargo.toml"); + let (mut manifest, _) = read_manifest(&manifest_path)?; + migrate_manifest(&mut manifest)?; + + std::fs::write(&manifest_path, serialize_manifest(&manifest)) + .fs_context("failed to rewrite Cargo manifest", &manifest_path)?; + + Ok(()) +} + +fn migrate_manifest(manifest: &mut DocumentMut) -> Result<()> { + let version = dependency_version(); + + let remove_features = features_to_remove(); + let rename_features = features_to_rename(); + let rename_message = rename_features + .iter() + .map(|(from, to)| format!("{from} to {to}")) + .collect::>() + .join(", "); + + for (dependency, table) in [ + // normal deps + ("tauri", "dependencies"), + ("tauri-utils", "dependencies"), + ("tauri-runtime", "dependencies"), + ("tauri-codegen", "dependencies"), + ("tauri-macros", "dependencies"), + ("tauri-runtime-wry", "dependencies"), + // normal deps - plugins + ("tauri-plugin-authenticator", "dependencies"), + ("tauri-plugin-autostart", "dependencies"), + ("tauri-plugin-fs-extra", "dependencies"), + ("tauri-plugin-fs-watch", "dependencies"), + ("tauri-plugin-localhost", "dependencies"), + ("tauri-plugin-log", "dependencies"), + ("tauri-plugin-persisted-scope", "dependencies"), + ("tauri-plugin-positioner", "dependencies"), + ("tauri-plugin-single-instance", "dependencies"), + ("tauri-plugin-sql", "dependencies"), + ("tauri-plugin-store", "dependencies"), + ("tauri-plugin-stronghold", "dependencies"), + ("tauri-plugin-upload", "dependencies"), + ("tauri-plugin-websocket", "dependencies"), + ("tauri-plugin-window-state", "dependencies"), + // dev + ("tauri", "dev-dependencies"), + ("tauri-utils", "dev-dependencies"), + ("tauri-runtime", "dev-dependencies"), + ("tauri-codegen", "dev-dependencies"), + ("tauri-macros", "dev-dependencies"), + ("tauri-runtime-wry", "dev-dependencies"), + // build + ("tauri-build", "build-dependencies"), + ] { + let items = find_dependency(manifest, dependency, table); + + for item in items { + // do not rewrite if dependency uses workspace inheritance + if item + .get("workspace") + .and_then(|v| v.as_bool()) + .unwrap_or_default() + { + log::warn!( + "`{dependency}` dependency has workspace inheritance enabled. This migration must be manually migrated to v2 by changing its version to {version}, removing any of the {remove_features:?} and renaming [{}] Cargo features.", + rename_message + ); + } else { + migrate_dependency(item, &version, &remove_features, &rename_features); + } + } + } + + if let Some(lib) = manifest + .as_table_mut() + .get_mut("lib") + .and_then(|l| l.as_table_mut()) + { + match lib.entry("crate-type") { + Entry::Occupied(mut e) => { + if let Item::Value(Value::Array(types)) = e.get_mut() { + let mut crate_types_to_add = CRATE_TYPES.to_vec(); + for t in types.iter() { + // type is already in the manifest, skip adding it + if let Some(i) = crate_types_to_add + .iter() + .position(|ty| Some(ty) == t.as_str().as_ref()) + { + crate_types_to_add.remove(i); + } + } + for t in crate_types_to_add { + types.push(t); + } + } + } + Entry::Vacant(e) => { + let mut arr = toml_edit::Array::new(); + arr.extend(CRATE_TYPES.to_vec()); + e.insert(Item::Value(arr.into())); + } + } + } + + Ok(()) +} + +fn find_dependency<'a>( + manifest: &'a mut DocumentMut, + name: &'a str, + table: &'a str, +) -> Vec<&'a mut Item> { + let m = manifest.as_table_mut(); + for (k, v) in m.iter_mut() { + if let Some(t) = v.as_table_mut() { + if k == table { + if let Some(item) = t.get_mut(name) { + return vec![item]; + } + } else if k == "target" { + let mut matching_deps = Vec::new(); + for (_, target_value) in t.iter_mut() { + if let Some(target_table) = target_value.as_table_mut() { + if let Some(deps) = target_table.get_mut(table) { + if let Some(item) = deps.as_table_mut().and_then(|t| t.get_mut(name)) { + matching_deps.push(item); + } + } + } + } + return matching_deps; + } + } + } + + Vec::new() +} + +fn features_to_rename() -> Vec<(&'static str, &'static str)> { + vec![ + ("window-data-url", "webview-data-url"), + ("reqwest-native-tls-vendored", "native-tls-vendored"), + ("system-tray", "tray-icon"), + ("icon-ico", "image-ico"), + ("icon-png", "image-png"), + ] +} + +fn features_to_remove() -> Vec<&'static str> { + let mut features_to_remove = tauri_utils::config_v1::AllowlistConfig::all_features(); + features_to_remove.extend(&[ + "reqwest-client", + "http-multipart", + "process-command-api", + "shell-open-api", + "os-api", + "global-shortcut", + "clipboard", + "dialog", + "notification", + "fs-extract-api", + "windows7-compat", + "updater", + "cli", + "linux-protocol-headers", + "dox", + ]); + + // this allowlist feature was not removed + let index = features_to_remove + .iter() + .position(|x| x == &"protocol-asset") + .unwrap(); + features_to_remove.remove(index); + + features_to_remove +} + +fn dependency_version() -> String { + let pre = env!("CARGO_PKG_VERSION_PRE"); + if pre.is_empty() { + env!("CARGO_PKG_VERSION_MAJOR").to_string() + } else { + format!( + "{}.0.0-{}", + env!("CARGO_PKG_VERSION_MAJOR"), + pre.split('.').next().unwrap() + ) + } +} + +fn migrate_dependency(item: &mut Item, version: &str, remove: &[&str], rename: &[(&str, &str)]) { + if let Some(dep) = item.as_table_mut() { + migrate_dependency_table(dep, version, remove, rename); + } else if let Some(Value::InlineTable(table)) = item.as_value_mut() { + migrate_dependency_table(table, version, remove, rename); + } else if item.as_str().is_some() { + *item = Item::Value(version.into()); + } +} + +fn migrate_dependency_table( + dep: &mut D, + version: &str, + remove: &[&str], + rename: &[(&str, &str)], +) { + dep.remove("rev"); + dep.remove("git"); + dep.remove("branch"); + dep.remove("tag"); + *dep.entry("version").or_insert(Item::None) = Item::Value(version.into()); + let manifest_features = dep.entry("features").or_insert(Item::None); + if let Some(features_array) = manifest_features.as_array_mut() { + // remove features that shouldn't be in the manifest anymore + let mut i = features_array.len(); + let mut add_features = Vec::new(); + while i != 0 { + let index = i - 1; + if let Some(f) = features_array.get(index).and_then(|f| f.as_str()) { + if remove.contains(&f) { + features_array.remove(index); + } else if let Some((_from, rename_to)) = rename.iter().find(|(from, _to)| *from == f) { + features_array.remove(index); + add_features.push(rename_to); + } + } + i -= 1; + } + + for f in add_features { + features_array.push(f.to_string()); + } + } +} + +#[cfg(test)] +mod tests { + use itertools::Itertools; + + fn migrate_deps String>(get_toml: F) { + let keep_features = vec!["isolation", "protocol-asset"]; + let mut features = super::features_to_remove(); + features.extend(keep_features.clone()); + let toml = get_toml(&features); + + let mut manifest = toml + .parse::() + .expect("invalid toml"); + super::migrate_manifest(&mut manifest).expect("failed to migrate manifest"); + + let dependencies = manifest + .as_table() + .get("dependencies") + .expect("missing manifest dependencies") + .as_table() + .expect("manifest dependencies isn't a table"); + + let tauri = dependencies + .get("tauri") + .expect("missing tauri dependency in manifest"); + + let tauri_table = if let Some(table) = tauri.as_table() { + table.clone() + } else if let Some(toml_edit::Value::InlineTable(table)) = tauri.as_value() { + table.clone().into_table() + } else if let Some(version) = tauri.as_str() { + // convert the value to a table for the assert logic below + let mut table = toml_edit::Table::new(); + table.insert( + "version", + toml_edit::Item::Value(version.to_string().into()), + ); + table.insert( + "features", + toml_edit::Item::Value(toml_edit::Value::Array(Default::default())), + ); + table + } else { + panic!("unexpected tauri dependency format"); + }; + + // assert version matches + let version = tauri_table + .get("version") + .expect("missing version") + .as_str() + .expect("version must be a string"); + assert_eq!(version, super::dependency_version()); + + // assert features matches + let features = tauri_table + .get("features") + .expect("missing features") + .as_array() + .expect("features must be an array") + .clone(); + + if toml.contains("reqwest-native-tls-vendored") { + assert!( + features + .iter() + .any(|f| f.as_str().expect("feature must be a string") == "native-tls-vendored"), + "reqwest-native-tls-vendored was not replaced with native-tls-vendored" + ); + } + + if toml.contains("system-tray") { + assert!( + features + .iter() + .any(|f| f.as_str().expect("feature must be a string") == "tray-icon"), + "system-tray was not replaced with tray-icon" + ); + } + + for feature in features.iter() { + let feature = feature.as_str().expect("feature must be a string"); + assert!( + keep_features.contains(&feature) + || feature == "native-tls-vendored" + || feature == "tray-icon", + "feature {feature} should have been removed" + ); + } + } + + #[test] + fn migrate_table() { + migrate_deps(|features| { + format!( + r#" + [dependencies] + tauri = {{ version = "1.0.0", features = [{}] }} +"#, + features.iter().map(|f| format!("{f:?}")).join(", ") + ) + }); + } + + #[test] + fn migrate_inline_table() { + migrate_deps(|features| { + format!( + r#" + [dependencies.tauri] + version = "1.0.0" + features = [{}] +"#, + features.iter().map(|f| format!("{f:?}")).join(", ") + ) + }); + } + + #[test] + fn migrate_str() { + migrate_deps(|_features| { + r#" + [dependencies] + tauri = "1.0.0" +"# + .into() + }) + } + + #[test] + fn migrate_add_crate_types() { + let toml = r#" + [lib] + crate-type = ["something"]"#; + + let mut manifest = toml + .parse::() + .expect("invalid toml"); + super::migrate_manifest(&mut manifest).expect("failed to migrate manifest"); + + if let Some(crate_types) = manifest + .as_table() + .get("lib") + .and_then(|l| l.get("crate-type")) + .and_then(|c| c.as_array()) + { + let mut not_added_crate_types = super::CRATE_TYPES.to_vec(); + for t in crate_types { + let t = t.as_str().expect("crate-type must be a string"); + if let Some(i) = not_added_crate_types.iter().position(|ty| ty == &t) { + not_added_crate_types.remove(i); + } + } + assert!( + not_added_crate_types.is_empty(), + "missing crate-type: {not_added_crate_types:?}" + ); + } + } +} diff --git a/crates/tauri-cli/src/migrate/migrations/v1/mod.rs b/crates/tauri-cli/src/migrate/migrations/v1/mod.rs new file mode 100644 index 0000000..1559749 --- /dev/null +++ b/crates/tauri-cli/src/migrate/migrations/v1/mod.rs @@ -0,0 +1,34 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{Result, error::Context, helpers::app_paths::Dirs}; + +mod config; +mod frontend; +mod manifest; + +pub fn run(dirs: &Dirs) -> Result<()> { + let mut migrated = config::migrate(dirs.tauri).context("Could not migrate config")?; + manifest::migrate(dirs.tauri).context("Could not migrate manifest")?; + let plugins = frontend::migrate(dirs.frontend)?; + + migrated.plugins.extend(plugins); + + // Add plugins + for plugin in migrated.plugins { + crate::add::run( + crate::add::Options { + plugin: plugin.clone(), + branch: None, + tag: None, + rev: None, + no_fmt: false, + }, + dirs, + ) + .with_context(|| format!("Could not migrate plugin '{plugin}'"))?; + } + + Ok(()) +} diff --git a/crates/tauri-cli/src/migrate/migrations/v2_beta.rs b/crates/tauri-cli/src/migrate/migrations/v2_beta.rs new file mode 100644 index 0000000..4e18a73 --- /dev/null +++ b/crates/tauri-cli/src/migrate/migrations/v2_beta.rs @@ -0,0 +1,194 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + Result, + error::{Context, ErrorExt}, + helpers::{app_paths::Dirs, npm::PackageManager}, + interface::rust::manifest::{read_manifest, serialize_manifest}, +}; + +use std::{fs::read_to_string, path::Path}; + +use toml_edit::{DocumentMut, Item, Table, TableLike, Value}; + +pub fn run(dirs: &Dirs) -> Result<()> { + let manifest_path = dirs.tauri.join("Cargo.toml"); + let (mut manifest, _) = read_manifest(&manifest_path)?; + migrate_manifest(&mut manifest)?; + + migrate_permissions(dirs.tauri)?; + + migrate_npm_dependencies(dirs.frontend)?; + + std::fs::write(&manifest_path, serialize_manifest(&manifest)) + .fs_context("failed to rewrite Cargo manifest", &manifest_path)?; + + Ok(()) +} + +fn migrate_npm_dependencies(frontend_dir: &Path) -> Result<()> { + let pm = PackageManager::from_project(frontend_dir); + + let mut install_deps = Vec::new(); + for pkg in [ + "@tauri-apps/cli", + "@tauri-apps/api", + "@tauri-apps/plugin-authenticator", + "@tauri-apps/plugin-autostart", + "@tauri-apps/plugin-barcode-scanner", + "@tauri-apps/plugin-biometric", + "@tauri-apps/plugin-cli", + "@tauri-apps/plugin-clipboard-manager", + "@tauri-apps/plugin-deep-link", + "@tauri-apps/plugin-dialog", + "@tauri-apps/plugin-fs", + "@tauri-apps/plugin-global-shortcut", + "@tauri-apps/plugin-http", + "@tauri-apps/plugin-log", + "@tauri-apps/plugin-nfc", + "@tauri-apps/plugin-notification", + "@tauri-apps/plugin-os", + "@tauri-apps/plugin-positioner", + "@tauri-apps/plugin-process", + "@tauri-apps/plugin-shell", + "@tauri-apps/plugin-sql", + "@tauri-apps/plugin-store", + "@tauri-apps/plugin-stronghold", + "@tauri-apps/plugin-updater", + "@tauri-apps/plugin-upload", + "@tauri-apps/plugin-websocket", + "@tauri-apps/plugin-window-state", + ] { + let version = pm + .current_package_version(pkg, frontend_dir) + .unwrap_or_default() + .unwrap_or_default(); + if version.starts_with('1') { + install_deps.push(format!("{pkg}@^2.0.0")); + } + } + + if !install_deps.is_empty() { + pm.install(&install_deps, frontend_dir)?; + } + + Ok(()) +} + +fn migrate_permissions(tauri_dir: &Path) -> Result<()> { + let core_plugins = [ + "app", + "event", + "image", + "menu", + "path", + "resources", + "tray", + "webview", + "window", + ]; + + for entry in walkdir::WalkDir::new(tauri_dir.join("capabilities")) { + let entry = entry.map_err(std::io::Error::other).fs_context( + "failed to walk capabilities directory", + tauri_dir.join("capabilities"), + )?; + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "json") { + let mut capability = + read_to_string(path).fs_context("failed to read capability", path.to_path_buf())?; + for plugin in core_plugins { + capability = capability.replace(&format!("\"{plugin}:"), &format!("\"core:{plugin}:")); + } + std::fs::write(path, capability) + .fs_context("failed to rewrite capability", path.to_path_buf())?; + } + } + Ok(()) +} + +fn migrate_manifest(manifest: &mut DocumentMut) -> Result<()> { + let version = "2.0.0"; + + let dependencies = manifest + .as_table_mut() + .entry("dependencies") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .context("manifest dependencies isn't a table")?; + + for dep in [ + "tauri", + "tauri-plugin-authenticator", + "tauri-plugin-autostart", + "tauri-plugin-barcode-scanner", + "tauri-plugin-biometric", + "tauri-plugin-cli", + "tauri-plugin-clipboard-manager", + "tauri-plugin-deep-link", + "tauri-plugin-dialog", + "tauri-plugin-fs", + "tauri-plugin-global-shortcut", + "tauri-plugin-http", + "tauri-plugin-localhost", + "tauri-plugin-log", + "tauri-plugin-nfc", + "tauri-plugin-notification", + "tauri-plugin-os", + "tauri-plugin-persisted-scope", + "tauri-plugin-positioner", + "tauri-plugin-process", + "tauri-plugin-shell", + "tauri-plugin-single-instance", + "tauri-plugin-sql", + "tauri-plugin-store", + "tauri-plugin-stronghold", + "tauri-plugin-updater", + "tauri-plugin-upload", + "tauri-plugin-websocket", + "tauri-plugin-window-state", + ] { + migrate_dependency(dependencies, dep, version); + } + + let build_dependencies = manifest + .as_table_mut() + .entry("build-dependencies") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .context("manifest build-dependencies isn't a table")?; + + migrate_dependency(build_dependencies, "tauri-build", version); + + Ok(()) +} + +fn migrate_dependency(dependencies: &mut Table, name: &str, version: &str) { + let item = dependencies.entry(name).or_insert(Item::None); + + // do not rewrite if dependency uses workspace inheritance + if item + .get("workspace") + .and_then(|v| v.as_bool()) + .unwrap_or_default() + { + log::info!( + "`{name}` dependency has workspace inheritance enabled. The features array won't be automatically rewritten." + ); + return; + } + + if let Some(dep) = item.as_table_mut() { + migrate_dependency_table(dep, version); + } else if let Some(Value::InlineTable(table)) = item.as_value_mut() { + migrate_dependency_table(table, version); + } else if item.as_str().is_some() { + *item = Item::Value(version.into()); + } +} + +fn migrate_dependency_table(dep: &mut D, version: &str) { + *dep.entry("version").or_insert(Item::None) = Item::Value(version.into()); +} diff --git a/crates/tauri-cli/src/migrate/mod.rs b/crates/tauri-cli/src/migrate/mod.rs new file mode 100644 index 0000000..609560d --- /dev/null +++ b/crates/tauri-cli/src/migrate/mod.rs @@ -0,0 +1,72 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + Result, + error::{Context, ErrorExt, bail}, + helpers::cargo_manifest::{CargoLock, CargoManifest, crate_version}, + interface::rust::get_workspace_dir, +}; + +use std::{fs::read_to_string, str::FromStr}; + +mod migrations; + +pub fn command() -> Result<()> { + let dirs = crate::helpers::app_paths::resolve_dirs(); + + let manifest_contents = read_to_string(dirs.tauri.join("Cargo.toml")).fs_context( + "failed to read Cargo manifest", + dirs.tauri.join("Cargo.toml"), + )?; + let manifest = toml::from_str::(&manifest_contents).with_context(|| { + format!( + "failed to parse Cargo manifest {}", + dirs.tauri.join("Cargo.toml").display() + ) + })?; + + let workspace_dir = get_workspace_dir(dirs.tauri)?; + let lock_path = workspace_dir.join("Cargo.lock"); + let lock = if lock_path.exists() { + let lockfile_contents = + read_to_string(&lock_path).fs_context("failed to read Cargo lockfile", &lock_path)?; + let lock = toml::from_str::(&lockfile_contents) + .with_context(|| format!("failed to parse Cargo lockfile {}", lock_path.display()))?; + Some(lock) + } else { + None + }; + + let tauri_version = crate_version(dirs.tauri, Some(&manifest), lock.as_ref(), "tauri") + .version + .context("failed to get tauri version")?; + let tauri_version = semver::Version::from_str(&tauri_version) + .with_context(|| format!("failed to parse tauri version {tauri_version}"))?; + + if tauri_version.major == 1 { + migrations::v1::run(&dirs).context("failed to migrate from v1")?; + } else if tauri_version.major == 2 { + if let Some((pre, _number)) = tauri_version.pre.as_str().split_once('.') { + match pre { + "beta" => { + migrations::v2_beta::run(&dirs).context("failed to migrate from v2 beta")?; + } + "alpha" => { + bail!( + "Migrating from v2 alpha ({tauri_version}) to v2 stable is not supported yet, \ + if your project started early, try downgrading to v1 and then try again" + ) + } + _ => { + bail!("Migrating from {tauri_version} to v2 stable is not supported yet") + } + } + } else { + log::info!("Nothing to do, the tauri version is already at v2 stable"); + } + } + + Ok(()) +} diff --git a/crates/tauri-cli/src/mobile/android/android_studio_script.rs b/crates/tauri-cli/src/mobile/android/android_studio_script.rs new file mode 100644 index 0000000..9465640 --- /dev/null +++ b/crates/tauri-cli/src/mobile/android/android_studio_script.rs @@ -0,0 +1,334 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{MobileTarget, detect_target_ok, ensure_init, env, get_app, get_config, read_options}; +use crate::{ + Error, Result, + error::{Context, ErrorExt}, + helpers::config::{get_config as get_tauri_config, reload_config as reload_tauri_config}, + interface::AppInterface, + mobile::CliOptions, +}; +use clap::{ArgAction, Parser}; + +use cargo_mobile2::{ + android::{adb, device::ConnectionStatus, target::Target}, + opts::Profile, + target::{TargetTrait, call_for_targets_with_fallback}, +}; + +use std::path::Path; + +#[derive(Debug, Parser)] +pub struct Options { + /// Targets to build. + #[clap( + short, + long = "target", + action = ArgAction::Append, + num_args(0..), + default_value = Target::DEFAULT_KEY, + value_parser(clap::builder::PossibleValuesParser::new(Target::name_list())) + )] + targets: Option>, + /// Builds with the release flag + #[clap(short, long)] + release: bool, +} + +pub fn command(options: Options) -> Result<()> { + let dirs = crate::helpers::app_paths::resolve_dirs(); + + let profile = if options.release { + Profile::Release + } else { + Profile::Debug + }; + + let mut tauri_config = get_tauri_config(tauri_utils::platform::Target::Android, &[], dirs.tauri)?; + let cli_options = read_options(&tauri_config); + + if !cli_options.config.is_empty() { + // reload config with merges from the android dev|build script + reload_tauri_config( + &mut tauri_config, + &cli_options + .config + .iter() + .map(|conf| &conf.0) + .collect::>(), + dirs.tauri, + )? + }; + + let (config, metadata) = get_config( + &get_app( + MobileTarget::Android, + &tauri_config, + &AppInterface::new(&tauri_config, None, dirs.tauri)?, + dirs.tauri, + ), + &tauri_config, + &[], + &cli_options, + ); + + ensure_init( + &tauri_config, + config.app(), + config.project_dir(), + MobileTarget::Android, + std::env::var("CI").is_ok(), + )?; + + if !cli_options.config.is_empty() { + crate::helpers::config::merge_config_with( + &mut tauri_config, + &cli_options + .config + .iter() + .map(|conf| &conf.0) + .collect::>(), + )?; + } + + let env = env(std::env::var("CI").is_ok())?; + + if cli_options.dev { + if let Some(url) = &tauri_config.build.dev_url { + let localhost = match url.host() { + Some(url::Host::Domain(d)) => d == "localhost", + Some(url::Host::Ipv4(i)) => i == std::net::Ipv4Addr::LOCALHOST, + _ => false, + }; + + if localhost { + if let Some(port) = url.port_or_known_default() { + adb_forward_port(port, &env, &cli_options)?; + } + } + } + } + + let mut validated_lib = false; + + let installed_targets = + crate::interface::rust::installation::installed_targets().unwrap_or_default(); + + call_for_targets_with_fallback( + options.targets.unwrap_or_default().iter(), + &detect_target_ok, + &env, + |target: &Target| { + if !installed_targets.contains(&target.triple().into()) { + log::info!("Installing target {}", target.triple()); + target + .install() + .map_err(|error| Error::CommandFailed { + command: "rustup target add".to_string(), + error, + }) + .context("failed to install target")?; + } + + target + .build( + &config, + &metadata, + &env, + cli_options.noise_level, + true, + profile, + ) + .context("failed to build Android app")?; + + if !validated_lib { + validated_lib = true; + + let lib_path = config + .app() + .target_dir(target.triple, profile) + .join(config.so_name()); + + validate_lib(&lib_path).context("failed to validate library")?; + } + + Ok(()) + }, + ) + .map_err(|e| Error::GenericError(e.to_string()))? +} + +fn validate_lib(path: &Path) -> Result<()> { + let so_bytes = std::fs::read(path).fs_context("failed to read library", path.to_path_buf())?; + let elf = elf::ElfBytes::::minimal_parse(&so_bytes) + .context("failed to parse ELF")?; + let (symbol_table, string_table) = elf + .dynamic_symbol_table() + .context("failed to read dynsym section")? + .context("missing dynsym tables")?; + + let mut symbols = Vec::new(); + for s in symbol_table.iter() { + if let Ok(symbol) = string_table.get(s.st_name as usize) { + symbols.push(symbol); + } + } + + if !symbols.contains(&"Java_app_tauri_plugin_PluginManager_handlePluginResponse") { + crate::error::bail!( + "Library from {} does not include required runtime symbols. This means you are likely missing the tauri::mobile_entry_point macro usage, see the documentation for more information: https://v2.tauri.app/start/migrate/from-tauri-1", + path.display() + ); + } + + Ok(()) +} + +fn adb_forward_port( + port: u16, + env: &cargo_mobile2::android::env::Env, + cli_options: &CliOptions, +) -> Result<()> { + let forward = format!("tcp:{port}"); + log::info!("Forwarding port {port} with adb"); + + 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!( + "ADB device list is empty, waiting a few seconds to see if there's any booting device..." + ); + + let max = 5; + let mut count = 0; + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + + devices = adb::device_list(env) + .unwrap_or_default() + .into_iter() + .filter(|d| d.status() == ConnectionStatus::Connected) + .collect::>(); + if !devices.is_empty() { + break; + } + + count += 1; + if count == max { + break; + } + } + } + + let target_device = if let Some(target_device) = &cli_options.target_device { + Some((target_device.id.clone(), target_device.name.clone())) + } else if devices.len() == 1 { + let device = devices.first().unwrap(); + Some((device.serial_no().to_string(), device.name().to_string())) + } else if devices.len() > 1 { + crate::error::bail!( + "Multiple Android devices are connected ({}), please disconnect devices you do not intend to use so Tauri can determine which to use", + devices + .iter() + .map(|d| d.name()) + .collect::>() + .join(", ") + ); + } else { + // when building the app without running to a device, we might have an empty devices list + None + }; + + if let Some((target_device_serial_no, target_device_name)) = target_device { + let mut already_forwarded = false; + + // clear port forwarding for all devices + for device in &devices { + let reverse_list_output = + adb_reverse_list(env, device.serial_no()).map_err(|error| Error::CommandFailed { + command: "adb reverse --list".to_string(), + error, + })?; + + // check if the device has the port forwarded + if String::from_utf8_lossy(&reverse_list_output.stdout).contains(&forward) { + // device matches our target, we can skip forwarding + if device.serial_no() == target_device_serial_no { + log::debug!( + "device {} already has the forward for {}", + device.name(), + forward + ); + already_forwarded = true; + } + break; + } + } + + // if there's a known target, we should forward the port to it + if already_forwarded { + log::info!("{forward} already forwarded to {target_device_name}"); + } else { + loop { + run_adb_reverse(env, &target_device_serial_no, &forward, &forward).map_err(|error| { + Error::CommandFailed { + command: format!("adb reverse {forward} {forward}"), + error, + } + })?; + + let reverse_list_output = + adb_reverse_list(env, &target_device_serial_no).map_err(|error| { + Error::CommandFailed { + command: "adb reverse --list".to_string(), + error, + } + })?; + // wait and retry until the port has actually been forwarded + if String::from_utf8_lossy(&reverse_list_output.stdout).contains(&forward) { + break; + } else { + log::warn!( + "waiting for the port to be forwarded to {}...", + target_device_name + ); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + } + } + } else { + log::warn!("no running devices detected with ADB; skipping port forwarding"); + } + + Ok(()) +} + +fn run_adb_reverse( + env: &cargo_mobile2::android::env::Env, + device_serial_no: &str, + remote: &str, + local: &str, +) -> std::io::Result { + adb::adb(env, ["-s", device_serial_no, "reverse", remote, local]) + .stdin_file(os_pipe::dup_stdin().unwrap()) + .stdout_file(os_pipe::dup_stdout().unwrap()) + .stderr_file(os_pipe::dup_stdout().unwrap()) + .run() +} + +fn adb_reverse_list( + env: &cargo_mobile2::android::env::Env, + device_serial_no: &str, +) -> std::io::Result { + adb::adb(env, ["-s", device_serial_no, "reverse", "--list"]) + .stdin_file(os_pipe::dup_stdin().unwrap()) + .stdout_capture() + .stderr_capture() + .run() +} diff --git a/crates/tauri-cli/src/mobile/android/build.rs b/crates/tauri-cli/src/mobile/android/build.rs new file mode 100644 index 0000000..60f56f1 --- /dev/null +++ b/crates/tauri-cli/src/mobile/android/build.rs @@ -0,0 +1,342 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{ + MobileTarget, OptionsHandle, configure_cargo, delete_codegen_vars, ensure_init, env, get_app, + get_config, inject_resources, log_finished, open_and_wait, sync_debug_application_id_suffix, +}; +use crate::{ + ConfigValue, Error, Result, + build::Options as BuildOptions, + error::Context, + helpers::{ + app_paths::Dirs, + config::{ConfigMetadata, get_config as get_tauri_config}, + flock, + }, + interface::{AppInterface, Options as InterfaceOptions}, + mobile::{CliOptions, TargetDevice, android::generate_tauri_properties, write_options}, +}; +use clap::{ArgAction, Parser}; + +use cargo_mobile2::{ + android::{aab, apk, config::Config as AndroidConfig, env::Env, target::Target}, + opts::{NoiseLevel, Profile}, + target::TargetTrait, +}; + +use std::env::set_current_dir; +use std::path::Path; + +#[derive(Debug, Clone, Parser)] +#[clap( + about = "Build your app in release mode for Android and generate APKs and AABs", + long_about = "Build your app in release mode for Android and generate APKs and AABs. It makes use of the `build.frontendDist` property from your `tauri.conf.json` file. It also runs your `build.beforeBuildCommand` which usually builds your frontend into `build.frontendDist`." +)] +pub struct Options { + /// Builds with the debug flag + #[clap(short, long)] + pub debug: bool, + /// Which targets to build (all by default). + #[clap( + short, + long = "target", + action = ArgAction::Append, + num_args(0..), + value_parser(clap::builder::PossibleValuesParser::new(Target::name_list())) + )] + pub targets: Option>, + /// List of cargo features to activate + #[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')] + pub features: Vec, + /// JSON strings or paths to JSON, JSON5 or TOML files to merge with the default configuration file + /// + /// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts. + /// + /// Note that a platform-specific file is looked up and merged with the default file by default + /// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json) + /// but you can use this for more specific use cases such as different build flavors. + #[clap(short, long)] + pub config: Vec, + /// Whether to split the APKs and AABs per ABIs. + #[clap(long)] + pub split_per_abi: bool, + /// Build APKs. + #[clap(long)] + pub apk: bool, + /// Build AABs. + #[clap(long)] + pub aab: bool, + #[clap(skip)] + pub skip_bundle: bool, + /// Open Android Studio + #[clap(short, long)] + pub open: bool, + /// Skip prompting for values + #[clap(long, env = "CI")] + pub ci: bool, + /// Command line arguments passed to the runner. + /// Use `--` to explicitly mark the start of the arguments. + /// e.g. `tauri android build -- [runnerArgs]`. + #[clap(last(true))] + pub args: Vec, + /// Do not error out if a version mismatch is detected on a Tauri package. + /// + /// 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, + /// Target device of this build + #[clap(skip)] + pub target_device: Option, +} + +impl From for BuildOptions { + fn from(options: Options) -> Self { + Self { + runner: None, + debug: options.debug, + target: None, + features: options.features, + bundles: None, + no_bundle: false, + config: options.config, + args: options.args, + ci: options.ci, + skip_stapling: false, + ignore_version_mismatches: options.ignore_version_mismatches, + no_sign: false, + no_binary_patching: false, + } + } +} + +pub struct BuiltApplication { + pub config: AndroidConfig, + pub interface: AppInterface, + // prevent drop + #[allow(dead_code)] + options_handle: OptionsHandle, +} + +pub fn command(options: Options, noise_level: NoiseLevel) -> Result { + let dirs = crate::helpers::app_paths::resolve_dirs(); + let tauri_config = get_tauri_config( + tauri_utils::platform::Target::Android, + &options + .config + .iter() + .map(|conf| &conf.0) + .collect::>(), + dirs.tauri, + )?; + run(options, noise_level, &dirs, &tauri_config) +} + +pub fn run( + options: Options, + noise_level: NoiseLevel, + dirs: &Dirs, + tauri_config: &ConfigMetadata, +) -> Result { + delete_codegen_vars(); + + let mut build_options: BuildOptions = options.clone().into(); + + let first_target = Target::all() + .get( + options + .targets + .as_ref() + .and_then(|l| l.first().map(|t| t.as_str())) + .unwrap_or(Target::DEFAULT_KEY), + ) + .unwrap(); + build_options.target = Some(first_target.triple.into()); + + let interface = AppInterface::new(tauri_config, build_options.target.clone(), dirs.tauri)?; + interface.build_options(&mut build_options.args, &mut build_options.features, true); + + let app = get_app(MobileTarget::Android, tauri_config, &interface, dirs.tauri); + let (config, metadata) = get_config( + &app, + tauri_config, + &build_options.features, + &CliOptions { + dev: false, + features: build_options.features.clone(), + args: build_options.args.clone(), + noise_level, + vars: Default::default(), + config: build_options.config.clone(), + target_device: None, + }, + ); + + let profile = if options.debug { + Profile::Debug + } else { + Profile::Release + }; + + set_current_dir(dirs.tauri).context("failed to set current directory to Tauri directory")?; + + ensure_init( + tauri_config, + config.app(), + config.project_dir(), + MobileTarget::Android, + options.ci, + )?; + + let mut env = env(options.ci)?; + 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)?; + + let installed_targets = + crate::interface::rust::installation::installed_targets().unwrap_or_default(); + + if !installed_targets.contains(&first_target.triple().into()) { + log::info!("Installing target {}", first_target.triple()); + first_target + .install() + .map_err(|error| Error::CommandFailed { + command: "rustup target add".to_string(), + error, + }) + .context("failed to install target")?; + } + // run an initial build to initialize plugins + first_target + .build(&config, &metadata, &env, noise_level, true, profile) + .context("failed to build Android app")?; + + let open = options.open; + let options_handle = run_build( + &interface, + options, + build_options, + tauri_config, + profile, + &config, + &mut env, + noise_level, + dirs.tauri, + )?; + + if open { + open_and_wait(&config, &env); + } + + Ok(BuiltApplication { + config, + interface, + options_handle, + }) +} + +#[allow(clippy::too_many_arguments)] +fn run_build( + interface: &AppInterface, + mut options: Options, + build_options: BuildOptions, + tauri_config: &ConfigMetadata, + profile: Profile, + config: &AndroidConfig, + env: &mut Env, + noise_level: NoiseLevel, + tauri_dir: &Path, +) -> Result { + if !(options.skip_bundle || options.apk || options.aab) { + // if the user didn't specify the format to build, we'll do both + options.apk = true; + options.aab = true; + } + + let interface_options = InterfaceOptions { + debug: build_options.debug, + target: build_options.target.clone(), + args: build_options.args.clone(), + ..Default::default() + }; + + let app_settings = interface.app_settings(); + let out_dir = app_settings.out_dir(&interface_options, tauri_dir)?; + let _lock = flock::open_rw(out_dir.join("lock").with_extension("android"), "Android")?; + + let cli_options = CliOptions { + dev: false, + features: build_options.features.clone(), + args: build_options.args.clone(), + noise_level, + vars: Default::default(), + config: build_options.config, + target_device: options.target_device.clone(), + }; + let handle = write_options(tauri_config, cli_options)?; + + inject_resources(config, tauri_config)?; + + let apk_outputs = if options.apk { + apk::build( + config, + env, + noise_level, + profile, + get_targets_or_all(options.targets.clone().unwrap_or_default())?, + options.split_per_abi, + ) + .context("failed to build APK")? + } else { + Vec::new() + }; + + let aab_outputs = if options.aab { + aab::build( + config, + env, + noise_level, + profile, + get_targets_or_all(options.targets.unwrap_or_default())?, + options.split_per_abi, + ) + .context("failed to build AAB")? + } else { + Vec::new() + }; + + if !apk_outputs.is_empty() { + log_finished(apk_outputs, "APK"); + } + if !aab_outputs.is_empty() { + log_finished(aab_outputs, "AAB"); + } + + Ok(handle) +} + +fn get_targets_or_all<'a>(targets: Vec) -> Result>> { + if targets.is_empty() { + Ok(Target::all().iter().map(|t| t.1).collect()) + } else { + let mut outs = Vec::new(); + + let possible_targets = Target::all() + .keys() + .map(|key| key.to_string()) + .collect::>() + .join(","); + + for t in targets { + let target = Target::for_name(&t).with_context(|| { + format!("Target {t} is invalid; the possible targets are {possible_targets}",) + })?; + outs.push(target); + } + Ok(outs) + } +} diff --git a/crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs b/crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs new file mode 100644 index 0000000..b1c222c --- /dev/null +++ b/crates/tauri-cli/src/mobile/android/check_java_gradle_versions.rs @@ -0,0 +1,156 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Java detection and Java/Gradle compatibility checks for Android builds. + +use std::path::{Path, PathBuf}; + +pub fn check_java_gradle_versions() -> Option<()> { + let java_major_version = java_major_version()?; + let project_gradle_version = project_gradle_version()?; + let gradle_max_supported_java = gradle_max_supported_java(&project_gradle_version)?; + // Read the Gradle version this project actually uses (the user may have upgraded it away from + // the template default) and only warn when the detected Java is too new for that Gradle. + if java_major_version > gradle_max_supported_java { + log::warn!( + "Detected Java {java_major_version}, but Gradle {project_gradle_version} used by this project can only run on Java up to {gradle_max_supported_java}. \ + Android builds will likely fail with a cryptic error. Install a JDK that Gradle {project_gradle_version} supports \ + (Java {gradle_max_supported_java} or older) and point JAVA_HOME at it. \ + See https://docs.gradle.org/current/userguide/compatibility.html" + ); + } + Some(()) +} + +/// Reads the Gradle version this project is pinned to from its +/// `gradle/wrapper/gradle-wrapper.properties` `distributionUrl` (e.g. +/// `gradle-8.14-bin.zip` -> `8.14`). Returns `None` when the project isn't +/// generated yet or the file can't be parsed, in which case no warning is shown. +fn project_gradle_version() -> Option { + let project_path = std::env::var_os("TAURI_ANDROID_PROJECT_PATH")?; + let properties = std::fs::read_to_string( + Path::new(&project_path).join("gradle/wrapper/gradle-wrapper.properties"), + ) + .ok()?; + properties.lines().find_map(|line| { + let url = line.trim().strip_prefix("distributionUrl=")?; + let file = url.rsplit('/').next()?; + let version = file.strip_prefix("gradle-")?.split('-').next()?; + (!version.is_empty()).then(|| version.to_string()) + }) +} + +/// Highest Java feature version a given Gradle release can *run* on. +/// Known Gradle 7.x through 9.x releases are mapped. Unknown future major +/// versions return `None` to avoid false warnings. See +/// . +fn gradle_max_supported_java(version: &str) -> Option { + let mut parts = version.split('.'); + let major: u32 = parts.next()?.parse().ok()?; + let minor: u32 = parts.next().and_then(|m| m.parse().ok()).unwrap_or(0); + + if major >= 10 { + return None; + } + + Some(match (major, minor) { + v if v >= (9, 4) => 26, + v if v >= (9, 1) => 25, + v if v >= (8, 14) => 24, + v if v >= (8, 10) => 23, + v if v >= (8, 8) => 22, + v if v >= (8, 5) => 21, + v if v >= (8, 3) => 20, + v if v >= (7, 6) => 19, + v if v >= (7, 5) => 18, + v if v >= (7, 3) => 17, + _ => return None, + }) +} + +/// Detects the major (feature) version of the active Java installation by +/// reading the `release` metadata file shipped inside every JDK, preferring +/// `JAVA_HOME` and falling back to the `java` binary on `PATH`. This avoids +/// spawning a `java -version` subprocess on every Android command. +fn java_major_version() -> Option { + let release = std::fs::read_to_string(java_home_dir()?.join("release")).ok()?; + release + .lines() + .find_map(|line| line.strip_prefix("JAVA_VERSION=")) + .and_then(parse_java_major) +} + +/// Resolves the active JDK home directory, preferring `JAVA_HOME` and otherwise +/// deriving it from the `java` binary on `PATH` (`/bin/java` -> ``). +fn java_home_dir() -> Option { + if let Some(home) = std::env::var_os("JAVA_HOME") { + return Some(PathBuf::from(home)); + } + let java = which::which("java").ok()?; + java.parent()?.parent().map(Path::to_path_buf) +} + +/// Parses the major (feature) version out of a quoted Java version string (the +/// `JAVA_VERSION="..."` line of a JDK `release` file, or a `java -version` +/// banner), handling both the legacy `1.x` scheme (`1.8.0_292` -> 8) and the +/// modern scheme (`21.0.1` -> 21). +fn parse_java_major(version_output: &str) -> Option { + let start = version_output.find('"')?; + let rest = &version_output[start + 1..]; + let end = rest.find('"')?; + let mut parts = rest[..end].split(['.', '_', '-']); + + let first = parts.next()?; + if first == "1" { + parts.next()?.parse().ok() + } else { + first.parse().ok() + } +} + +#[cfg(test)] +mod tests { + use super::{gradle_max_supported_java, parse_java_major}; + + #[test] + fn parses_java_major_version() { + // modern scheme + assert_eq!( + parse_java_major("openjdk version \"21.0.1\" 2023-10-17"), + Some(21) + ); + assert_eq!( + parse_java_major("openjdk version \"26\" 2026-03-17"), + Some(26) + ); + assert_eq!( + parse_java_major("java version \"17.0.9\" 2023-10-17 LTS"), + Some(17) + ); + // legacy 1.x scheme + assert_eq!(parse_java_major("java version \"1.8.0_292\""), Some(8)); + // JDK `release` file line (the path java_major_version actually feeds in) + assert_eq!(parse_java_major("\"21.0.1\""), Some(21)); + // garbage + assert_eq!(parse_java_major("no version here"), None); + } + + #[test] + fn maps_gradle_to_max_java() { + assert_eq!(gradle_max_supported_java("9.6.1"), Some(26)); + assert_eq!(gradle_max_supported_java("9.4.0"), Some(26)); + assert_eq!(gradle_max_supported_java("9.3"), Some(25)); + assert_eq!(gradle_max_supported_java("9.1.0"), Some(25)); + assert_eq!(gradle_max_supported_java("9.0"), Some(24)); + assert_eq!(gradle_max_supported_java("8.14"), Some(24)); + assert_eq!(gradle_max_supported_java("8.5"), Some(21)); + assert_eq!(gradle_max_supported_java("7.3"), Some(17)); + // pre-7.3 is unmapped + assert_eq!(gradle_max_supported_java("7.2"), None); + // Unknown future Gradle majors are left unmapped to avoid false warnings. + assert_eq!(gradle_max_supported_java("10.0"), None); + // unparsable + assert_eq!(gradle_max_supported_java("not-a-version"), None); + } +} diff --git a/crates/tauri-cli/src/mobile/android/dev.rs b/crates/tauri-cli/src/mobile/android/dev.rs new file mode 100644 index 0000000..404641f --- /dev/null +++ b/crates/tauri-cli/src/mobile/android/dev.rs @@ -0,0 +1,413 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{ + MobileTarget, configure_cargo, delete_codegen_vars, device_prompt, ensure_init, env, get_app, + get_config, inject_resources, open_and_wait, sync_debug_application_id_suffix, +}; +use crate::{ + ConfigValue, Error, Result, + dev::Options as DevOptions, + error::{Context, ErrorExt}, + helpers::{ + app_paths::Dirs, + config::{ConfigMetadata, get_config as get_tauri_config}, + flock, + }, + interface::{AppInterface, MobileOptions, Options as InterfaceOptions}, + mobile::{ + CliOptions, DevChild, DevHost, DevProcess, TargetDevice, android::generate_tauri_properties, + use_network_address_for_dev_url, write_options, + }, +}; +use clap::{ArgAction, Parser}; + +use cargo_mobile2::{ + android::{ + config::{Config as AndroidConfig, Metadata as AndroidMetadata}, + device::Device, + env::Env, + target::Target, + }, + opts::{FilterLevel, NoiseLevel, Profile}, + target::TargetTrait, +}; +use url::Host; + +use std::{env::set_current_dir, net::Ipv4Addr, path::PathBuf}; + +#[derive(Debug, Clone, Parser)] +#[clap( + about = "Run your app in development mode on Android", + long_about = "Run your app in development mode on Android with hot-reloading for the Rust code. It makes use of the `build.devUrl` property from your `tauri.conf.json` file. It also runs your `build.beforeDevCommand` which usually starts your frontend devServer." +)] +pub struct Options { + /// List of cargo features to activate + #[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')] + pub features: Vec, + /// Exit on panic + #[clap(short, long)] + exit_on_panic: bool, + /// JSON strings or paths to JSON, JSON5 or TOML files to merge with the default configuration file + /// + /// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts. + /// + /// Note that a platform-specific file is looked up and merged with the default file by default + /// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json) + /// but you can use this for more specific use cases such as different build flavors. + #[clap(short, long)] + pub config: Vec, + /// Run the code in release mode + #[clap(long = "release")] + pub release_mode: bool, + /// Skip waiting for the frontend dev server to start before building the tauri application. + #[clap(long, env = "TAURI_CLI_NO_DEV_SERVER_WAIT")] + pub no_dev_server_wait: bool, + /// Disable the file watcher + #[clap(long)] + pub no_watch: bool, + /// Additional paths to watch for changes. + #[clap(long)] + pub additional_watch_folders: Vec, + /// Open Android Studio instead of trying to run on a connected device + #[clap(short, long)] + pub open: bool, + /// Runs on the given device name + pub device: Option, + /// Force prompting for an IP to use to connect to the dev server on mobile. + #[clap(long)] + pub force_ip_prompt: bool, + /// Use the public network address for the development server. + /// If an actual address it provided, it is used instead of prompting to pick one. + /// + /// On Windows we use the public network address by default. + /// + /// This option is particularly useful along the `--open` flag when you intend on running on a physical device. + /// + /// This replaces the devUrl configuration value to match the public network address host, + /// it is your responsibility to set up your development server to listen on this address + /// by using 0.0.0.0 as host for instance. + /// + /// When this is set or when running on an iOS device the CLI sets the `TAURI_DEV_HOST` + /// environment variable so you can check this on your framework's configuration to expose the development server + /// on the public network address. + #[clap(long, default_value_t, default_missing_value(""), num_args(0..=1))] + pub host: DevHost, + /// Disable the built-in dev server for static files. + #[clap(long)] + pub no_dev_server: bool, + /// Specify port for the built-in dev server for static files. Defaults to 1430. + #[clap(long, env = "TAURI_CLI_PORT")] + pub port: Option, + /// Command line arguments passed to the runner. + /// Use `--` to explicitly mark the start of the arguments. + /// e.g. `tauri android dev -- [runnerArgs]`. + #[clap(last(true))] + pub args: Vec, + /// Path to the certificate file used by your dev server. Required for mobile dev when using HTTPS. + #[clap(long, env = "TAURI_DEV_ROOT_CERTIFICATE_PATH")] + pub root_certificate_path: Option, +} + +impl From for DevOptions { + fn from(options: Options) -> Self { + Self { + runner: None, + target: None, + features: options.features, + exit_on_panic: options.exit_on_panic, + config: options.config, + args: options.args, + no_watch: options.no_watch, + additional_watch_folders: options.additional_watch_folders, + no_dev_server_wait: options.no_dev_server_wait, + no_dev_server: options.no_dev_server, + port: options.port, + release_mode: options.release_mode, + host: options.host.0.unwrap_or_default(), + } + } +} + +pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { + let dirs = crate::helpers::app_paths::resolve_dirs(); + + let result = run_command(options, noise_level, dirs); + if result.is_err() { + crate::dev::kill_before_dev_process(); + } + result +} + +fn run_command(options: Options, noise_level: NoiseLevel, dirs: Dirs) -> Result<()> { + delete_codegen_vars(); + // setup env additions before calling env() + if let Some(root_certificate_path) = &options.root_certificate_path { + unsafe { + std::env::set_var( + "TAURI_DEV_ROOT_CERTIFICATE", + std::fs::read_to_string(root_certificate_path).fs_context( + "failed to read certificate file", + root_certificate_path.clone(), + )?, + ) + }; + } + + let tauri_config = get_tauri_config( + tauri_utils::platform::Target::Android, + &options + .config + .iter() + .map(|conf| &conf.0) + .collect::>(), + dirs.tauri, + )?; + + let env = env(false)?; + let device = if options.open { + None + } else { + match device_prompt(&env, options.device.as_deref()) { + Ok(d) => Some(d), + Err(e) => { + log::error!("{e}"); + None + } + } + }; + + let mut dev_options: DevOptions = options.clone().into(); + let target_triple = device + .as_ref() + .map(|d| d.target().triple.to_string()) + .unwrap_or_else(|| Target::all().values().next().unwrap().triple.into()); + dev_options.target = Some(target_triple); + dev_options.args.push("--lib".into()); + + let interface = AppInterface::new(&tauri_config, dev_options.target.clone(), dirs.tauri)?; + + let app = get_app(MobileTarget::Android, &tauri_config, &interface, dirs.tauri); + let (config, metadata) = get_config( + &app, + &tauri_config, + dev_options.features.as_ref(), + &CliOptions { + dev: true, + features: dev_options.features.clone(), + args: dev_options.args.clone(), + noise_level, + vars: Default::default(), + config: dev_options.config.clone(), + target_device: None, + }, + ); + + set_current_dir(dirs.tauri).context("failed to set current directory to Tauri directory")?; + + ensure_init( + &tauri_config, + config.app(), + config.project_dir(), + MobileTarget::Android, + false, + )?; + run_dev( + interface, + options, + dev_options, + tauri_config, + device, + env, + &config, + &metadata, + noise_level, + &dirs, + ) +} + +#[allow(clippy::too_many_arguments)] +fn run_dev( + mut interface: AppInterface, + options: Options, + mut dev_options: DevOptions, + mut tauri_config: ConfigMetadata, + device: Option, + mut env: Env, + config: &AndroidConfig, + metadata: &AndroidMetadata, + noise_level: NoiseLevel, + dirs: &Dirs, +) -> Result<()> { + // when --host is provided or running on a physical device or resolving 0.0.0.0 we must use the network IP + if options.host.0.is_some() + || device + .as_ref() + .map(|device| !device.serial_no().starts_with("emulator")) + .unwrap_or(false) + || tauri_config.build.dev_url.as_ref().is_some_and(|url| { + matches!( + url.host(), + Some(Host::Ipv4(i)) if i == Ipv4Addr::UNSPECIFIED + ) + }) + { + use_network_address_for_dev_url( + &mut tauri_config, + &mut dev_options, + options.force_ip_prompt, + dirs.tauri, + )?; + } + + crate::dev::setup(&interface, &mut dev_options, &mut tauri_config, dirs)?; + + let interface_options = InterfaceOptions { + debug: !dev_options.release_mode, + target: dev_options.target.clone(), + ..Default::default() + }; + + let app_settings = interface.app_settings(); + let out_dir = app_settings.out_dir(&interface_options, dirs.tauri)?; + let _lock = flock::open_rw(out_dir.join("lock").with_extension("android"), "Android")?; + + 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(); + + // run an initial build to initialize plugins + let target_triple = dev_options.target.as_ref().unwrap(); + let target = Target::all() + .values() + .find(|t| t.triple == target_triple) + .unwrap_or_else(|| Target::all().values().next().unwrap()); + if !installed_targets.contains(&target.triple().into()) { + log::info!("Installing target {}", target.triple()); + target.install().map_err(|error| Error::CommandFailed { + command: "rustup target add".to_string(), + error, + })?; + } + + target + .build( + config, + metadata, + &env, + noise_level, + true, + if options.release_mode { + Profile::Release + } else { + Profile::Debug + }, + ) + .context("failed to build Android app")?; + + let open = options.open; + interface.mobile_dev( + &mut tauri_config, + MobileOptions { + debug: !options.release_mode, + features: options.features, + args: options.args, + config: dev_options.config.clone(), + no_watch: options.no_watch, + additional_watch_folders: options.additional_watch_folders, + }, + |options, tauri_config| { + let cli_options = CliOptions { + dev: true, + features: options.features.clone(), + args: options.args.clone(), + noise_level, + vars: Default::default(), + config: dev_options.config.clone(), + target_device: device.as_ref().map(|d| TargetDevice { + id: d.serial_no().to_string(), + name: d.name().to_string(), + }), + }; + + let _handle = write_options(tauri_config, cli_options)?; + + inject_resources(config, tauri_config)?; + + if open { + open_and_wait(config, &env) + } else if let Some(device) = &device { + 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(); + Err(e) + } + } + } else { + open_and_wait(config, &env) + } + }, + dirs, + ) +} + +fn run( + device: &Device<'_>, + options: MobileOptions, + config: &AndroidConfig, + env: &Env, + metadata: &AndroidMetadata, + noise_level: NoiseLevel, + tauri_config: &tauri_utils::config::Config, +) -> crate::Result { + let profile = if options.debug { + Profile::Debug + } else { + Profile::Release + }; + + 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_with_application_id_suffix( + config, + env, + noise_level, + profile, + Some(match noise_level { + NoiseLevel::Polite => FilterLevel::Info, + NoiseLevel::LoudAndProud => FilterLevel::Debug, + NoiseLevel::FranklyQuitePedantic => FilterLevel::Verbose, + }), + build_app_bundle, + false, + 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 new file mode 100644 index 0000000..87d21ed --- /dev/null +++ b/crates/tauri-cli/src/mobile/android/mod.rs @@ -0,0 +1,1233 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use cargo_mobile2::{ + android::{ + adb, + config::{Config as AndroidConfig, Metadata as AndroidMetadata, Raw as RawAndroidConfig}, + device::{ConnectionStatus, Device}, + emulator, + env::Env, + target::Target, + }, + config::app::{App, DEFAULT_ASSET_DIR}, + opts::{FilterLevel, NoiseLevel}, + os, + target::TargetTrait, + util::prompt, +}; +use clap::{Parser, Subcommand}; +use semver::Version; +use std::{ + env::set_var, + fs::{create_dir, create_dir_all, read_dir, write}, + io::Cursor, + path::{Path, PathBuf}, + process::{Command, exit}, + sync::OnceLock, + thread::sleep, + time::Duration, +}; +use sublime_fuzzy::best_match; +use tauri_utils::resources::ResourcePaths; + +use super::{ + CliOptions, MIN_DEVICE_MATCH_SCORE, OptionsHandle, Target as MobileTarget, ensure_init, get_app, + init::command as init_command, log_finished, read_options, +}; +use crate::{ + ConfigValue, Error, ErrorExt, Result, + error::Context, + helpers::config::{BundleResources, Config as TauriConfig}, + mobile::android::check_java_gradle_versions::check_java_gradle_versions, +}; + +mod android_studio_script; +mod build; +mod check_java_gradle_versions; +mod dev; +pub(crate) mod project; +mod run; + +const NDK_VERSION: &str = "29.0.13846066"; +const SDK_VERSION: u8 = 37; + +#[cfg(target_os = "macos")] +const CMDLINE_TOOLS_URL: &str = + "https://dl.google.com/android/repository/commandlinetools-mac-13114758_latest.zip"; +#[cfg(target_os = "linux")] +const CMDLINE_TOOLS_URL: &str = + "https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip"; +#[cfg(windows)] +const CMDLINE_TOOLS_URL: &str = + "https://dl.google.com/android/repository/commandlinetools-win-13114758_latest.zip"; + +#[derive(Parser)] +#[clap( + author, + version, + about = "Android commands", + subcommand_required(true), + arg_required_else_help(true) +)] +pub struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Debug, Parser)] +#[clap(about = "Initialize Android target in the project")] +pub struct InitOptions { + /// Skip prompting for values + #[clap(long, env = "CI")] + ci: bool, + /// Skips installing rust toolchains via rustup + #[clap(long)] + skip_targets_install: bool, + /// JSON strings or paths to JSON, JSON5 or TOML files to merge with the default configuration file + /// + /// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts. + /// + /// Note that a platform-specific file is looked up and merged with the default file by default + /// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json) + /// but you can use this for more specific use cases such as different build flavors. + #[clap(short, long)] + pub config: Vec, +} + +#[derive(Subcommand)] +enum Commands { + Init(InitOptions), + Dev(dev::Options), + Build(build::Options), + Run(run::Options), + #[clap(hide(true))] + AndroidStudioScript(android_studio_script::Options), +} + +pub fn command(cli: Cli, verbosity: u8) -> Result<()> { + let noise_level = NoiseLevel::from_occurrences(verbosity as u64); + match cli.command { + Commands::Init(options) => init_command( + MobileTarget::Android, + options.ci, + false, + options.skip_targets_install, + options.config, + )?, + Commands::Dev(options) => dev::command(options, noise_level)?, + Commands::Build(options) => build::command(options, noise_level).map(|_| ())?, + Commands::Run(options) => run::command(options, noise_level)?, + Commands::AndroidStudioScript(options) => android_studio_script::command(options)?, + } + + Ok(()) +} + +pub fn get_config( + app: &App, + config: &TauriConfig, + features: &[String], + cli_options: &CliOptions, +) -> (AndroidConfig, AndroidMetadata) { + let mut android_options = cli_options.clone(); + android_options.features.extend_from_slice(features); + + let raw = RawAndroidConfig { + features: Some(android_options.features.clone()), + logcat_filter_specs: vec![ + "RustStdoutStderr".into(), + format!( + "*:{}", + match cli_options.noise_level { + NoiseLevel::Polite => FilterLevel::Info, + NoiseLevel::LoudAndProud => FilterLevel::Debug, + NoiseLevel::FranklyQuitePedantic => FilterLevel::Verbose, + } + .logcat() + ), + ], + min_sdk_version: Some(config.bundle.android.min_sdk_version), + ..Default::default() + }; + let config = AndroidConfig::from_raw(app.clone(), Some(raw)).unwrap(); + + let metadata = AndroidMetadata { + supported: true, + cargo_args: Some(android_options.args), + features: Some(android_options.features), + ..Default::default() + }; + + unsafe { + set_var( + "WRY_ANDROID_PACKAGE", + app.android_identifier_escape_kotlin_keyword(), + ) + }; + unsafe { set_var("TAURI_ANDROID_PACKAGE_UNESCAPED", app.identifier()) }; + unsafe { set_var("WRY_ANDROID_LIBRARY", app.lib_name()) }; + unsafe { set_var("TAURI_ANDROID_PROJECT_PATH", config.project_dir()) }; + + let src_main_dir = config + .project_dir() + .join("app/src/main") + .join(format!("java/{}", app.identifier().replace('.', "/"),)); + if config.project_dir().exists() { + if src_main_dir.exists() { + let _ = create_dir(src_main_dir.join("generated")); + } else { + log::error!( + "Project directory {} does not exist. Did you update the package name in `Cargo.toml` or the bundle identifier in `tauri.conf.json > identifier`? Save your changes, delete the `gen/android` folder and run `tauri android init` to recreate the Android project.", + src_main_dir.display() + ); + exit(1); + } + } + unsafe { + set_var( + "WRY_ANDROID_KOTLIN_FILES_OUT_DIR", + src_main_dir.join("generated"), + ) + }; + + check_java_gradle_versions(); + + (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")?; + cargo_mobile2::android::env::Env::from_env(env).context("failed to load Android environment") +} + +fn download_cmdline_tools(extract_path: &Path) -> Result<()> { + log::info!("Downloading Android command line tools..."); + + let mut response = crate::helpers::http::get(CMDLINE_TOOLS_URL) + .context("failed to download Android command line tools")?; + let body = response + .body_mut() + .with_config() + .limit(200 * 1024 * 1024 /* 200MB */) + .read_to_vec() + .context("failed to read Android command line tools download response")?; + + let mut zip = zip::ZipArchive::new(Cursor::new(body)) + .context("failed to create zip archive from Android command line tools download response")?; + + log::info!( + "Extracting Android command line tools to {}", + extract_path.display() + ); + zip + .extract(extract_path) + .context("failed to extract Android command line tools")?; + + Ok(()) +} + +fn ensure_env(non_interactive: bool) -> Result<()> { + ensure_java()?; + ensure_sdk(non_interactive)?; + ensure_ndk(non_interactive)?; + Ok(()) +} + +fn ensure_java() -> Result<()> { + if std::env::var_os("JAVA_HOME").is_none() { + #[cfg(windows)] + let default_java_home = "C:\\Program Files\\Android\\Android Studio\\jbr"; + #[cfg(target_os = "macos")] + let default_java_home = "/Applications/Android Studio.app/Contents/jbr/Contents/Home"; + #[cfg(target_os = "linux")] + let default_java_home = "/opt/android-studio/jbr"; + + if Path::new(default_java_home).exists() { + log::info!("Using Android Studio's default Java installation: {default_java_home}"); + unsafe { std::env::set_var("JAVA_HOME", default_java_home) }; + } else if which::which("java").is_err() { + crate::error::bail!( + "Java not found in PATH, default Android Studio Java installation not found at {default_java_home} and JAVA_HOME environment variable not set. Please install Java before proceeding" + ); + } + } + + Ok(()) +} + +fn ensure_sdk(non_interactive: bool) -> Result<()> { + let android_home = std::env::var_os("ANDROID_HOME") + .or_else(|| std::env::var_os("ANDROID_SDK_ROOT")) + .map(PathBuf::from); + if !android_home.as_ref().is_some_and(|v| v.exists()) { + log::info!( + "ANDROID_HOME {}, trying to locate Android SDK...", + if let Some(v) = &android_home { + format!("not found at {}", v.display()) + } else { + "not set".into() + } + ); + + #[cfg(target_os = "macos")] + let default_android_home = dirs::home_dir().unwrap().join("Library/Android/sdk"); + #[cfg(target_os = "linux")] + let default_android_home = dirs::home_dir().unwrap().join("Android/Sdk"); + #[cfg(windows)] + let default_android_home = dirs::data_local_dir().unwrap().join("Android/Sdk"); + + if default_android_home.exists() { + log::info!( + "Using installed Android SDK: {}", + default_android_home.display() + ); + } else if non_interactive { + crate::error::bail!( + "Android SDK not found. Make sure the SDK and NDK are installed and the ANDROID_HOME and NDK_HOME environment variables are set." + ); + } else { + log::error!( + "Android SDK not found at {}", + default_android_home.display() + ); + + let extract_path = if create_dir_all(&default_android_home).is_ok() { + default_android_home.clone() + } else { + std::env::current_dir().context("failed to get current directory")? + }; + + let sdk_manager_path = extract_path + .join("cmdline-tools/bin/sdkmanager") + .with_extension(if cfg!(windows) { "bat" } else { "" }); + + let mut granted_permission_to_install = false; + + if !sdk_manager_path.exists() { + granted_permission_to_install = crate::helpers::prompts::confirm( + "Do you want to install the Android Studio command line tools to setup the Android SDK?", + Some(false), + ) + .unwrap_or_default(); + + if !granted_permission_to_install { + crate::error::bail!( + "Skipping Android Studio command line tools installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android" + ); + } + + download_cmdline_tools(&extract_path)?; + } + + if !granted_permission_to_install { + granted_permission_to_install = crate::helpers::prompts::confirm( + "Do you want to install the Android SDK using the command line tools?", + Some(false), + ) + .unwrap_or_default(); + + if !granted_permission_to_install { + crate::error::bail!( + "Skipping Android Studio SDK installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android" + ); + } + } + + log::info!( + "Running sdkmanager to install platform-tools, android-{SDK_VERSION} and ndk-{NDK_VERSION} on {}...", + default_android_home.display() + ); + let status = Command::new(&sdk_manager_path) + .arg(format!("--sdk_root={}", default_android_home.display())) + .arg("--install") + .arg("platform-tools") + .arg(format!("platforms;android-{SDK_VERSION}")) + .arg(format!("ndk;{NDK_VERSION}")) + .status() + .map_err(|error| Error::CommandFailed { + command: format!("{} --sdk_root={} --install platform-tools platforms;android-{SDK_VERSION} ndk;{NDK_VERSION}", sdk_manager_path.display(), default_android_home.display()), + error, + })?; + + if !status.success() { + crate::error::bail!("Failed to install Android SDK"); + } + } + + unsafe { std::env::set_var("ANDROID_HOME", default_android_home) }; + } + + Ok(()) +} + +fn ensure_ndk(non_interactive: bool) -> Result<()> { + // re-evaluate ANDROID_HOME + let android_home = std::env::var_os("ANDROID_HOME") + .or_else(|| std::env::var_os("ANDROID_SDK_ROOT")) + .map(PathBuf::from) + .context("Failed to locate Android SDK")?; + + // check NDK_HOME + let mut installed_ndks = if let Some(ndk_home) = std::env::var_os("NDK_HOME") { + let ndk_path = PathBuf::from(ndk_home); + if ndk_path.is_dir() { + vec![ndk_path] + } else { + crate::error::bail!( + "Android NDK invalid. Make sure the NDK_HOME environment variable has correct value." + ); + } + } else { + read_dir(android_home.join("ndk")) + .map(|dir| { + dir + .into_iter() + .flat_map(|e| e.ok().map(|e| e.path())) + .collect::>() + }) + .unwrap_or_default() + }; + installed_ndks.sort(); + + if let Some(ndk) = installed_ndks.last() { + log::info!("Using installed NDK: {}", ndk.display()); + unsafe { std::env::set_var("NDK_HOME", ndk) }; + } else if non_interactive { + crate::error::bail!( + "Android NDK not found. Make sure the NDK is installed and the NDK_HOME environment variable is set." + ); + } else { + let sdk_manager_path = android_home + .join("cmdline-tools/bin/sdkmanager") + .with_extension(if cfg!(windows) { "bat" } else { "" }); + + let mut granted_permission_to_install = false; + + if !sdk_manager_path.exists() { + granted_permission_to_install = crate::helpers::prompts::confirm( + "Do you want to install the Android Studio command line tools to setup the Android NDK?", + Some(false), + ) + .unwrap_or_default(); + + if !granted_permission_to_install { + crate::error::bail!( + "Skipping Android Studio command line tools installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android" + ); + } + + download_cmdline_tools(&android_home)?; + } + + if !granted_permission_to_install { + granted_permission_to_install = crate::helpers::prompts::confirm( + "Do you want to install the Android NDK using the command line tools?", + Some(false), + ) + .unwrap_or_default(); + + if !granted_permission_to_install { + crate::error::bail!( + "Skipping Android Studio NDK installation. Please go through the manual setup process described in the documentation: https://tauri.app/start/prerequisites/#android" + ); + } + } + + log::info!( + "Running sdkmanager to install ndk-{NDK_VERSION} on {}...", + android_home.display() + ); + let status = Command::new(&sdk_manager_path) + .arg(format!("--sdk_root={}", android_home.display())) + .arg("--install") + .arg(format!("ndk;{NDK_VERSION}")) + .status() + .map_err(|error| Error::CommandFailed { + command: format!( + "{} --sdk_root={} --install ndk;{NDK_VERSION}", + sdk_manager_path.display(), + android_home.display() + ), + error, + })?; + + if !status.success() { + crate::error::bail!("Failed to install Android NDK"); + } + + let ndk_path = android_home.join("ndk").join(NDK_VERSION); + log::info!("Installed NDK: {}", ndk_path.display()); + unsafe { std::env::set_var("NDK_HOME", ndk_path) }; + } + + Ok(()) +} + +fn delete_codegen_vars() { + for (k, _) in std::env::vars() { + if k.starts_with("WRY_") && (k.ends_with("CLASS_EXTENSION") || k.ends_with("CLASS_INIT")) { + unsafe { std::env::remove_var(k) }; + } + } +} + +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")? + .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 + .into_iter() + .rev() + .map(|d| { + let score = best_match(t, d.name()).map_or(0, |m| m.score()); + (d, score) + }) + .max_by_key(|(_, score)| *score) + // we already checked the list is not empty + .unwrap(); + if score > MIN_DEVICE_MATCH_SCORE { + device + } else { + crate::error::bail!("Could not find an Android device matching {t}") + } + } else if device_list.len() > 1 { + let index = prompt::list( + concat!("Detected ", "Android", " devices"), + device_list.iter(), + "device", + None, + "Device", + ) + .context("failed to prompt for device")?; + device_list.into_iter().nth(index).unwrap() + } else { + device_list.into_iter().next().unwrap() + }; + + log::info!( + "Detected connected device: {} with target {:?}", + device, + device.target().triple, + ); + Ok(device) + } else { + Err(Error::GenericError( + "No connected Android devices detected".to_string(), + )) + } +} + +fn emulator_prompt(env: &'_ Env, target: Option<&str>) -> Result { + let emulator_list = emulator::avd_list(env).unwrap_or_default(); + if !emulator_list.is_empty() { + let emulator = if let Some(t) = target { + let (device, score) = emulator_list + .into_iter() + .rev() + .map(|d| { + let score = best_match(t, d.name()).map_or(0, |m| m.score()); + (d, score) + }) + .max_by_key(|(_, score)| *score) + // we already checked the list is not empty + .unwrap(); + if score > MIN_DEVICE_MATCH_SCORE { + device + } else { + crate::error::bail!("Could not find an Android Emulator matching {t}") + } + } else if emulator_list.len() > 1 { + let index = prompt::list( + concat!("Detected ", "Android", " emulators"), + emulator_list.iter(), + "emulator", + None, + "Emulator", + ) + .context("failed to prompt for emulator")?; + emulator_list.into_iter().nth(index).unwrap() + } else { + emulator_list.into_iter().next().unwrap() + }; + + Ok(emulator) + } else { + Err(Error::GenericError( + "No available Android Emulator detected".to_string(), + )) + } +} + +enum EmulatorStatus { + Offline { serial_no: String }, + Connected, +} + +fn device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result> { + if let Ok(device) = adb_device_prompt(env, target) { + Ok(device) + } else { + let emulator = emulator_prompt(env, target)?; + 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)); + // 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()) { + if 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; + } + } + } + } +} + +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..."); + } + } + } + } + + Ok(()) +} + +fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> { + device_prompt(env, None).map(|device| device.target()).ok() +} + +fn open_and_wait(config: &AndroidConfig, env: &Env) -> ! { + log::info!("Opening Android Studio"); + if let Err(e) = os::open_file_with("Android Studio", config.project_dir(), &env.base) { + log::error!("{e}"); + } + loop { + sleep(Duration::from_secs(24 * 60 * 60)); + } +} + +fn inject_resources(config: &AndroidConfig, tauri_config: &TauriConfig) -> Result<()> { + let asset_dir = config + .project_dir() + .join("app/src/main") + .join(DEFAULT_ASSET_DIR); + create_dir_all(&asset_dir).fs_context("failed to create asset directory", asset_dir.clone())?; + + write( + asset_dir.join("tauri.conf.json"), + serde_json::to_string(&tauri_config).with_context(|| "failed to serialize tauri config")?, + ) + .fs_context( + "failed to write tauri config", + asset_dir.join("tauri.conf.json"), + )?; + + let resources = match &tauri_config.bundle.resources { + Some(BundleResources::List(paths)) => Some(ResourcePaths::new(paths.as_slice(), true)), + Some(BundleResources::Map(map)) => Some(ResourcePaths::from_map(map, true)), + None => None, + }; + if let Some(resources) = resources { + for resource in resources.iter() { + let resource = resource.context("failed to get resource")?; + let dest = asset_dir.join(resource.target()); + crate::helpers::fs::copy_file(resource.path(), dest).context("failed to copy resource")?; + } + } + + Ok(()) +} + +fn configure_cargo(env: &mut Env, config: &AndroidConfig) -> Result<()> { + for target in Target::all().values() { + let config = target + .generate_cargo_config(config, env) + .context("failed to find Android tool")?; + let target_var_name = target.triple.replace('-', "_").to_uppercase(); + if let Some(linker) = config.linker { + env.base.insert_env_var( + format!("CARGO_TARGET_{target_var_name}_LINKER"), + linker.into(), + ); + } + env.base.insert_env_var( + format!("CARGO_TARGET_{target_var_name}_RUSTFLAGS"), + config.rustflags.join(" ").into(), + ); + } + + Ok(()) +} + +fn generate_tauri_properties( + config: &AndroidConfig, + tauri_config: &TauriConfig, + dev: bool, +) -> Result<()> { + let app_tauri_properties_path = config.project_dir().join("app").join("tauri.properties"); + + let mut app_tauri_properties = Vec::new(); + if let Some(version) = tauri_config.version.as_ref() { + app_tauri_properties.push(format!("tauri.android.versionName={version}")); + if tauri_config.bundle.android.auto_increment_version_code && !dev { + let last_version_code = std::fs::read_to_string(&app_tauri_properties_path) + .ok() + .and_then(|content| { + content + .lines() + .find(|line| line.starts_with("tauri.android.versionCode=")) + .and_then(|line| line.split('=').nth(1)) + .and_then(|s| s.trim().parse::().ok()) + }); + let new_version_code = last_version_code.map(|v| v.saturating_add(1)).unwrap_or(1); + app_tauri_properties.push(format!("tauri.android.versionCode={new_version_code}")); + } else if let Some(version_code) = tauri_config.bundle.android.version_code.as_ref() { + app_tauri_properties.push(format!("tauri.android.versionCode={version_code}")); + } else if let Ok(version) = Version::parse(version) { + let mut version_code = version.major * 1000000 + version.minor * 1000 + version.patch; + + if dev { + version_code = version_code.clamp(1, 2100000000); + } + + if version_code == 0 { + crate::error::bail!( + "You must change the `version` in `tauri.conf.json`. The default value `0.0.0` is not allowed for Android package and must be at least `0.0.1`." + ); + } else if version_code > 2100000000 { + crate::error::bail!( + "Invalid version code {}. Version code must be between 1 and 2100000000. You must change the `version` in `tauri.conf.json`.", + version_code + ); + } + + app_tauri_properties.push(format!("tauri.android.versionCode={version_code}")); + } + } + + if !app_tauri_properties.is_empty() { + let app_tauri_properties_content = format!( + "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n{}", + app_tauri_properties.join("\n") + ); + if std::fs::read_to_string(&app_tauri_properties_path) + .map(|o| o != app_tauri_properties_content) + .unwrap_or(true) + { + write(&app_tauri_properties_path, app_tauri_properties_content) + .context("failed to write 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/project.rs b/crates/tauri-cli/src/mobile/android/project.rs new file mode 100644 index 0000000..d967622 --- /dev/null +++ b/crates/tauri-cli/src/mobile/android/project.rs @@ -0,0 +1,209 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + Error, Result, + error::{Context, ErrorExt}, + helpers::template, +}; +use cargo_mobile2::{ + android::{ + config::{Config, Metadata}, + target::Target, + }, + config::app::DEFAULT_ASSET_DIR, + os, + target::TargetTrait as _, + util::{ + self, + cli::{Report, TextWrapper}, + prefix_path, + }, +}; +use handlebars::Handlebars; +use include_dir::{Dir, include_dir}; + +use std::{ + ffi::OsStr, + fs, + path::{Path, PathBuf}, +}; + +const TEMPLATE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/mobile/android"); + +pub fn generate( + config: &Config, + metadata: &Metadata, + (handlebars, mut map): (Handlebars, template::JsonMap), + wrapper: &TextWrapper, + skip_targets_install: bool, +) -> Result<()> { + if !skip_targets_install { + let installed_targets = + crate::interface::rust::installation::installed_targets().unwrap_or_default(); + let missing_targets = Target::all() + .values() + .filter(|t| !installed_targets.contains(&t.triple().into())) + .collect::>(); + + if !missing_targets.is_empty() { + log::info!("Installing Android Rust targets..."); + for target in missing_targets { + log::info!("Installing target {}", target.triple()); + target.install().map_err(|error| Error::CommandFailed { + command: "rustup target add".to_string(), + error, + })?; + } + } + } + println!("Generating Android Studio project..."); + let dest = config.project_dir(); + let asset_packs = metadata.asset_packs().unwrap_or_default(); + + map.insert( + "root-dir-rel", + Path::new(&os::replace_path_separator( + util::relativize_path( + config.app().root_dir(), + config.project_dir().join(config.app().name_snake()), + ) + .into_os_string(), + )), + ); + map.insert("root-dir", config.app().root_dir()); + map.insert( + "abi-list", + Target::all() + .values() + .map(|target| target.abi) + .collect::>(), + ); + map.insert("target-list", Target::all().keys().collect::>()); + map.insert( + "arch-list", + Target::all() + .values() + .map(|target| target.arch) + .collect::>(), + ); + map.insert("android-app-plugins", metadata.app_plugins()); + map.insert( + "android-project-dependencies", + metadata.project_dependencies(), + ); + map.insert("android-app-dependencies", metadata.app_dependencies()); + map.insert( + "android-app-dependencies-platform", + metadata.app_dependencies_platform(), + ); + map.insert( + "has-code", + metadata.project_dependencies().is_some() + || metadata.app_dependencies().is_some() + || metadata.app_dependencies_platform().is_some(), + ); + map.insert("has-asset-packs", !asset_packs.is_empty()); + map.insert( + "asset-packs", + asset_packs + .iter() + .map(|p| p.name.as_str()) + .collect::>(), + ); + map.insert("windows", cfg!(windows)); + + let identifier = config.app().identifier().replace('.', "/"); + let package_path = format!("java/{identifier}"); + + map.insert("package-path", &package_path); + + let mut created_dirs = Vec::new(); + template::render_with_generator( + &handlebars, + map.inner(), + &TEMPLATE_DIR, + &dest, + &mut |path| generate_out_file(&path, &dest, &package_path, &mut created_dirs), + ) + .with_context(|| "failed to process template")?; + + if !asset_packs.is_empty() { + Report::action_request( + "When running from Android Studio, you must first set your deployment option to \"APK from app bundle\".", + "Android Studio will not be able to find your asset packs otherwise. The option can be found under \"Run > Edit Configurations > Deploy\"." + ).print(wrapper); + } + + let source_dest = dest.join("app"); + for source in metadata.app_sources() { + let source_src = config.app().root_dir().join(source); + let source_file = source_src + .file_name() + .with_context(|| format!("asset source {} is invalid", source_src.display()))?; + fs::copy(&source_src, source_dest.join(source_file)) + .fs_context("failed to copy asset", source_src)?; + } + + let dest = prefix_path(dest, "app/src/main/"); + fs::create_dir_all(&dest).fs_context("failed to create directory", dest.clone())?; + + let asset_dir = dest.join(DEFAULT_ASSET_DIR); + if !asset_dir.is_dir() { + fs::create_dir_all(&asset_dir).fs_context("failed to create asset dir", asset_dir)?; + } + + Ok(()) +} + +fn generate_out_file( + path: &Path, + dest: &Path, + package_path: &str, + created_dirs: &mut Vec, +) -> std::io::Result> { + let mut iter = path.iter(); + let root = iter.next().unwrap().to_str().unwrap(); + let path_without_root: std::path::PathBuf = iter.collect(); + let path = match ( + root, + path.extension().and_then(|o| o.to_str()), + path_without_root.strip_prefix("src/main"), + ) { + ("app" | "buildSrc", Some("kt"), Ok(path)) => { + let parent = path.parent().unwrap(); + let file_name = path.file_name().unwrap(); + let out_dir = dest + .join(root) + .join("src/main") + .join(package_path) + .join(parent); + out_dir.join(file_name) + } + _ => dest.join(path), + }; + + let parent = path.parent().unwrap().to_path_buf(); + if !created_dirs.contains(&parent) { + fs::create_dir_all(&parent)?; + created_dirs.push(parent); + } + + let mut options = fs::OpenOptions::new(); + options.write(true); + + #[cfg(unix)] + if path.file_name().unwrap() == OsStr::new("gradlew") { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o755); + } + + if path.file_name().unwrap() == OsStr::new("BuildTask.kt") { + options.truncate(true).create(true).open(path).map(Some) + } else if !path.exists() { + options.create(true).open(path).map(Some) + } else { + Ok(None) + } +} diff --git a/crates/tauri-cli/src/mobile/android/run.rs b/crates/tauri-cli/src/mobile/android/run.rs new file mode 100644 index 0000000..785af59 --- /dev/null +++ b/crates/tauri-cli/src/mobile/android/run.rs @@ -0,0 +1,184 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use cargo_mobile2::{ + android::target::Target, + opts::{FilterLevel, NoiseLevel, Profile}, + target::TargetTrait, +}; +use clap::{ArgAction, Parser}; +use std::path::PathBuf; + +use super::{configure_cargo, device_prompt, env, sync_debug_application_id_suffix}; +use crate::{ + ConfigValue, Result, + error::Context, + helpers::config::ConfigMetadata, + interface::{DevProcess, WatcherOptions}, + mobile::{DevChild, TargetDevice}, +}; + +#[derive(Debug, Clone, Parser)] +#[clap( + about = "Run your app in production mode on Android", + long_about = "Run your app in production mode on Android. It makes use of the `build.frontendDist` property from your `tauri.conf.json` file. It also runs your `build.beforeBuildCommand` which usually builds your frontend into `build.frontendDist`." +)] +pub struct Options { + /// Run the app in release mode + #[clap(short, long)] + pub release: bool, + /// List of cargo features to activate + #[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')] + pub features: Vec, + /// JSON strings or paths to JSON, JSON5 or TOML files to merge with the default configuration file + /// + /// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts. + /// + /// Note that a platform-specific file is looked up and merged with the default file by default + /// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json) + /// but you can use this for more specific use cases such as different build flavors. + #[clap(short, long)] + pub config: Vec, + /// Disable the file watcher + #[clap(long)] + pub no_watch: bool, + /// Additional paths to watch for changes. + #[clap(long)] + pub additional_watch_folders: Vec, + /// Open Android Studio + #[clap(short, long)] + pub open: bool, + /// Runs on the given device name + pub device: Option, + /// Command line arguments passed to the runner. + /// Use `--` to explicitly mark the start of the arguments. + /// e.g. `tauri android build -- [runnerArgs]`. + #[clap(last(true))] + pub args: Vec, + /// Do not error out if a version mismatch is detected on a Tauri package. + /// + /// 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, +} + +pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { + let mut env = env(false)?; + + let device = if options.open { + None + } else { + match device_prompt(&env, options.device.as_deref()) { + Ok(d) => Some(d), + Err(e) => { + log::error!("{e}"); + None + } + } + }; + + let dirs = crate::helpers::app_paths::resolve_dirs(); + let mut tauri_config = crate::helpers::config::get_config( + tauri_utils::platform::Target::Android, + &options + .config + .iter() + .map(|conf| &conf.0) + .collect::>(), + dirs.tauri, + )?; + let mut built_application = super::build::run( + super::build::Options { + debug: !options.release, + targets: device.as_ref().map(|d| { + vec![ + Target::all() + .iter() + .find(|(_key, t)| t.arch == d.target().arch) + .map(|(key, _t)| key.to_string()) + .expect("Target not found"), + ] + }), + features: options.features, + config: options.config.clone(), + split_per_abi: true, + apk: false, + aab: false, + skip_bundle: true, + open: options.open, + ci: false, + args: options.args, + ignore_version_mismatches: options.ignore_version_mismatches, + target_device: device.as_ref().map(|d| TargetDevice { + id: d.serial_no().to_string(), + name: d.name().to_string(), + }), + }, + noise_level, + &dirs, + &tauri_config, + )?; + + configure_cargo(&mut env, &built_application.config)?; + + // options.open is handled by the build command + // so all we need to do here is run the app on the selected device + if let Some(device) = device { + let config = built_application.config.clone(); + let release = options.release; + + 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_with_application_id_suffix( + &config, + &env, + noise_level, + if !release { + Profile::Debug + } else { + Profile::Release + }, + Some(match noise_level { + NoiseLevel::Polite => FilterLevel::Info, + NoiseLevel::LoudAndProud => FilterLevel::Debug, + NoiseLevel::FranklyQuitePedantic => FilterLevel::Verbose, + }), + false, + false, + format!("{}.MainActivity", config.app().identifier()), + application_id_suffix, + ) + .map(|c| Box::new(DevChild::new(c)) as Box) + .context("failed to run Android app") + }; + + if options.no_watch { + runner(&tauri_config)?; + } else { + built_application.interface.watch( + &mut tauri_config, + WatcherOptions { + config: options.config, + additional_watch_folders: options.additional_watch_folders, + }, + runner, + &dirs, + )?; + } + } + + Ok(()) +} diff --git a/crates/tauri-cli/src/mobile/init.rs b/crates/tauri-cli/src/mobile/init.rs new file mode 100644 index 0000000..9f79e11 --- /dev/null +++ b/crates/tauri-cli/src/mobile/init.rs @@ -0,0 +1,414 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{Target, get_app}; +use crate::{ + ConfigValue, Result, + helpers::app_paths::Dirs, + helpers::{config::get_config as get_tauri_config, template::JsonMap}, + interface::AppInterface, +}; +use cargo_mobile2::{ + config::app::App, + reserved_names::KOTLIN_ONLY_KEYWORDS, + util::{ + self, + cli::{Report, TextWrapper}, + }, +}; +use handlebars::{ + Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError, RenderErrorReason, +}; + +use std::{env::var_os, path::PathBuf}; + +pub fn command( + target: Target, + ci: bool, + reinstall_deps: bool, + skip_targets_install: bool, + config: Vec, +) -> Result<()> { + let dirs = crate::helpers::app_paths::resolve_dirs(); + let wrapper = TextWrapper::default(); + + exec( + target, + &wrapper, + ci, + reinstall_deps, + skip_targets_install, + config, + dirs, + )?; + Ok(()) +} + +fn exec( + target: Target, + wrapper: &TextWrapper, + #[allow(unused_variables)] non_interactive: bool, + #[allow(unused_variables)] reinstall_deps: bool, + skip_targets_install: bool, + config: Vec, + dirs: Dirs, +) -> Result { + let tauri_config = get_tauri_config( + target.platform_target(), + &config.iter().map(|conf| &conf.0).collect::>(), + dirs.tauri, + )?; + + let app = get_app( + target, + &tauri_config, + &AppInterface::new(&tauri_config, None, dirs.tauri)?, + dirs.tauri, + ); + + let (handlebars, mut map) = handlebars(&app); + + let mut args = std::env::args_os(); + + let (binary, mut build_args) = args + .next() + .map(|bin| { + let bin_path = PathBuf::from(&bin); + let mut build_args = vec!["tauri"]; + + if let Some(bin_stem) = bin_path.file_stem() { + let r = regex::Regex::new("(nodejs|node)\\-?([1-9]*)*$").unwrap(); + if r.is_match(&bin_stem.to_string_lossy()) { + if var_os("PNPM_PACKAGE_NAME").is_some() { + return ("pnpm".into(), build_args); + } else if is_pnpm_dlx() { + return ("pnpm".into(), vec!["dlx", "@tauri-apps/cli"]); + } else if is_pnpm_run() { + return ("pnpm".into(), build_args); + } else if let Some(npm_execpath) = var_os("npm_execpath") { + let manager_stem = PathBuf::from(&npm_execpath) + .file_stem() + .unwrap() + .to_os_string(); + let is_npm = manager_stem == "npm-cli"; + let binary = if is_npm { + "npm".into() + } else if manager_stem == "npx-cli" { + "npx".into() + } else { + manager_stem + }; + + if is_npm { + build_args.insert(0, "run"); + build_args.insert(1, "--"); + } + + return (binary, build_args); + } + } else if bin_stem == "deno" { + build_args.insert(0, "task"); + return (std::ffi::OsString::from("deno"), build_args); + } else if !cfg!(debug_assertions) && bin_stem == "cargo-tauri" { + return (std::ffi::OsString::from("cargo"), build_args); + } + } + + (bin, build_args) + }) + .unwrap_or_else(|| (std::ffi::OsString::from("cargo"), vec!["tauri"])); + + build_args.push(target.command_name()); + build_args.push(target.ide_build_script_name()); + + let mut binary = binary.to_string_lossy().to_string(); + if binary.ends_with(".exe") || binary.ends_with(".cmd") || binary.ends_with(".bat") { + // remove Windows-only extension + binary.pop(); + binary.pop(); + binary.pop(); + binary.pop(); + } + + map.insert("tauri-binary", binary); + map.insert("tauri-binary-args", &build_args); + map.insert("tauri-binary-args-str", build_args.join(" ")); + + let app = match target { + // Generate Android Studio project + Target::Android => { + let _env = super::android::env(non_interactive)?; + 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, + (handlebars, map), + wrapper, + skip_targets_install, + )?; + app + } + #[cfg(target_os = "macos")] + // Generate Xcode project + Target::Ios => { + let (config, metadata) = + super::ios::get_config(&app, &tauri_config, &[], &Default::default(), dirs.tauri)?; + map.insert("apple", &config); + super::ios::project::generate( + &tauri_config, + &config, + &metadata, + (handlebars, map), + wrapper, + non_interactive, + reinstall_deps, + skip_targets_install, + )?; + app + } + }; + + Report::victory( + "Project generated successfully!", + "Make cool apps! 🌻 🐕 🎉", + ) + .print(wrapper); + Ok(app) +} + +fn handlebars(app: &App) -> (Handlebars<'static>, JsonMap) { + let mut h = Handlebars::new(); + h.register_escape_fn(handlebars::no_escape); + + h.register_helper("html-escape", Box::new(html_escape)); + h.register_helper("join", Box::new(join)); + h.register_helper("quote-and-join", Box::new(quote_and_join)); + h.register_helper( + "quote-and-join-colon-prefix", + Box::new(quote_and_join_colon_prefix), + ); + h.register_helper("snake-case", Box::new(snake_case)); + h.register_helper("escape-kotlin-keyword", Box::new(escape_kotlin_keyword)); + // don't mix these up or very bad things will happen to all of us + h.register_helper("prefix-path", Box::new(prefix_path)); + h.register_helper("unprefix-path", Box::new(unprefix_path)); + + let mut map = JsonMap::default(); + map.insert("app", app); + + (h, map) +} + +fn get_str<'a>(helper: &'a Helper) -> &'a str { + helper + .param(0) + .and_then(|v| v.value().as_str()) + .unwrap_or("") +} + +fn get_str_array(helper: &Helper, formatter: impl Fn(&str) -> String) -> Option> { + helper.param(0).and_then(|v| { + v.value() + .as_array() + .and_then(|arr| arr.iter().map(|val| val.as_str().map(&formatter)).collect()) + }) +} + +fn html_escape( + helper: &Helper, + _: &Handlebars, + _ctx: &Context, + _: &mut RenderContext, + out: &mut dyn Output, +) -> HelperResult { + out + .write(&handlebars::html_escape(get_str(helper))) + .map_err(Into::into) +} + +fn join( + helper: &Helper, + _: &Handlebars, + _: &Context, + _: &mut RenderContext, + out: &mut dyn Output, +) -> HelperResult { + out + .write( + &get_str_array(helper, |s| s.to_string()) + .ok_or_else(|| { + RenderErrorReason::ParamTypeMismatchForName("join", "0".to_owned(), "array".to_owned()) + })? + .join(", "), + ) + .map_err(Into::into) +} + +fn quote_and_join( + helper: &Helper, + _: &Handlebars, + _: &Context, + _: &mut RenderContext, + out: &mut dyn Output, +) -> HelperResult { + out + .write( + &get_str_array(helper, |s| format!("{s:?}")) + .ok_or_else(|| { + RenderErrorReason::ParamTypeMismatchForName( + "quote-and-join", + "0".to_owned(), + "array".to_owned(), + ) + })? + .join(", "), + ) + .map_err(Into::into) +} + +fn quote_and_join_colon_prefix( + helper: &Helper, + _: &Handlebars, + _: &Context, + _: &mut RenderContext, + out: &mut dyn Output, +) -> HelperResult { + out + .write( + &get_str_array(helper, |s| format!("{:?}", format!(":{s}"))) + .ok_or_else(|| { + RenderErrorReason::ParamTypeMismatchForName( + "quote-and-join-colon-prefix", + "0".to_owned(), + "array".to_owned(), + ) + })? + .join(", "), + ) + .map_err(Into::into) +} + +fn snake_case( + helper: &Helper, + _: &Handlebars, + _: &Context, + _: &mut RenderContext, + out: &mut dyn Output, +) -> HelperResult { + use heck::ToSnekCase as _; + out + .write(&get_str(helper).to_snek_case()) + .map_err(Into::into) +} + +fn escape_kotlin_keyword( + helper: &Helper, + _: &Handlebars, + _: &Context, + _: &mut RenderContext, + out: &mut dyn Output, +) -> HelperResult { + let escaped_result = get_str(helper) + .split('.') + .map(|s| { + if KOTLIN_ONLY_KEYWORDS.contains(&s) { + format!("`{s}`") + } else { + s.to_string() + } + }) + .collect::>() + .join("."); + + out.write(&escaped_result).map_err(Into::into) +} + +fn app_root(ctx: &Context) -> std::result::Result<&str, RenderError> { + let app_root = ctx + .data() + .get("app") + .ok_or_else(|| RenderErrorReason::Other("`app` missing from template data.".to_owned()))? + .get("root-dir") + .ok_or_else(|| { + RenderErrorReason::Other("`app.root-dir` missing from template data.".to_owned()) + })?; + app_root.as_str().ok_or_else(|| { + RenderErrorReason::Other("`app.root-dir` contained invalid UTF-8.".to_owned()).into() + }) +} + +fn prefix_path( + helper: &Helper, + _: &Handlebars, + ctx: &Context, + _: &mut RenderContext, + out: &mut dyn Output, +) -> HelperResult { + out + .write( + util::prefix_path(app_root(ctx)?, get_str(helper)) + .to_str() + .ok_or_else(|| { + RenderErrorReason::Other( + "Either the `app.root-dir` or the specified path contained invalid UTF-8.".to_owned(), + ) + })?, + ) + .map_err(Into::into) +} + +fn unprefix_path( + helper: &Helper, + _: &Handlebars, + ctx: &Context, + _: &mut RenderContext, + out: &mut dyn Output, +) -> HelperResult { + out + .write( + util::unprefix_path(app_root(ctx)?, get_str(helper)) + .map_err(|_| { + RenderErrorReason::Other( + "Attempted to unprefix a path that wasn't in the app root dir.".to_owned(), + ) + })? + .to_str() + .ok_or_else(|| { + RenderErrorReason::Other( + "Either the `app.root-dir` or the specified path contained invalid UTF-8.".to_owned(), + ) + })?, + ) + .map_err(Into::into) +} + +/// pnpm's native binary (pnpm >= 11) runs package scripts without setting `PNPM_PACKAGE_NAME`, +/// and `npm_execpath` points at the binary itself, which is not necessarily named `pnpm` +/// (corepack downloads it as `pnpm-native`), so the user agent is what identifies it. +fn is_pnpm_run() -> bool { + std::env::var("npm_config_user_agent").is_ok_and(|user_agent| user_agent.starts_with("pnpm/")) +} + +fn is_pnpm_dlx() -> bool { + var_os("NODE_PATH") + .map(PathBuf::from) + .is_some_and(|node_path| { + let mut iter = node_path.components().peekable(); + while let Some(c) = iter.next() { + if c.as_os_str() == "pnpm" && iter.peek().is_some_and(|c| c.as_os_str() == "dlx") { + return true; + } + } + false + }) +} diff --git a/crates/tauri-cli/src/mobile/ios/build.rs b/crates/tauri-cli/src/mobile/ios/build.rs new file mode 100644 index 0000000..d86ca9f --- /dev/null +++ b/crates/tauri-cli/src/mobile/ios/build.rs @@ -0,0 +1,665 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{ + MobileTarget, OptionsHandle, detect_target_ok, ensure_init, env, get_app, get_config, + inject_resources, load_pbxproj, log_finished, open_and_wait, project_config, + synchronize_project_config, +}; +use crate::{ + ConfigValue, Error, Result, + build::Options as BuildOptions, + error::{Context, ErrorExt}, + helpers::{ + app_paths::Dirs, + config::{ConfigMetadata, get_config as get_tauri_config}, + flock, + plist::merge_plist, + }, + interface::{AppInterface, Options as InterfaceOptions}, + mobile::{CliOptions, TargetDevice, ios::ensure_ios_runtime_installed, write_options}, +}; +use clap::{ArgAction, Parser, ValueEnum}; + +use cargo_mobile2::{ + apple::{ + config::Config as AppleConfig, + target::{ArchiveConfig, BuildConfig, ExportConfig, Target}, + }, + env::Env, + opts::{NoiseLevel, Profile}, + target::{TargetInvalid, TargetTrait, call_for_targets_with_fallback}, +}; +use rand::distr::{Alphanumeric, SampleString}; + +use std::{ + env::{set_current_dir, var, var_os}, + fs, + path::{Path, PathBuf}, +}; + +#[derive(Debug, Clone, Parser)] +#[clap( + about = "Build your app in release mode for iOS and generate IPAs", + long_about = "Build your app in release mode for iOS and generate IPAs. It makes use of the `build.frontendDist` property from your `tauri.conf.json` file. It also runs your `build.beforeBuildCommand` which usually builds your frontend into `build.frontendDist`." +)] +pub struct Options { + /// Builds with the debug flag + #[clap(short, long)] + pub debug: bool, + /// Which targets to build. + #[clap( + short, + long = "target", + action = ArgAction::Append, + num_args(0..), + default_value = Target::DEFAULT_KEY, + value_parser(clap::builder::PossibleValuesParser::new(Target::name_list())) + )] + pub targets: Option>, + /// List of cargo features to activate + #[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')] + pub features: Vec, + /// JSON strings or paths to JSON, JSON5 or TOML files to merge with the default configuration file + /// + /// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts. + /// + /// Note that a platform-specific file is looked up and merged with the default file by default + /// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json) + /// but you can use this for more specific use cases such as different build flavors. + #[clap(short, long)] + pub config: Vec, + /// Build number to append to the app version. + #[clap(long)] + pub build_number: Option, + /// Open Xcode + #[clap(short, long)] + pub open: bool, + /// Skip prompting for values + #[clap(long, env = "CI")] + pub ci: bool, + /// Describes how Xcode should export the archive. + /// + /// Use this to create a package ready for the App Store (app-store-connect option) or TestFlight (release-testing option). + #[clap(long, value_enum)] + pub export_method: Option, + /// Command line arguments passed to the runner. + /// Use `--` to explicitly mark the start of the arguments. + /// e.g. `tauri ios build -- [runnerArgs]`. + #[clap(last(true))] + pub args: Vec, + /// Do not error out if a version mismatch is detected on a Tauri package. + /// + /// 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, +} + +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum ExportMethod { + AppStoreConnect, + ReleaseTesting, + Debugging, +} + +impl ExportMethod { + /// Xcode 15.4 deprecated these names (in this case we should use the Display impl). + pub fn pre_xcode_15_4_name(&self) -> String { + match self { + Self::AppStoreConnect => "app-store".to_string(), + Self::ReleaseTesting => "ad-hoc".to_string(), + Self::Debugging => "development".to_string(), + } + } +} + +impl std::fmt::Display for ExportMethod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::AppStoreConnect => write!(f, "app-store-connect"), + Self::ReleaseTesting => write!(f, "release-testing"), + Self::Debugging => write!(f, "debugging"), + } + } +} + +impl std::str::FromStr for ExportMethod { + type Err = &'static str; + + fn from_str(s: &str) -> std::result::Result { + match s { + "app-store-connect" => Ok(Self::AppStoreConnect), + "release-testing" => Ok(Self::ReleaseTesting), + "debugging" => Ok(Self::Debugging), + _ => Err("unknown ios target"), + } + } +} + +impl From for BuildOptions { + fn from(options: Options) -> Self { + Self { + runner: None, + debug: options.debug, + target: None, + features: options.features, + bundles: None, + no_bundle: false, + config: options.config, + args: options.args, + ci: options.ci, + skip_stapling: false, + ignore_version_mismatches: options.ignore_version_mismatches, + no_sign: options.no_sign, + no_binary_patching: false, + } + } +} + +pub struct BuiltApplication { + pub config: AppleConfig, + pub interface: AppInterface, + // prevent drop + #[allow(dead_code)] + options_handle: OptionsHandle, +} + +pub fn command(options: Options, noise_level: NoiseLevel) -> Result { + let dirs = crate::helpers::app_paths::resolve_dirs(); + run(options, noise_level, &dirs) +} + +pub fn run(options: Options, noise_level: NoiseLevel, dirs: &Dirs) -> Result { + let mut build_options: BuildOptions = options.clone().into(); + build_options.target = Some( + Target::all() + .get( + options + .targets + .as_ref() + .and_then(|t| t.first()) + .map(|t| t.as_str()) + .unwrap_or(Target::DEFAULT_KEY), + ) + .unwrap() + .triple + .into(), + ); + + let tauri_config = get_tauri_config( + tauri_utils::platform::Target::Ios, + &options.config.iter().map(|c| &c.0).collect::>(), + dirs.tauri, + )?; + let interface = AppInterface::new(&tauri_config, build_options.target.clone(), dirs.tauri)?; + interface.build_options(&mut build_options.args, &mut build_options.features, true); + + let app = get_app(MobileTarget::Ios, &tauri_config, &interface, dirs.tauri); + let (mut config, _) = get_config( + &app, + &tauri_config, + &build_options.features, + &CliOptions { + dev: false, + features: build_options.features.clone(), + args: build_options.args.clone(), + noise_level, + vars: Default::default(), + config: build_options.config.clone(), + target_device: None, + }, + dirs.tauri, + )?; + + set_current_dir(dirs.tauri).context("failed to set current directory")?; + + ensure_init( + &tauri_config, + config.app(), + config.project_dir(), + MobileTarget::Ios, + options.ci, + )?; + inject_resources(&config, &tauri_config)?; + + let mut plist = plist::Dictionary::new(); + plist.insert( + "CFBundleShortVersionString".into(), + config.bundle_version_short().into(), + ); + + let info_plist_path = config + .project_dir() + .join(config.scheme()) + .join("Info.plist"); + let mut src_plists = vec![info_plist_path.clone().into()]; + src_plists.push(plist::Value::Dictionary(plist).into()); + if dirs.tauri.join("Info.plist").exists() { + src_plists.push(dirs.tauri.join("Info.plist").into()); + } + 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(associations) = tauri_config.bundle.file_associations.as_ref() { + if 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 + .to_file_xml(&info_plist_path) + .map_err(std::io::Error::other) + .fs_context("failed to save merged Info.plist file", info_plist_path)?; + + let mut env = env().context("failed to load iOS environment")?; + + if !options.open { + ensure_ios_runtime_installed()?; + } + + let mut export_options_plist = plist::Dictionary::new(); + if let Some(method) = options.export_method { + let xcode_version = + crate::info::env_system::xcode_version().context("failed to determine Xcode version")?; + let mut iter = xcode_version.split('.'); + let major = iter.next().context(format!( + "failed to parse Xcode version `{xcode_version}` as semver" + ))?; + let minor = iter.next().context(format!( + "failed to parse Xcode version `{xcode_version}` as semver" + ))?; + let major = major.parse::().ok().context(format!( + "failed to parse Xcode version `{xcode_version}` as semver: major is not a number" + ))?; + let minor = minor.parse::().ok().context(format!( + "failed to parse Xcode version `{xcode_version}` as semver: minor is not a number" + ))?; + + if major < 15 || (major == 15 && minor < 4) { + export_options_plist.insert("method".to_string(), method.pre_xcode_15_4_name().into()); + } else { + export_options_plist.insert("method".to_string(), method.to_string().into()); + } + } + + let (keychain, provisioning_profile) = super::signing_from_env()?; + let project_config = project_config(keychain.as_ref(), provisioning_profile.as_ref())?; + let mut pbxproj = load_pbxproj(&config)?; + + // synchronize pbxproj and exportoptions + synchronize_project_config( + &config, + &tauri_config, + &mut pbxproj, + &mut export_options_plist, + &project_config, + options.debug, + )?; + if pbxproj.has_changes() { + pbxproj + .save() + .fs_context("failed to save pbxproj file", pbxproj.path)?; + } + + // merge export options and write to temp file + let _export_options_tmp = if !export_options_plist.is_empty() { + let export_options_plist_path = config.project_dir().join("ExportOptions.plist"); + let export_options = + tempfile::NamedTempFile::new().context("failed to create temporary file")?; + + let merged_plist = merge_plist(vec![ + export_options_plist_path.into(), + plist::Value::from(export_options_plist).into(), + ])?; + merged_plist + .to_file_xml(export_options.path()) + .map_err(std::io::Error::other) + .fs_context( + "failed to save export options plist file", + export_options.path().to_path_buf(), + )?; + + config.set_export_options_plist_path(export_options.path()); + + Some(export_options) + } else { + None + }; + + let open = options.open; + let options_handle = run_build( + &interface, + options, + build_options, + tauri_config, + &mut config, + &mut env, + noise_level, + dirs, + )?; + + if open { + open_and_wait(&config, &env); + } + + Ok(BuiltApplication { + config, + interface, + options_handle, + }) +} + +#[allow(clippy::too_many_arguments)] +fn run_build( + interface: &AppInterface, + options: Options, + mut build_options: BuildOptions, + tauri_config: ConfigMetadata, + config: &mut AppleConfig, + env: &mut Env, + noise_level: NoiseLevel, + dirs: &Dirs, +) -> Result { + let profile = if options.debug { + Profile::Debug + } else { + Profile::Release + }; + + crate::build::setup(interface, &mut build_options, &tauri_config, dirs, true)?; + + let app_settings = interface.app_settings(); + let out_dir = app_settings.out_dir( + &InterfaceOptions { + debug: build_options.debug, + target: build_options.target.clone(), + args: build_options.args.clone(), + ..Default::default() + }, + dirs.tauri, + )?; + let _lock = flock::open_rw(out_dir.join("lock").with_extension("ios"), "iOS")?; + + let cli_options = CliOptions { + dev: false, + features: build_options.features.clone(), + args: build_options.args.clone(), + noise_level, + vars: Default::default(), + config: build_options.config.clone(), + target_device: options.target_device.clone(), + }; + let handle = write_options(&tauri_config, cli_options)?; + + if options.open { + return Ok(handle); + } + + let mut out_files = Vec::new(); + + let force_skip_target_fallback = options.targets.as_ref().is_some_and(|t| t.is_empty()); + + call_for_targets_with_fallback( + options.targets.unwrap_or_default().iter(), + if force_skip_target_fallback { + &|_| None + } else { + &detect_target_ok + }, + env, + |target: &Target| -> Result<()> { + let mut app_version = config.bundle_version().to_string(); + if let Some(build_number) = options.build_number { + app_version.push('.'); + app_version.push_str(&build_number.to_string()); + } + + let credentials = auth_credentials_from_env()?; + let skip_signing = options.no_sign || credentials.is_some(); + + if !(options.archive_only || options.no_sign) { + let mut build_config = BuildConfig::new().allow_provisioning_updates(); + if let Some(credentials) = &credentials { + build_config = build_config.authentication_credentials(credentials.clone()); + } + if skip_signing { + build_config = build_config.skip_codesign(); + } + + target + .build(None, config, env, noise_level, profile, build_config) + .context("failed to build iOS app")?; + } + + let mut archive_config = ArchiveConfig::new().allow_provisioning_updates(); + if let Some(credentials) = &credentials { + archive_config = archive_config.authentication_credentials(credentials.clone()); + } + if skip_signing { + archive_config = archive_config.skip_codesign(); + } + + target + .archive( + config, + env, + noise_level, + profile, + Some(app_version), + archive_config, + ) + .context("failed to archive iOS app")?; + + if options.archive_only { + out_files.push( + config + .archive_dir() + .join(format!("{}.xcarchive", config.scheme())), + ); + return Ok(()); + } + + let out_dir = config.export_dir().join(target.arch); + + if target.sdk == "iphonesimulator" { + fs::create_dir_all(&out_dir) + .fs_context("failed to create Xcode output directory", out_dir.clone())?; + + let app_path = config + .archive_dir() + .join(format!("{}.xcarchive", config.scheme())) + .join("Products") + .join("Applications") + .join(config.app().stylized_name()) + .with_extension("app"); + + let path = out_dir.join(app_path.file_name().unwrap()); + // `rename` does not replace a non-empty directory, such as the previous build + if path.exists() { + fs::remove_dir_all(&path).fs_context("failed to remove previous app", path.clone())?; + } + fs::rename(&app_path, &path).fs_context("failed to rename app", app_path)?; + out_files.push(path); + } else if options.no_sign { + fs::create_dir_all(&out_dir) + .fs_context("failed to create Xcode output directory", out_dir.clone())?; + + let app_path = config + .archive_dir() + .join(format!("{}.xcarchive", config.scheme())) + .join("Products") + .join("Applications") + .join(config.app().stylized_name()) + .with_extension("app"); + + let ipa_path = out_dir + .join(config.app().stylized_name()) + .with_extension("ipa"); + create_ipa(&app_path, &ipa_path)?; + out_files.push(ipa_path); + } else { + // if we skipped code signing, we do not have the entitlements applied to our exported IPA + // we must force sign the app binary with a dummy certificate just to preserve the entitlements + // target.export() will sign it with an actual certificate for us + if skip_signing { + let password = Alphanumeric.sample_string(&mut rand::rng(), 16); + let certificate = tauri_macos_sign::certificate::generate_self_signed( + tauri_macos_sign::certificate::SelfSignedCertificateRequest { + algorithm: "rsa".to_string(), + profile: tauri_macos_sign::certificate::CertificateProfile::AppleDistribution, + team_id: "unset".to_string(), + person_name: "Tauri".to_string(), + country_name: "NL".to_string(), + validity_days: 365, + password: password.clone(), + }, + ) + .map_err(Box::new)?; + let tmp_dir = tempfile::tempdir().context("failed to create temporary directory")?; + let cert_path = tmp_dir.path().join("cert.p12"); + std::fs::write(&cert_path, certificate) + .fs_context("failed to write certificate", cert_path.clone())?; + let self_signed_cert_keychain = + tauri_macos_sign::Keychain::with_certificate_file(&cert_path, &password.into()) + .map_err(Box::new)?; + + let app_dir = config + .export_dir() + .join(format!("{}.xcarchive", config.scheme())) + .join("Products/Applications") + .join(format!("{}.app", config.app().stylized_name())); + + self_signed_cert_keychain + .sign( + &app_dir.join(config.app().stylized_name()), + Some( + &config + .project_dir() + .join(config.scheme()) + .join(format!("{}.entitlements", config.scheme())), + ), + false, + ) + .map_err(Box::new)?; + } + + let mut export_config = ExportConfig::new().allow_provisioning_updates(); + if let Some(credentials) = &credentials { + export_config = export_config.authentication_credentials(credentials.clone()); + } + + target + .export(config, env, noise_level, export_config) + .context("failed to export iOS app")?; + + if let Ok(ipa_path) = config.ipa_path() { + fs::create_dir_all(&out_dir) + .fs_context("failed to create Xcode output directory", out_dir.clone())?; + let path = out_dir.join(ipa_path.file_name().unwrap()); + fs::rename(&ipa_path, &path).fs_context("failed to rename IPA", ipa_path)?; + out_files.push(path); + } + } + + Ok(()) + }, + ) + .map_err(|e: TargetInvalid| Error::GenericError(e.to_string()))??; + + if !out_files.is_empty() { + log_finished(out_files, "iOS Bundle"); + } + + Ok(handle) +} + +fn create_ipa(app_path: &Path, ipa_path: &Path) -> 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"), + var("APPLE_API_ISSUER"), + var_os("APPLE_API_KEY_PATH").map(PathBuf::from), + ) { + (Ok(key_id), Ok(key_issuer_id), Some(key_path)) => { + Ok(Some(cargo_mobile2::apple::AuthCredentials { + key_path, + key_id, + key_issuer_id, + })) + } + (Err(_), Err(_), None) => Ok(None), + _ => crate::error::bail!( + "APPLE_API_KEY, APPLE_API_ISSUER and APPLE_API_KEY_PATH must be provided for code signing" + ), + } +} diff --git a/crates/tauri-cli/src/mobile/ios/dev.rs b/crates/tauri-cli/src/mobile/ios/dev.rs new file mode 100644 index 0000000..7ebcc46 --- /dev/null +++ b/crates/tauri-cli/src/mobile/ios/dev.rs @@ -0,0 +1,407 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{ + MobileTarget, ProjectConfig, device_prompt, ensure_init, env, get_app, get_config, + inject_resources, load_pbxproj, open_and_wait, synchronize_project_config, +}; +use crate::{ + ConfigValue, Result, + dev::Options as DevOptions, + error::{Context, ErrorExt}, + helpers::{ + app_paths::Dirs, + config::{ConfigMetadata, get_config as get_tauri_config}, + flock, + plist::merge_plist, + }, + interface::{AppInterface, MobileOptions, Options as InterfaceOptions}, + mobile::{ + CliOptions, DevChild, DevHost, DevProcess, ios::ensure_ios_runtime_installed, + use_network_address_for_dev_url, write_options, + }, +}; +use clap::{ArgAction, Parser}; + +use cargo_mobile2::{ + apple::{ + config::Config as AppleConfig, + device::{Device, DeviceKind, RunError}, + target::BuildError, + }, + env::Env, + opts::{NoiseLevel, Profile}, +}; +use url::Host; + +use std::{env::set_current_dir, net::Ipv4Addr, path::PathBuf}; + +const PHYSICAL_IPHONE_DEV_WARNING: &str = "To develop on physical phones you need the `--host` option (not required for Simulators). See the documentation for more information: https://v2.tauri.app/develop/#development-server"; + +#[derive(Debug, Clone, Parser)] +#[clap( + about = "Run your app in development mode on iOS", + long_about = "Run your app in development mode on iOS with hot-reloading for the Rust code. +It makes use of the `build.devUrl` property from your `tauri.conf.json` file. +It also runs your `build.beforeDevCommand` which usually starts your frontend devServer. + +When connected to a physical iOS device, the public network address must be used instead of `localhost` +for the devUrl property. Tauri makes that change automatically, but your dev server might need +a different configuration to listen on the public address. You can check the `TAURI_DEV_HOST` +environment variable to determine whether the public network should be used or not." +)] +pub struct Options { + /// List of cargo features to activate + #[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')] + pub features: Vec, + /// Exit on panic + #[clap(short, long)] + exit_on_panic: bool, + /// JSON strings or paths to JSON, JSON5 or TOML files to merge with the default configuration file + /// + /// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts. + /// + /// Note that a platform-specific file is looked up and merged with the default file by default + /// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json) + /// but you can use this for more specific use cases such as different build flavors. + #[clap(short, long)] + pub config: Vec, + /// Run the code in release mode + #[clap(long = "release")] + pub release_mode: bool, + /// Skip waiting for the frontend dev server to start before building the tauri application. + #[clap(long, env = "TAURI_CLI_NO_DEV_SERVER_WAIT")] + pub no_dev_server_wait: bool, + /// Disable the file watcher + #[clap(long)] + pub no_watch: bool, + /// Additional paths to watch for changes. + #[clap(long)] + pub additional_watch_folders: Vec, + /// Open Xcode instead of trying to run on a connected device + #[clap(short, long)] + pub open: bool, + /// Runs on the given device name + pub device: Option, + /// Force prompting for an IP to use to connect to the dev server on mobile. + #[clap(long)] + pub force_ip_prompt: bool, + /// Use the public network address for the development server. + /// If an actual address it provided, it is used instead of prompting to pick one. + /// + /// This option is particularly useful along the `--open` flag when you intend on running on a physical device. + /// + /// This replaces the devUrl configuration value to match the public network address host, + /// it is your responsibility to set up your development server to listen on this address + /// by using 0.0.0.0 as host for instance. + /// + /// When this is set or when running on an iOS device the CLI sets the `TAURI_DEV_HOST` + /// environment variable so you can check this on your framework's configuration to expose the development server + /// on the public network address. + #[clap(long, default_value_t, default_missing_value(""), num_args(0..=1))] + pub host: DevHost, + /// Disable the built-in dev server for static files. + #[clap(long)] + pub no_dev_server: bool, + /// Specify port for the built-in dev server for static files. Defaults to 1430. + #[clap(long, env = "TAURI_CLI_PORT")] + pub port: Option, + /// Command line arguments passed to the runner. + /// Use `--` to explicitly mark the start of the arguments. + /// e.g. `tauri ios dev -- [runnerArgs]`. + #[clap(last(true))] + pub args: Vec, + /// Path to the certificate file used by your dev server. Required for mobile dev when using HTTPS. + #[clap(long, env = "TAURI_DEV_ROOT_CERTIFICATE_PATH")] + pub root_certificate_path: Option, +} + +impl From for DevOptions { + fn from(options: Options) -> Self { + Self { + runner: None, + target: None, + features: options.features, + exit_on_panic: options.exit_on_panic, + config: options.config, + release_mode: options.release_mode, + args: options.args, + no_watch: options.no_watch, + additional_watch_folders: options.additional_watch_folders, + no_dev_server: options.no_dev_server, + no_dev_server_wait: options.no_dev_server_wait, + port: options.port, + host: options.host.0.unwrap_or_default(), + } + } +} + +pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { + let dirs = crate::helpers::app_paths::resolve_dirs(); + + let result = run_command(options, noise_level, dirs); + if result.is_err() { + crate::dev::kill_before_dev_process(); + } + result +} + +fn run_command(options: Options, noise_level: NoiseLevel, dirs: Dirs) -> Result<()> { + // setup env additions before calling env() + if let Some(root_certificate_path) = &options.root_certificate_path { + unsafe { + std::env::set_var( + "TAURI_DEV_ROOT_CERTIFICATE", + std::fs::read_to_string(root_certificate_path).fs_context( + "failed to read root certificate file", + root_certificate_path.clone(), + )?, + ) + }; + } + + let env = env().context("failed to load iOS environment")?; + let device = if options.open { + None + } else { + match device_prompt(&env, options.device.as_deref()) { + Ok(d) => Some(d), + Err(e) => { + log::error!("{e}"); + None + } + } + }; + + if device.is_some() { + ensure_ios_runtime_installed()?; + } + + let mut dev_options: DevOptions = options.clone().into(); + let target_triple = device + .as_ref() + .map(|d| d.target().triple.to_string()) + .unwrap_or_else(|| "aarch64-apple-ios".into()); + dev_options.target = Some(target_triple.clone()); + dev_options.args.push("--lib".into()); + + let tauri_config = get_tauri_config( + tauri_utils::platform::Target::Ios, + &options.config.iter().map(|c| &c.0).collect::>(), + dirs.tauri, + )?; + let interface = AppInterface::new(&tauri_config, Some(target_triple), dirs.tauri)?; + + let app = get_app(MobileTarget::Ios, &tauri_config, &interface, dirs.tauri); + let (config, _) = get_config( + &app, + &tauri_config, + &dev_options.features, + &CliOptions { + dev: true, + features: dev_options.features.clone(), + args: dev_options.args.clone(), + noise_level, + vars: Default::default(), + config: dev_options.config.clone(), + target_device: None, + }, + dirs.tauri, + )?; + + set_current_dir(dirs.tauri).context("failed to set current directory to Tauri directory")?; + + ensure_init( + &tauri_config, + config.app(), + config.project_dir(), + MobileTarget::Ios, + false, + )?; + inject_resources(&config, &tauri_config)?; + + let info_plist_path = config + .project_dir() + .join(config.scheme()) + .join("Info.plist"); + let mut src_plists = vec![info_plist_path.clone().into()]; + if dirs.tauri.join("Info.plist").exists() { + src_plists.push(dirs.tauri.join("Info.plist").into()); + } + 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(associations) = tauri_config.bundle.file_associations.as_ref() { + if 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 + .to_file_xml(&info_plist_path) + .map_err(std::io::Error::other) + .fs_context("failed to save merged Info.plist file", info_plist_path)?; + + let mut pbxproj = load_pbxproj(&config)?; + + // synchronize pbxproj + synchronize_project_config( + &config, + &tauri_config, + &mut pbxproj, + &mut plist::Dictionary::new(), + &ProjectConfig { + code_sign_identity: None, + team_id: None, + provisioning_profile_uuid: None, + }, + !options.release_mode, + )?; + if pbxproj.has_changes() { + pbxproj + .save() + .fs_context("failed to save pbxproj file", pbxproj.path)?; + } + + run_dev( + interface, + options, + dev_options, + tauri_config, + device, + env, + &config, + noise_level, + &dirs, + ) +} + +#[allow(clippy::too_many_arguments)] +fn run_dev( + mut interface: AppInterface, + options: Options, + mut dev_options: DevOptions, + mut tauri_config: ConfigMetadata, + device: Option, + env: Env, + config: &AppleConfig, + noise_level: NoiseLevel, + dirs: &Dirs, +) -> Result<()> { + // when --host is provided or running on a physical device or resolving 0.0.0.0 we must use the network IP + if options.host.0.is_some() + || device + .as_ref() + .map(|device| !matches!(device.kind(), DeviceKind::Simulator)) + .unwrap_or(false) + || tauri_config.build.dev_url.as_ref().is_some_and(|url| { + matches!( + url.host(), + Some(Host::Ipv4(i)) if i == Ipv4Addr::UNSPECIFIED + ) + }) + { + use_network_address_for_dev_url( + &mut tauri_config, + &mut dev_options, + options.force_ip_prompt, + dirs.tauri, + )?; + } + + crate::dev::setup(&interface, &mut dev_options, &mut tauri_config, dirs)?; + + let app_settings = interface.app_settings(); + let out_dir = app_settings.out_dir( + &InterfaceOptions { + debug: !dev_options.release_mode, + target: dev_options.target.clone(), + ..Default::default() + }, + dirs.tauri, + )?; + let _lock = flock::open_rw(out_dir.join("lock").with_extension("ios"), "iOS")?; + + let set_host = options.host.0.is_some(); + + let open = options.open; + interface.mobile_dev( + &mut tauri_config, + MobileOptions { + debug: true, + features: options.features, + args: options.args, + config: dev_options.config.clone(), + no_watch: options.no_watch, + additional_watch_folders: options.additional_watch_folders, + }, + |options, tauri_config| { + let cli_options = CliOptions { + dev: true, + features: options.features.clone(), + args: options.args.clone(), + noise_level, + vars: Default::default(), + config: dev_options.config.clone(), + target_device: None, + }; + let _handle = write_options(tauri_config, cli_options)?; + + let open_xcode = || { + if !set_host { + log::warn!("{PHYSICAL_IPHONE_DEV_WARNING}"); + } + open_and_wait(config, &env) + }; + + if open { + open_xcode() + } else if let Some(device) = &device { + match run(device, options, config, noise_level, &env) { + Ok(c) => Ok(Box::new(c) as Box), + Err(RunError::BuildFailed(BuildError::Sdk(sdk_err))) => { + log::warn!("{sdk_err}"); + open_xcode() + } + Err(e) => { + crate::dev::kill_before_dev_process(); + crate::error::bail!("failed to run iOS app: {}", e) + } + } + } else { + open_xcode() + } + }, + dirs, + ) +} + +fn run( + device: &Device<'_>, + options: MobileOptions, + config: &AppleConfig, + noise_level: NoiseLevel, + env: &Env, +) -> std::result::Result { + let profile = if options.debug { + Profile::Debug + } else { + Profile::Release + }; + + device + .run( + config, + env, + noise_level, + false, // do not quit on app exit + profile, + ) + .map(DevChild::new) +} diff --git a/crates/tauri-cli/src/mobile/ios/mod.rs b/crates/tauri-cli/src/mobile/ios/mod.rs new file mode 100644 index 0000000..a09ee02 --- /dev/null +++ b/crates/tauri-cli/src/mobile/ios/mod.rs @@ -0,0 +1,704 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use cargo_mobile2::{ + apple::{ + config::{ + Config as AppleConfig, Metadata as AppleMetadata, Platform as ApplePlatform, + Raw as RawAppleConfig, + }, + device::{self, Device}, + target::Target, + teams::find_development_teams, + }, + config::app::{App, DEFAULT_ASSET_DIR}, + env::Env, + opts::NoiseLevel, + os, + util::{prompt, relativize_path}, +}; +use clap::{Parser, Subcommand}; +use serde::Deserialize; +use sublime_fuzzy::best_match; +use tauri_utils::resources::ResourcePaths; + +use super::{ + CliOptions, MIN_DEVICE_MATCH_SCORE, OptionsHandle, Target as MobileTarget, ensure_init, env, + get_app, init::command as init_command, log_finished, read_options, +}; +use crate::{ + ConfigValue, Error, Result, + error::{Context, ErrorExt}, + helpers::{ + config::{BundleResources, Config as TauriConfig, ConfigMetadata}, + pbxproj, strip_semver_prerelease_tag, + }, +}; + +use std::{ + env::{set_var, var_os}, + fs::create_dir_all, + path::Path, + str::FromStr, + thread::sleep, + time::Duration, +}; + +mod build; +mod dev; +pub(crate) mod project; +mod run; +mod xcode_script; + +pub const APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME: &str = "APPLE_DEVELOPMENT_TEAM"; +pub const LIB_OUTPUT_FILE_NAME: &str = "libapp.a"; + +#[derive(Parser)] +#[clap( + author, + version, + about = "iOS commands", + subcommand_required(true), + arg_required_else_help(true) +)] +pub struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Debug, Parser)] +#[clap(about = "Initialize iOS target in the project")] +pub struct InitOptions { + /// Skip prompting for values + #[clap(long, env = "CI")] + ci: bool, + /// Reinstall dependencies + #[clap(short, long)] + reinstall_deps: bool, + /// Skips installing rust toolchains via rustup + #[clap(long)] + skip_targets_install: bool, + /// JSON strings or paths to JSON, JSON5 or TOML files to merge with the default configuration file + /// + /// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts. + /// + /// Note that a platform-specific file is looked up and merged with the default file by default + /// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json) + /// but you can use this for more specific use cases such as different build flavors. + #[clap(short, long)] + pub config: Vec, +} + +#[derive(Subcommand)] +enum Commands { + Init(InitOptions), + Dev(dev::Options), + Build(build::Options), + Run(run::Options), + #[clap(hide(true))] + XcodeScript(xcode_script::Options), +} + +pub fn command(cli: Cli, verbosity: u8) -> Result<()> { + let noise_level = NoiseLevel::from_occurrences(verbosity as u64); + match cli.command { + Commands::Init(options) => init_command( + MobileTarget::Ios, + options.ci, + options.reinstall_deps, + options.skip_targets_install, + options.config, + )?, + Commands::Dev(options) => dev::command(options, noise_level)?, + Commands::Build(options) => build::command(options, noise_level).map(|_| ())?, + Commands::Run(options) => run::command(options, noise_level)?, + Commands::XcodeScript(options) => xcode_script::command(options)?, + } + + Ok(()) +} + +pub fn get_config( + app: &App, + tauri_config: &TauriConfig, + features: &[String], + cli_options: &CliOptions, + tauri_dir: &Path, +) -> Result<(AppleConfig, AppleMetadata)> { + let mut ios_options = cli_options.clone(); + ios_options.features.extend_from_slice(features); + + let bundle_version = if let Some(bundle_version) = tauri_config + .bundle + .ios + .bundle_version + .clone() + .or_else(|| tauri_config.version.clone()) + { + // if it's a semver string, we must strip the prerelease tag + if let Ok(mut version) = semver::Version::from_str(&bundle_version) { + if !version.pre.is_empty() { + log::warn!("CFBundleVersion cannot have prerelease tag; stripping from {bundle_version}"); + strip_semver_prerelease_tag(&mut version)?; + } + // correctly serialize version - cannot contain `+` as build metadata separator + Some(format!( + "{}.{}.{}{}", + version.major, + version.minor, + version.patch, + if version.build.is_empty() { + "".to_string() + } else { + format!(".{}", version.build.as_str()) + } + )) + } else { + // let it go as is - cargo-mobile2 will validate it + Some(bundle_version) + } + } else { + None + }; + let full_bundle_version_short = if let Some(app_version) = &tauri_config.version { + if let Ok(mut version) = semver::Version::from_str(app_version) { + if !version.pre.is_empty() { + log::warn!( + "CFBundleShortVersionString cannot have prerelease tag; stripping from {app_version}" + ); + strip_semver_prerelease_tag(&mut version)?; + } + // correctly serialize version - cannot contain `+` as build metadata separator + Some(format!( + "{}.{}.{}{}", + version.major, + version.minor, + version.patch, + if version.build.is_empty() { + "".to_string() + } else { + format!(".{}", version.build.as_str()) + } + )) + } else { + // let it go as is - cargo-mobile2 will validate it + Some(app_version.clone()) + } + } else { + bundle_version.clone() + }; + let bundle_version_short = if let Some(full_version) = full_bundle_version_short.as_deref() { + let mut s = full_version.split('.'); + let short_version = format!( + "{}.{}.{}", + s.next().unwrap_or("0"), + s.next().unwrap_or("0"), + s.next().unwrap_or("0") + ); + + if short_version != full_version { + log::warn!( + "{full_version:?} is not a valid CFBundleShortVersionString since it must contain exactly three dot separated integers; setting it to {short_version} instead" + ); + } + + Some(short_version) + } else { + None + }; + + let raw = RawAppleConfig { + development_team: std::env::var(APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME) + .ok() + .or_else(|| tauri_config.bundle.ios.development_team.clone()) + .or_else(|| { + let teams = find_development_teams().unwrap_or_default(); + match teams.len() { + 0 => { + log::warn!("No code signing certificates found. You must add one and set the certificate development team ID on the `bundle > iOS > developmentTeam` config value or the `{APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME}` environment variable. To list the available certificates, run `tauri info`."); + None + } + 1 => None, + _ => { + log::warn!("You must set the code signing certificate development team ID on the `bundle > iOS > developmentTeam` config value or the `{APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME}` environment variable. Available certificates: {}", teams.iter().map(|t| format!("{} (ID: {})", t.name, t.id)).collect::>().join(", ")); + None + } + } + }), + ios_features: Some(ios_options.features.clone()), + bundle_version, + bundle_version_short, + ios_version: Some(tauri_config.bundle.ios.minimum_system_version.clone()), + ..Default::default() + }; + let config = AppleConfig::from_raw(app.clone(), Some(raw)) + .context("failed to create Apple configuration")?; + + let mut vendor_frameworks = Vec::new(); + let mut frameworks = Vec::new(); + for framework in tauri_config + .bundle + .ios + .frameworks + .clone() + .unwrap_or_default() + { + let framework_path = Path::new(&framework); + let ext = framework_path.extension().unwrap_or_default(); + if ext.is_empty() { + frameworks.push(framework); + } else if ext == "framework" { + frameworks.push( + framework_path + .file_stem() + .unwrap() + .to_string_lossy() + .to_string(), + ); + } else { + vendor_frameworks.push( + relativize_path(tauri_dir.join(framework_path), config.project_dir()) + .to_string_lossy() + .to_string(), + ); + } + } + + let metadata = AppleMetadata { + supported: true, + ios: ApplePlatform { + cargo_args: Some(ios_options.args), + features: Some(ios_options.features), + frameworks: Some(frameworks), + vendor_frameworks: Some(vendor_frameworks), + ..Default::default() + }, + macos: Default::default(), + }; + + unsafe { set_var("TAURI_IOS_PROJECT_PATH", config.project_dir()) }; + unsafe { set_var("TAURI_IOS_APP_NAME", config.app().name()) }; + + Ok((config, metadata)) +} + +fn connected_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result> { + let device_list = device::list_devices(env).map_err(|cause| { + Error::GenericError(format!("Failed to detect connected iOS devices: {cause}")) + })?; + if !device_list.is_empty() { + let device = if let Some(t) = target { + let (device, score) = device_list + .into_iter() + .rev() + .map(|d| { + let score = best_match(t, d.name()).map_or(0, |m| m.score()); + (d, score) + }) + .max_by_key(|(_, score)| *score) + // we already checked the list is not empty + .unwrap(); + if score > MIN_DEVICE_MATCH_SCORE { + device + } else { + crate::error::bail!("Could not find an iOS device matching {t}") + } + } else { + let index = if device_list.len() > 1 { + prompt::list( + concat!("Detected ", "iOS", " devices"), + device_list.iter(), + "device", + None, + "Device", + ) + .context("failed to prompt for device")? + } else { + 0 + }; + device_list.into_iter().nth(index).unwrap() + }; + println!( + "Detected connected device: {} with target {:?}", + device, + device.target().triple, + ); + + Ok(device) + } else { + crate::error::bail!("No connected iOS devices detected") + } +} + +#[derive(Default, Deserialize)] +struct InstalledRuntimesList { + runtimes: Vec, +} + +#[derive(Deserialize)] +struct InstalledRuntime { + name: String, +} + +fn simulator_prompt(env: &'_ Env, target: Option<&str>) -> Result { + let simulator_list = device::list_simulators(env).map_err(|cause| { + Error::GenericError(format!( + "Failed to detect connected iOS Simulator devices: {cause}" + )) + })?; + if !simulator_list.is_empty() { + let device = if let Some(t) = target { + let (device, score) = simulator_list + .into_iter() + .rev() + .map(|d| { + let score = best_match(t, d.name()).map_or(0, |m| m.score()); + (d, score) + }) + .max_by_key(|(_, score)| *score) + // we already checked the list is not empty + .unwrap(); + if score > MIN_DEVICE_MATCH_SCORE { + device + } else { + crate::error::bail!("Could not find an iOS Simulator matching {t}") + } + } else if simulator_list.len() > 1 { + let index = prompt::list( + concat!("Detected ", "iOS", " simulators"), + simulator_list.iter(), + "simulator", + None, + "Simulator", + ) + .context("failed to prompt for simulator")?; + simulator_list.into_iter().nth(index).unwrap() + } else { + simulator_list.into_iter().next().unwrap() + }; + Ok(device) + } else { + log::warn!("No available iOS Simulator detected"); + let install_ios = crate::helpers::prompts::confirm( + "Would you like to install the latest iOS runtime?", + Some(false), + ) + .unwrap_or_default(); + if install_ios { + duct::cmd("xcodebuild", ["-downloadPlatform", "iOS"]) + .stdout_file(os_pipe::dup_stdout().unwrap()) + .stderr_file(os_pipe::dup_stderr().unwrap()) + .run() + .map_err(|e| Error::CommandFailed { + command: "xcodebuild -downloadPlatform iOS".to_string(), + error: e, + })?; + return simulator_prompt(env, target); + } + crate::error::bail!("No available iOS Simulator detected") + } +} + +fn device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result> { + if let Ok(device) = connected_device_prompt(env, target) { + Ok(device) + } else { + let simulator = simulator_prompt(env, target)?; + log::info!("Starting simulator {}", simulator.name()); + simulator + .start_detached(env) + .context("failed to start simulator")?; + Ok(simulator.into()) + } +} + +fn ensure_ios_runtime_installed() -> Result<()> { + let installed_platforms_json = duct::cmd("xcrun", ["simctl", "list", "runtimes", "--json"]) + .read() + .map_err(|e| Error::CommandFailed { + command: "xcrun simctl list runtimes --json".to_string(), + error: e, + })?; + let installed_platforms: InstalledRuntimesList = + serde_json::from_str(&installed_platforms_json).unwrap_or_default(); + if !installed_platforms + .runtimes + .iter() + .any(|r| r.name.starts_with("iOS")) + { + log::warn!("iOS platform not installed"); + let install_ios = crate::helpers::prompts::confirm( + "Would you like to install the latest iOS runtime?", + Some(false), + ) + .unwrap_or_default(); + if install_ios { + duct::cmd("xcodebuild", ["-downloadPlatform", "iOS"]) + .stdout_file(os_pipe::dup_stdout().unwrap()) + .stderr_file(os_pipe::dup_stderr().unwrap()) + .run() + .map_err(|e| Error::CommandFailed { + command: "xcodebuild -downloadPlatform iOS".to_string(), + error: e, + })?; + } else { + crate::error::bail!("iOS platform not installed"); + } + } + Ok(()) +} + +fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> { + device_prompt(env, None).map(|device| device.target()).ok() +} + +fn open_and_wait(config: &AppleConfig, env: &Env) -> ! { + log::info!("Opening Xcode"); + if let Err(e) = os::open_file_with("Xcode", config.project_dir(), env) { + log::error!("{e}"); + } + loop { + sleep(Duration::from_secs(24 * 60 * 60)); + } +} + +fn inject_resources(config: &AppleConfig, tauri_config: &TauriConfig) -> Result<()> { + let asset_dir = config.project_dir().join(DEFAULT_ASSET_DIR); + create_dir_all(&asset_dir).fs_context("failed to create asset directory", asset_dir.clone())?; + + let resources = match &tauri_config.bundle.resources { + Some(BundleResources::List(paths)) => Some(ResourcePaths::new(paths.as_slice(), true)), + Some(BundleResources::Map(map)) => Some(ResourcePaths::from_map(map, true)), + None => None, + }; + if let Some(resources) = resources { + for resource in resources.iter() { + let resource = resource.context("failed to get resource")?; + let dest = asset_dir.join(resource.target()); + crate::helpers::fs::copy_file(resource.path(), dest)?; + } + } + + Ok(()) +} + +pub fn signing_from_env() -> Result<( + Option, + Option, +)> { + let keychain = match ( + var_os("IOS_CERTIFICATE"), + var_os("IOS_CERTIFICATE_PASSWORD"), + ) { + (Some(certificate), Some(certificate_password)) => { + log::info!("Reading iOS certificates from "); + tauri_macos_sign::Keychain::with_certificate(&certificate, &certificate_password) + .map(Some) + .map_err(Box::new)? + } + (Some(_), None) => { + log::warn!( + "The IOS_CERTIFICATE environment variable is set but not IOS_CERTIFICATE_PASSWORD. Ignoring the certificate..." + ); + None + } + _ => None, + }; + + let provisioning_profile = if let Some(provisioning_profile) = var_os("IOS_MOBILE_PROVISION") { + tauri_macos_sign::ProvisioningProfile::from_base64(&provisioning_profile) + .map(Some) + .map_err(Box::new)? + } else { + if keychain.is_some() { + log::warn!( + "You have provided an iOS certificate via environment variables but the IOS_MOBILE_PROVISION environment variable is not set. This will fail when signing unless the profile is set in your Xcode project." + ); + } + None + }; + + Ok((keychain, provisioning_profile)) +} + +pub struct ProjectConfig { + pub code_sign_identity: Option, + pub team_id: Option, + pub provisioning_profile_uuid: Option, +} + +pub fn project_config( + keychain: Option<&tauri_macos_sign::Keychain>, + provisioning_profile: Option<&tauri_macos_sign::ProvisioningProfile>, +) -> Result { + Ok(ProjectConfig { + code_sign_identity: keychain.map(|k| k.signing_identity()), + team_id: keychain.and_then(|k| k.team_id().map(ToString::to_string)), + provisioning_profile_uuid: provisioning_profile.and_then(|p| p.uuid().ok()), + }) +} + +pub fn load_pbxproj(config: &AppleConfig) -> Result { + pbxproj::parse( + config + .project_dir() + .join(format!("{}.xcodeproj", config.app().name())) + .join("project.pbxproj"), + ) +} + +pub fn synchronize_project_config( + config: &AppleConfig, + tauri_config: &ConfigMetadata, + pbxproj: &mut pbxproj::Pbxproj, + export_options_plist: &mut plist::Dictionary, + project_config: &ProjectConfig, + debug: bool, +) -> Result<()> { + let identifier = tauri_config.identifier.clone(); + let product_name = tauri_config.product_name.clone(); + + let manual_signing = project_config.code_sign_identity.is_some() + || project_config.provisioning_profile_uuid.is_some(); + + if let Some(xc_configuration_list) = pbxproj + .xc_configuration_list + .clone() + .into_values() + .find(|l| l.comment.contains("_iOS")) + { + for build_configuration_ref in xc_configuration_list.build_configurations { + if manual_signing { + pbxproj.set_build_settings(&build_configuration_ref.id, "CODE_SIGN_STYLE", "Manual"); + } + + if let Some(team) = config.development_team() { + let team = format!("\"{team}\""); + pbxproj.set_build_settings(&build_configuration_ref.id, "DEVELOPMENT_TEAM", &team); + } + + pbxproj.set_build_settings( + &build_configuration_ref.id, + "PRODUCT_BUNDLE_IDENTIFIER", + &identifier, + ); + + if let Some(product_name) = &product_name { + pbxproj.set_build_settings( + &build_configuration_ref.id, + "PRODUCT_NAME", + &format!("\"{product_name}\""), + ); + } + + if let Some(identity) = &project_config.code_sign_identity { + let identity = format!("\"{identity}\""); + pbxproj.set_build_settings(&build_configuration_ref.id, "CODE_SIGN_IDENTITY", &identity); + pbxproj.set_build_settings( + &build_configuration_ref.id, + "\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\"", + &identity, + ); + } + + if let Some(id) = &project_config.team_id { + let id = format!("\"{id}\""); + pbxproj.set_build_settings(&build_configuration_ref.id, "DEVELOPMENT_TEAM", &id); + pbxproj.set_build_settings( + &build_configuration_ref.id, + "\"DEVELOPMENT_TEAM[sdk=iphoneos*]\"", + &id, + ); + } + + if let Some(profile_uuid) = &project_config.provisioning_profile_uuid { + let profile_uuid = format!("\"{profile_uuid}\""); + pbxproj.set_build_settings( + &build_configuration_ref.id, + "PROVISIONING_PROFILE_SPECIFIER", + &profile_uuid, + ); + pbxproj.set_build_settings( + &build_configuration_ref.id, + "\"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]\"", + &profile_uuid, + ); + } + } + } + + let build_configuration = { + if let Some(xc_configuration_list) = pbxproj + .xc_configuration_list + .clone() + .into_values() + .find(|l| l.comment.contains("_iOS")) + { + let mut configuration = None; + let target = if debug { "debug" } else { "release" }; + for build_configuration_ref in xc_configuration_list.build_configurations { + if build_configuration_ref.comments.contains(target) { + configuration = pbxproj + .xc_build_configuration + .get(&build_configuration_ref.id); + break; + } + } + + configuration + } else { + None + } + }; + + if let Some(build_configuration) = build_configuration { + if let Some(style) = build_configuration.get_build_setting("CODE_SIGN_STYLE") { + export_options_plist.insert( + "signingStyle".to_string(), + style.value.to_lowercase().into(), + ); + } else { + export_options_plist.insert("signingStyle".to_string(), "automatic".into()); + } + + if manual_signing { + if let Some(identity) = build_configuration + .get_build_setting("\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\"") + .or_else(|| build_configuration.get_build_setting("CODE_SIGN_IDENTITY")) + { + export_options_plist.insert( + "signingCertificate".to_string(), + identity.value.trim_matches('"').into(), + ); + } + + let profile_uuid = project_config + .provisioning_profile_uuid + .clone() + .or_else(|| { + build_configuration + .get_build_setting("\"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]\"") + .or_else(|| build_configuration.get_build_setting("PROVISIONING_PROFILE_SPECIFIER")) + .map(|setting| setting.value.trim_matches('"').to_string()) + }); + if let Some(profile_uuid) = profile_uuid { + let mut provisioning_profiles = plist::Dictionary::new(); + provisioning_profiles.insert(config.app().identifier().to_string(), profile_uuid.into()); + export_options_plist.insert( + "provisioningProfiles".to_string(), + provisioning_profiles.into(), + ); + } + } + + if let Some(id) = build_configuration + .get_build_setting("\"DEVELOPMENT_TEAM[sdk=iphoneos*]\"") + .or_else(|| build_configuration.get_build_setting("DEVELOPMENT_TEAM")) + { + export_options_plist.insert("teamID".to_string(), id.value.trim_matches('"').into()); + } + } + + Ok(()) +} diff --git a/crates/tauri-cli/src/mobile/ios/project.rs b/crates/tauri-cli/src/mobile/ios/project.rs new file mode 100644 index 0000000..d70d3bc --- /dev/null +++ b/crates/tauri-cli/src/mobile/ios/project.rs @@ -0,0 +1,239 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + Error, ErrorExt, Result, + error::Context, + helpers::{config::Config as TauriConfig, template}, + mobile::ios::LIB_OUTPUT_FILE_NAME, +}; +use cargo_mobile2::{ + apple::{ + config::{Config, Metadata}, + deps, + target::Target, + }, + config::app::DEFAULT_ASSET_DIR, + target::TargetTrait as _, + util::{self, cli::TextWrapper}, +}; +use handlebars::Handlebars; +use include_dir::{Dir, include_dir}; +use std::{ + ffi::OsString, + fs::{OpenOptions, create_dir_all}, + path::{Component, PathBuf}, +}; + +const TEMPLATE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/mobile/ios"); + +// unprefixed app_root seems pretty dangerous!! +// TODO: figure out what cargo-mobile meant by that +#[allow(clippy::too_many_arguments)] +pub fn generate( + tauri_config: &TauriConfig, + config: &Config, + metadata: &Metadata, + (handlebars, mut map): (Handlebars, template::JsonMap), + wrapper: &TextWrapper, + non_interactive: bool, + reinstall_deps: bool, + skip_targets_install: bool, +) -> Result<()> { + if !skip_targets_install { + let installed_targets = + crate::interface::rust::installation::installed_targets().unwrap_or_default(); + let missing_targets = Target::all() + .values() + .filter(|t| !installed_targets.contains(&t.triple().into())) + .collect::>(); + + if !missing_targets.is_empty() { + log::info!("Installing iOS Rust targets..."); + for target in missing_targets { + log::info!("Installing target {}", target.triple()); + target.install().map_err(|error| Error::CommandFailed { + command: "rustup target add".to_string(), + error, + })?; + } + } + } + + deps::install_all(wrapper, non_interactive, true, reinstall_deps).map_err(|error| { + Error::CommandFailed { + command: "pod install".to_string(), + error: std::io::Error::other(error), + } + })?; + + let dest = config.project_dir(); + let rel_prefix = util::relativize_path(config.app().root_dir(), &dest); + let source_dirs = vec![rel_prefix.join("src")]; + + let asset_catalogs = metadata.ios().asset_catalogs().unwrap_or_default(); + let ios_pods = metadata.ios().pods().unwrap_or_default(); + let macos_pods = metadata.macos().pods().unwrap_or_default(); + + #[cfg(target_arch = "aarch64")] + let default_archs = ["arm64"]; + #[cfg(not(target_arch = "aarch64"))] + let default_archs = ["arm64", "x86_64"]; + + map.insert("lib-output-file-name", LIB_OUTPUT_FILE_NAME); + + map.insert("file-groups", &source_dirs); + map.insert("ios-frameworks", metadata.ios().frameworks()); + map.insert("ios-valid-archs", default_archs); + map.insert("ios-vendor-frameworks", metadata.ios().vendor_frameworks()); + map.insert("ios-vendor-sdks", metadata.ios().vendor_sdks()); + map.insert("macos-frameworks", metadata.macos().frameworks()); + map.insert( + "macos-vendor-frameworks", + metadata.macos().vendor_frameworks(), + ); + map.insert("macos-vendor-sdks", metadata.macos().vendor_frameworks()); + map.insert("asset-catalogs", asset_catalogs); + map.insert("ios-pods", ios_pods); + map.insert("macos-pods", macos_pods); + map.insert( + "ios-additional-targets", + metadata.ios().additional_targets(), + ); + map.insert( + "macos-additional-targets", + metadata.macos().additional_targets(), + ); + map.insert("ios-pre-build-scripts", metadata.ios().pre_build_scripts()); + map.insert( + "ios-post-compile-scripts", + metadata.ios().post_compile_scripts(), + ); + map.insert( + "ios-post-build-scripts", + metadata.ios().post_build_scripts(), + ); + map.insert( + "macos-pre-build-scripts", + metadata.macos().pre_build_scripts(), + ); + map.insert( + "macos-post-compile-scripts", + metadata.macos().post_compile_scripts(), + ); + map.insert( + "macos-post-build-scripts", + metadata.macos().post_build_scripts(), + ); + map.insert( + "ios-command-line-arguments", + metadata.ios().command_line_arguments(), + ); + map.insert( + "macos-command-line-arguments", + metadata.macos().command_line_arguments(), + ); + + let mut created_dirs = Vec::new(); + template::render_with_generator( + &handlebars, + map.inner(), + &TEMPLATE_DIR, + &dest, + &mut |path| { + let mut components: Vec<_> = path.components().collect(); + let mut new_component = None; + for component in &mut components { + if let Component::Normal(c) = component { + let c = c.to_string_lossy(); + if c.contains("{{app.name}}") { + new_component.replace(OsString::from( + &c.replace("{{app.name}}", config.app().name()), + )); + *component = Component::Normal(new_component.as_ref().unwrap()); + break; + } + } + } + let path = dest.join(components.iter().collect::()); + + let parent = path.parent().unwrap().to_path_buf(); + if !created_dirs.contains(&parent) { + create_dir_all(&parent)?; + created_dirs.push(parent); + } + + let mut options = OpenOptions::new(); + options.write(true); + + if !path.exists() { + options.create(true).open(path).map(Some) + } else { + Ok(None) + } + }, + ) + .with_context(|| "failed to process template")?; + + if let Some(template_path) = tauri_config.bundle.ios.template.as_ref() { + let template = std::fs::read_to_string(template_path).fs_context( + "failed to read custom Xcode project template", + template_path.to_path_buf(), + )?; + let mut output_file = std::fs::File::create(dest.join("project.yml")).fs_context( + "failed to create project.yml file", + dest.join("project.yml"), + )?; + handlebars + .render_template_to_write(&template, map.inner(), &mut output_file) + .expect("Failed to render template"); + } + + let mut dirs_to_create = asset_catalogs.to_vec(); + dirs_to_create.push(dest.join(DEFAULT_ASSET_DIR)); + dirs_to_create.push(dest.join("Externals")); + dirs_to_create.push(dest.join(format!("{}_iOS", config.app().name()))); + + // Create all required project directories if they don't already exist + for dir in &dirs_to_create { + std::fs::create_dir_all(dir).fs_context("failed to create directory", dir.to_path_buf())?; + } + + // Note that Xcode doesn't always reload the project nicely; reopening is + // often necessary. + println!("Generating Xcode project..."); + duct::cmd( + "xcodegen", + [ + "generate", + "--spec", + &dest.join("project.yml").to_string_lossy(), + ], + ) + .stdout_file(os_pipe::dup_stdout().unwrap()) + .stderr_file(os_pipe::dup_stderr().unwrap()) + .run() + .map_err(|error| Error::CommandFailed { + command: "xcodegen".to_string(), + error, + })?; + + if !ios_pods.is_empty() || !macos_pods.is_empty() { + duct::cmd( + "pod", + [ + "install", + &format!("--project-directory={}", dest.display()), + ], + ) + .stdout_file(os_pipe::dup_stdout().unwrap()) + .stderr_file(os_pipe::dup_stderr().unwrap()) + .run() + .map_err(|error| Error::CommandFailed { + command: "pod install".to_string(), + error, + })?; + } + Ok(()) +} diff --git a/crates/tauri-cli/src/mobile/ios/run.rs b/crates/tauri-cli/src/mobile/ios/run.rs new file mode 100644 index 0000000..a40da84 --- /dev/null +++ b/crates/tauri-cli/src/mobile/ios/run.rs @@ -0,0 +1,144 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +use cargo_mobile2::opts::{NoiseLevel, Profile}; +use clap::{ArgAction, Parser}; + +use super::{device_prompt, env}; +use crate::{ + ConfigValue, Result, + error::Context, + helpers::config::{ConfigMetadata, get_config as get_tauri_config}, + interface::{DevProcess, WatcherOptions}, + mobile::{DevChild, TargetDevice}, +}; + +#[derive(Debug, Clone, Parser)] +#[clap( + about = "Run your app in production mode on iOS", + long_about = "Run your app in production mode on iOS. It makes use of the `build.frontendDist` property from your `tauri.conf.json` file. It also runs your `build.beforeBuildCommand` which usually builds your frontend into `build.frontendDist`." +)] +pub struct Options { + /// Run the app in release mode + #[clap(short, long)] + pub release: bool, + /// List of cargo features to activate + #[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')] + pub features: Vec, + /// JSON strings or paths to JSON, JSON5 or TOML files to merge with the default configuration file + /// + /// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts. + /// + /// Note that a platform-specific file is looked up and merged with the default file by default + /// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json) + /// but you can use this for more specific use cases such as different build flavors. + #[clap(short, long)] + pub config: Vec, + /// Disable the file watcher + #[clap(long)] + pub no_watch: bool, + /// Additional paths to watch for changes. + #[clap(long)] + pub additional_watch_folders: Vec, + /// Open Xcode + #[clap(short, long)] + pub open: bool, + /// Runs on the given device name + pub device: Option, + /// Command line arguments passed to the runner. + /// Use `--` to explicitly mark the start of the arguments. + /// e.g. `tauri android build -- [runnerArgs]`. + #[clap(last(true))] + pub args: Vec, + /// Do not error out if a version mismatch is detected on a Tauri package. + /// + /// 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, +} + +pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { + let env = env().context("failed to load iOS environment")?; + let device = if options.open { + None + } else { + match device_prompt(&env, options.device.as_deref()) { + Ok(d) => Some(d), + Err(e) => { + log::error!("{e}"); + None + } + } + }; + + let dirs = crate::helpers::app_paths::resolve_dirs(); + + let mut built_application = super::build::run( + super::build::Options { + debug: !options.release, + targets: Some(vec![]), /* skips IPA build since there's no target */ + features: Vec::new(), + config: options.config.clone(), + build_number: None, + open: options.open, + ci: false, + 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(), + }), + }, + noise_level, + &dirs, + )?; + + let mut tauri_config = get_tauri_config( + tauri_utils::platform::Target::Ios, + &options.config.iter().map(|c| &c.0).collect::>(), + dirs.tauri, + )?; + + // options.open is handled by the build command + // so all we need to do here is run the app on the selected device + if let Some(device) = device { + let runner = move |_tauri_config: &ConfigMetadata| { + device + .run( + &built_application.config, + &env, + noise_level, + false, // do not quit on app exit + if !options.release { + Profile::Debug + } else { + Profile::Release + }, + ) + .map(|c| Box::new(DevChild::new(c)) as Box) + .context("failed to run iOS app") + }; + + if options.no_watch { + runner(&tauri_config)?; + } else { + built_application.interface.watch( + &mut tauri_config, + WatcherOptions { + config: options.config, + additional_watch_folders: options.additional_watch_folders, + }, + runner, + &dirs, + )?; + } + } + + Ok(()) +} diff --git a/crates/tauri-cli/src/mobile/ios/xcode_script.rs b/crates/tauri-cli/src/mobile/ios/xcode_script.rs new file mode 100644 index 0000000..64b2efe --- /dev/null +++ b/crates/tauri-cli/src/mobile/ios/xcode_script.rs @@ -0,0 +1,354 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{MobileTarget, ensure_init, env, get_app, get_config, read_options}; +use crate::{ + Error, Result, + error::{Context, ErrorExt}, + helpers::config::{get_config as get_tauri_config, reload_config as reload_tauri_config}, + interface::{AppInterface, Options as InterfaceOptions}, + mobile::ios::LIB_OUTPUT_FILE_NAME, +}; + +use cargo_mobile2::{apple::target::Target, opts::Profile, target::TargetTrait}; +use clap::{ArgAction, Parser}; +use object::{Object, ObjectSymbol}; + +use std::{ + collections::HashMap, + env::{current_dir, set_current_dir, var, var_os}, + ffi::OsStr, + fs::read_to_string, + io::Read, + path::{Path, PathBuf}, +}; + +#[derive(Debug, Parser)] +pub struct Options { + /// Value of `PLATFORM_DISPLAY_NAME` env var + #[clap(long)] + platform: String, + /// Value of `SDKROOT` env var + #[clap(long)] + sdk_root: PathBuf, + /// Value of `FRAMEWORK_SEARCH_PATHS` env var + #[clap(long, action = ArgAction::Append, num_args(0..))] + framework_search_paths: Vec, + /// Value of `GCC_PREPROCESSOR_DEFINITIONS` env var + #[clap(long, action = ArgAction::Append, num_args(0..))] + gcc_preprocessor_definitions: Vec, + /// Value of `HEADER_SEARCH_PATHS` env var + #[clap(long, action = ArgAction::Append, num_args(0..))] + header_search_paths: Vec, + /// Value of `CONFIGURATION` env var + #[clap(long)] + configuration: String, + /// Value of `FORCE_COLOR` env var + #[clap(long)] + force_color: bool, + /// Value of `ARCHS` env var + #[clap(index = 1, required = true)] + arches: Vec, +} + +pub fn command(options: Options) -> Result<()> { + fn macos_from_platform(platform: &str) -> bool { + platform == "macOS" + } + + fn profile_from_configuration(configuration: &str) -> Profile { + if configuration == "release" { + Profile::Release + } else { + Profile::Debug + } + } + + let process_path = std::env::current_exe().unwrap_or_default(); + + // `xcode-script` is ran from the `gen/apple` folder when not using NPM/yarn/pnpm/deno. + // so we must change working directory to the src-tauri folder to resolve the tauri dir + // additionally, bun@<1.2 does not modify the current working directory, so it is also runs this script from `gen/apple` + // bun@>1.2 now actually moves the CWD to the package root so we shouldn't modify CWD in that case + // see https://bun.sh/blog/bun-v1.2#bun-run-uses-the-correct-directory + if (var_os("npm_lifecycle_event").is_none() + && var_os("PNPM_PACKAGE_NAME").is_none() + && process_path.file_stem().unwrap_or_default() != "deno") + || var("npm_config_user_agent") + .is_ok_and(|agent| agent.starts_with("bun/1.0") || agent.starts_with("bun/1.1")) + { + set_current_dir( + current_dir() + .context("failed to resolve current directory")? + .parent() + .unwrap() + .parent() + .unwrap(), + ) + .unwrap(); + } + + let dirs = crate::helpers::app_paths::resolve_dirs(); + + let profile = profile_from_configuration(&options.configuration); + let macos = macos_from_platform(&options.platform); + + let mut tauri_config = get_tauri_config(tauri_utils::platform::Target::Ios, &[], dirs.tauri)?; + let cli_options = read_options(&tauri_config); + if !cli_options.config.is_empty() { + // reload config with merges from the ios dev|build script + reload_tauri_config( + &mut tauri_config, + &cli_options + .config + .iter() + .map(|conf| &conf.0) + .collect::>(), + dirs.tauri, + )? + }; + + let (config, metadata) = get_config( + &get_app( + MobileTarget::Ios, + &tauri_config, + &AppInterface::new(&tauri_config, None, dirs.tauri)?, + dirs.tauri, + ), + &tauri_config, + &[], + &cli_options, + dirs.tauri, + )?; + + ensure_init( + &tauri_config, + config.app(), + config.project_dir(), + MobileTarget::Ios, + std::env::var("CI").is_ok(), + )?; + + if !cli_options.config.is_empty() { + crate::helpers::config::merge_config_with( + &mut tauri_config, + &cli_options + .config + .iter() + .map(|conf| &conf.0) + .collect::>(), + )?; + } + + let env = env() + .context("failed to load iOS environment")? + .explicit_env_vars(cli_options.vars); + + if !options.sdk_root.is_dir() { + crate::error::bail!( + "SDK root provided by Xcode was invalid. {} doesn't exist or isn't a directory", + options.sdk_root.display(), + ); + } + let include_dir = options.sdk_root.join("usr/include"); + if !include_dir.is_dir() { + crate::error::bail!( + "Include dir was invalid. {} doesn't exist or isn't a directory", + include_dir.display() + ); + } + + // Host flags that are used by build scripts + let macos_isysroot = { + let macos_sdk_root = options + .sdk_root + .join("../../../../MacOSX.platform/Developer/SDKs/MacOSX.sdk"); + if !macos_sdk_root.is_dir() { + crate::error::bail!("Invalid SDK root {}", macos_sdk_root.display()); + } + format!("-isysroot {}", macos_sdk_root.display()) + }; + + let mut host_env = HashMap::<&str, &OsStr>::new(); + + host_env.insert("RUST_BACKTRACE", "1".as_ref()); + + host_env.insert("CFLAGS_x86_64_apple_darwin", macos_isysroot.as_ref()); + host_env.insert("CXXFLAGS_x86_64_apple_darwin", macos_isysroot.as_ref()); + + host_env.insert( + "OBJC_INCLUDE_PATH_x86_64_apple_darwin", + include_dir.as_os_str(), + ); + + let framework_search_paths = options.framework_search_paths.join(" "); + host_env.insert("FRAMEWORK_SEARCH_PATHS", framework_search_paths.as_ref()); + + let gcc_preprocessor_definitions = options.gcc_preprocessor_definitions.join(" "); + host_env.insert( + "GCC_PREPROCESSOR_DEFINITIONS", + gcc_preprocessor_definitions.as_ref(), + ); + + let header_search_paths = options.header_search_paths.join(" "); + host_env.insert("HEADER_SEARCH_PATHS", header_search_paths.as_ref()); + + let macos_target = Target::macos(); + + let isysroot = format!("-isysroot {}", options.sdk_root.display()); + + let simulator = + options.platform == "iOS Simulator" || options.arches.contains(&"Simulator".to_string()); + let arches = if simulator { + // when compiling for the simulator, we don't need to build other targets + vec![ + if cfg!(target_arch = "aarch64") { + "arm64" + } else { + "x86_64" + } + .to_string(), + ] + } else { + options.arches + }; + + let installed_targets = + crate::interface::rust::installation::installed_targets().unwrap_or_default(); + + for arch in arches { + // Set target-specific flags + let (env_triple, rust_triple) = match arch.as_str() { + "arm64" if !simulator => ("aarch64_apple_ios", "aarch64-apple-ios"), + "arm64" if simulator => ("aarch64_apple_ios_sim", "aarch64-apple-ios-sim"), + "x86_64" => ("x86_64_apple_ios", "x86_64-apple-ios"), + _ => { + crate::error::bail!("Arch specified by Xcode was invalid. {arch} isn't a known arch") + } + }; + + let interface = AppInterface::new(&tauri_config, Some(rust_triple.into()), dirs.tauri)?; + + let cflags = format!("CFLAGS_{env_triple}"); + let cxxflags = format!("CFLAGS_{env_triple}"); + let objc_include_path = format!("OBJC_INCLUDE_PATH_{env_triple}"); + let mut target_env = host_env.clone(); + target_env.insert(cflags.as_ref(), isysroot.as_ref()); + target_env.insert(cxxflags.as_ref(), isysroot.as_ref()); + target_env.insert(objc_include_path.as_ref(), include_dir.as_ref()); + + let target = if macos { + &macos_target + } else { + Target::for_arch(if arch == "arm64" && simulator { + "arm64-sim" + } else { + &arch + }) + .with_context(|| format!("Arch specified by Xcode was invalid. {arch} isn't a known arch"))? + }; + + if !installed_targets.contains(&rust_triple.into()) { + log::info!("Installing target {}", target.triple()); + target.install().map_err(|error| Error::CommandFailed { + command: "rustup target add".to_string(), + error, + })?; + } + + target + .compile_lib( + &config, + &metadata, + cli_options.noise_level, + true, + profile, + &env, + target_env, + ) + .context("failed to compile iOS app")?; + + let out_dir = interface.app_settings().out_dir( + &InterfaceOptions { + debug: matches!(profile, Profile::Debug), + target: Some(rust_triple.into()), + ..Default::default() + }, + dirs.tauri, + )?; + + let lib_path = out_dir.join(format!("lib{}.a", config.app().lib_name())); + if !lib_path.exists() { + crate::error::bail!( + "Library not found at {}. Make sure your Cargo.toml file has a [lib] block with `crate-type = [\"staticlib\", \"cdylib\", \"lib\"]`", + lib_path.display() + ); + } + + validate_lib(&lib_path)?; + + let project_dir = config.project_dir(); + let externals_lib_dir = project_dir.join(format!("Externals/{arch}/{}", profile.as_str())); + std::fs::create_dir_all(&externals_lib_dir).fs_context( + "failed to create externals lib directory", + externals_lib_dir.clone(), + )?; + + // backwards compatible lib output file name + let uses_new_lib_output_file_name = { + let pbxproj_path = project_dir + .join(format!("{}.xcodeproj", config.app().name())) + .join("project.pbxproj"); + let pbxproj_contents = read_to_string(&pbxproj_path) + .fs_context("failed to read project.pbxproj file", pbxproj_path)?; + + pbxproj_contents.contains(LIB_OUTPUT_FILE_NAME) + }; + + let lib_output_file_name = if uses_new_lib_output_file_name { + LIB_OUTPUT_FILE_NAME.to_string() + } else { + format!("lib{}.a", config.app().lib_name()) + }; + + std::fs::copy(&lib_path, externals_lib_dir.join(lib_output_file_name)).fs_context( + "failed to copy mobile lib file to Externals directory", + lib_path.to_path_buf(), + )?; + } + Ok(()) +} + +fn validate_lib(path: &Path) -> Result<()> { + let mut archive = ar::Archive::new( + std::fs::File::open(path).fs_context("failed to open mobile lib file", path.to_path_buf())?, + ); + // Iterate over all entries in the archive: + while let Some(entry) = archive.next_entry() { + let Ok(mut entry) = entry else { + continue; + }; + let mut obj_bytes = Vec::new(); + entry + .read_to_end(&mut obj_bytes) + .fs_context("failed to read mobile lib entry", path.to_path_buf())?; + + let file = object::File::parse(&*obj_bytes) + .map_err(std::io::Error::other) + .fs_context("failed to parse mobile lib entry", path.to_path_buf())?; + for symbol in file.symbols() { + let Ok(name) = symbol.name() else { + continue; + }; + if name.contains("start_app") { + return Ok(()); + } + } + } + + crate::error::bail!( + "Library from {} does not include required runtime symbols. This means you are likely missing the tauri::mobile_entry_point macro usage, see the documentation for more information: https://v2.tauri.app/start/migrate/from-tauri-1", + path.display() + ) +} diff --git a/crates/tauri-cli/src/mobile/mod.rs b/crates/tauri-cli/src/mobile/mod.rs new file mode 100644 index 0000000..826218c --- /dev/null +++ b/crates/tauri-cli/src/mobile/mod.rs @@ -0,0 +1,642 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + ConfigValue, Error, Result, + error::{Context, ErrorExt}, + helpers::config::{Config as TauriConfig, ConfigMetadata, reload_config}, + interface::{AppInterface, AppSettings, DevProcess, Options as InterfaceOptions}, +}; +use heck::ToSnekCase; +use jsonrpsee::core::client::{Client, ClientBuilder, ClientT}; +use jsonrpsee::server::{RpcModule, ServerBuilder, ServerHandle}; +use jsonrpsee_client_transport::ws::WsTransportClientBuilder; +use jsonrpsee_core::rpc_params; +use serde::{Deserialize, Serialize}; + +use cargo_mobile2::{ + ChildHandle, + config::app::{App, Raw as RawAppConfig}, + env::Error as EnvError, + opts::{NoiseLevel, Profile}, +}; +use std::{ + collections::HashMap, + env::{set_var, temp_dir}, + ffi::OsString, + fmt::{Display, Write}, + fs::{read_to_string, write}, + net::{AddrParseError, IpAddr, Ipv4Addr, SocketAddr}, + path::{Path, PathBuf}, + process::{ExitStatus, exit}, + str::FromStr, + sync::{ + Arc, OnceLock, + atomic::{AtomicBool, Ordering}, + }, +}; +use tokio::runtime::Runtime; + +#[cfg(not(windows))] +use cargo_mobile2::env::Env; +#[cfg(windows)] +use cargo_mobile2::os::Env; + +pub mod android; +mod init; +#[cfg(target_os = "macos")] +pub mod ios; + +const MIN_DEVICE_MATCH_SCORE: isize = 0; + +#[derive(Clone)] +pub struct DevChild { + child: Arc, + manually_killed_process: Arc, +} + +impl DevChild { + fn new(handle: ChildHandle) -> Self { + Self { + child: Arc::new(handle), + manually_killed_process: Default::default(), + } + } +} + +impl DevProcess for DevChild { + fn kill(&self) -> std::io::Result<()> { + self.child.kill()?; + self.manually_killed_process.store(true, Ordering::SeqCst); + Ok(()) + } + + fn wait(&self) -> std::io::Result { + self.child.wait().map(|o| o.status) + } + + fn manually_killed_process(&self) -> bool { + self.manually_killed_process.load(Ordering::SeqCst) + } +} + +#[derive(PartialEq, Eq, Copy, Clone)] +pub enum Target { + Android, + #[cfg(target_os = "macos")] + Ios, +} + +impl Target { + fn ide_name(&self) -> &'static str { + match self { + Self::Android => "Android Studio", + #[cfg(target_os = "macos")] + Self::Ios => "Xcode", + } + } + + fn command_name(&self) -> &'static str { + match self { + Self::Android => "android", + #[cfg(target_os = "macos")] + Self::Ios => "ios", + } + } + + fn ide_build_script_name(&self) -> &'static str { + match self { + Self::Android => "android-studio-script", + #[cfg(target_os = "macos")] + Self::Ios => "xcode-script", + } + } + + fn platform_target(&self) -> tauri_utils::platform::Target { + match self { + Self::Android => tauri_utils::platform::Target::Android, + #[cfg(target_os = "macos")] + Self::Ios => tauri_utils::platform::Target::Ios, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TargetDevice { + id: String, + name: String, +} + +#[derive(Debug, Clone)] +pub struct DevHost(Option>); + +impl FromStr for DevHost { + type Err = AddrParseError; + fn from_str(s: &str) -> std::result::Result { + if s.is_empty() || s == "" { + Ok(Self(Some(None))) + } else if s == "" { + Ok(Self(None)) + } else { + IpAddr::from_str(s).map(|addr| Self(Some(Some(addr)))) + } + } +} + +impl Display for DevHost { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + Some(None) => write!(f, ""), + Some(Some(addr)) => write!(f, "{addr}"), + None => write!(f, ""), + } + } +} + +impl Default for DevHost { + fn default() -> Self { + // on Windows we want to force using the public network address for the development server + // because the adb port forwarding does not work well + if cfg!(windows) { + Self(Some(None)) + } else { + Self(None) + } + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct CliOptions { + pub dev: bool, + pub features: Vec, + pub args: Vec, + pub noise_level: NoiseLevel, + pub vars: HashMap, + pub config: Vec, + pub target_device: Option, +} + +fn local_ip_address(force: bool) -> &'static IpAddr { + static LOCAL_IP: OnceLock = OnceLock::new(); + LOCAL_IP.get_or_init(|| { + let prompt_for_ip = || { + let addresses: Vec = local_ip_address::list_afinet_netifas() + .expect("failed to list networks") + .into_iter() + .map(|(_, ipaddr)| ipaddr) + .filter(|ipaddr| match ipaddr { + IpAddr::V4(i) => i != &Ipv4Addr::LOCALHOST, + IpAddr::V6(i) => i.to_string().ends_with("::2"), + + }) + .collect(); + match addresses.as_slice() { + [] => panic!("No external IP detected."), + [ipaddr] => *ipaddr, + _ => { + let selected = dialoguer::Select::with_theme(&dialoguer::theme::ColorfulTheme::default()) + .with_prompt( + "Failed to detect external IP, What IP should we use to access your development server?", + ) + .items(&addresses) + .default(0) + .interact() + .expect("failed to select external IP"); + *addresses.get(selected).unwrap() + } + } + }; + + let ip = if force { + prompt_for_ip() + } else { + local_ip_address::local_ip().unwrap_or_else(|_| prompt_for_ip()) + }; + log::info!("Using {ip} to access the development server."); + ip + }) +} + +struct DevUrlConfig { + no_dev_server_wait: bool, +} + +fn use_network_address_for_dev_url( + config: &mut ConfigMetadata, + dev_options: &mut crate::dev::Options, + force_ip_prompt: bool, + tauri_dir: &Path, +) -> crate::Result { + let mut dev_url = config.build.dev_url.clone(); + + let ip = if let Some(url) = &mut dev_url { + let localhost = match url.host() { + Some(url::Host::Domain(d)) => d == "localhost", + Some(url::Host::Ipv4(i)) => i == Ipv4Addr::LOCALHOST || i == Ipv4Addr::UNSPECIFIED, + _ => false, + }; + + if localhost { + let ip = dev_options + .host + .unwrap_or_else(|| *local_ip_address(force_ip_prompt)); + log::info!( + "Replacing devUrl host with {ip}. {}.", + "If your frontend is not listening on that address, try configuring your development server to use the `TAURI_DEV_HOST` environment variable or 0.0.0.0 as host" + ); + + let url_str = format!( + "{}://{}{}", + url.scheme(), + SocketAddr::new(ip, url.port_or_known_default().unwrap()), + url.path() + ); + *url = + url::Url::parse(&url_str).with_context(|| format!("failed to parse URL: {url_str}"))?; + + dev_options + .config + .push(crate::ConfigValue(serde_json::json!({ + "build": { + "devUrl": url + } + }))); + + reload_config( + config, + &dev_options + .config + .iter() + .map(|conf| &conf.0) + .collect::>(), + tauri_dir, + )?; + + Some(ip) + } else { + None + } + } else if !dev_options.no_dev_server { + let ip = dev_options + .host + .unwrap_or_else(|| *local_ip_address(force_ip_prompt)); + dev_options.host.replace(ip); + Some(ip) + } else { + None + }; + + let mut dev_url_config = DevUrlConfig { + no_dev_server_wait: false, + }; + + if let Some(ip) = ip { + unsafe { std::env::set_var("TAURI_DEV_HOST", ip.to_string()) }; + unsafe { std::env::set_var("TRUNK_SERVE_ADDRESS", ip.to_string()) }; + if ip.is_ipv6() { + // in this case we can't ping the server for some reason + dev_url_config.no_dev_server_wait = true; + } + } + + Ok(dev_url_config) +} + +fn env_vars() -> HashMap { + let mut vars = HashMap::new(); + vars.insert("RUST_LOG_STYLE".into(), "always".into()); + for (k, v) in std::env::vars_os() { + let k = k.to_string_lossy(); + if (k.starts_with("TAURI") + && k != "TAURI_SIGNING_PRIVATE_KEY" + && k != "TAURI_SIGNING_PRIVATE_KEY_PASSWORD") + || k.starts_with("WRY") + || k.starts_with("CARGO_") + || k.starts_with("RUST_") + || k == "TMPDIR" + || k == "PATH" + { + vars.insert(k.into_owned(), v); + } + } + vars +} + +fn env() -> std::result::Result { + let env = Env::new()?.explicit_env_vars(env_vars()); + Ok(env) +} + +pub struct OptionsHandle(#[allow(unused)] Runtime, #[allow(unused)] ServerHandle); + +/// Writes CLI options to be used later on the Xcode and Android Studio build commands +pub fn write_options( + config: &ConfigMetadata, + mut options: CliOptions, +) -> crate::Result { + options.vars.extend(env_vars()); + + let runtime = Runtime::new().unwrap(); + let r: crate::Result<(ServerHandle, SocketAddr)> = runtime.block_on(async move { + let server = ServerBuilder::default() + .build("127.0.0.1:0") + .await + .context("failed to build WebSocket server")?; + let addr = server.local_addr().context("failed to get local address")?; + + let mut module = RpcModule::new(()); + module + .register_method("options", move |_, _, _| Some(options.clone())) + .context("failed to register options method")?; + + let handle = server.start(module); + + Ok((handle, addr)) + }); + let (handle, addr) = r?; + + let server_addr_path = temp_dir().join(format!( + "{}-server-addr", + config + .original_identifier() + .context("app configuration is missing an identifier")? + )); + + write(&server_addr_path, addr.to_string()) + .fs_context("failed to write server address file", server_addr_path)?; + + Ok(OptionsHandle(runtime, handle)) +} + +fn read_options(config: &ConfigMetadata) -> CliOptions { + let runtime = tokio::runtime::Runtime::new().unwrap(); + let options = runtime + .block_on(async move { + let addr_path = temp_dir().join(format!( + "{}-server-addr", + config + .original_identifier() + .context("app configuration is missing an identifier")? + )); + let (tx, rx) = WsTransportClientBuilder::default() + .build( + format!( + "ws://{}", + read_to_string(&addr_path).unwrap_or_else(|e| panic!( + "failed to read missing addr file {}: {e}", + addr_path.display() + )) + ) + .parse() + .unwrap(), + ) + .await + .context("failed to build WebSocket client")?; + let client: Client = ClientBuilder::default().build_with_tokio(tx, rx); + let options: CliOptions = client + .request("options", rpc_params![]) + .await + .context("failed to request options")?; + Ok::(options) + }) + .expect("failed to read CLI options"); + + for (k, v) in &options.vars { + unsafe { set_var(k, v) }; + } + options +} + +pub fn get_app( + target: Target, + config: &TauriConfig, + interface: &AppInterface, + tauri_dir: &Path, +) -> App { + let identifier = match target { + Target::Android => config.identifier.replace('-', "_"), + #[cfg(target_os = "macos")] + Target::Ios => config.identifier.replace('_', "-"), + }; + + if identifier.is_empty() { + log::error!("Bundle identifier set in `tauri.conf.json > identifier` cannot be empty"); + exit(1); + } + + let app_name = interface + .app_settings() + .app_name() + .unwrap_or_else(|| "app".into()); + let lib_name = interface + .app_settings() + .lib_name() + .unwrap_or_else(|| app_name.to_snek_case()); + + if config.product_name.is_none() { + log::warn!( + "`productName` is not set in the Tauri configuration. Using `{app_name}` as the app name." + ); + } + + let raw = RawAppConfig { + name: app_name, + lib_name: Some(lib_name), + stylized_name: config.product_name.clone(), + identifier, + asset_dir: None, + template_pack: None, + }; + + let app_settings = interface.app_settings(); + let tauri_dir = tauri_dir.to_path_buf(); + App::from_raw(tauri_dir.to_path_buf(), raw) + .unwrap() + .with_target_dir_resolver(move |target, profile| { + app_settings + .out_dir( + &InterfaceOptions { + debug: matches!(profile, Profile::Debug), + target: Some(target.into()), + ..Default::default() + }, + &tauri_dir, + ) + .expect("failed to resolve target directory") + }) +} + +#[allow(unused_variables)] +fn ensure_init( + tauri_config: &ConfigMetadata, + app: &App, + project_dir: PathBuf, + target: Target, + noninteractive: bool, +) -> Result<()> { + if !project_dir.exists() { + crate::error::bail!( + "{} project directory {} doesn't exist. Please run `tauri {} init` and try again.", + target.ide_name(), + project_dir.display(), + target.command_name(), + ) + } + + let mut project_outdated_reasons = Vec::new(); + + match target { + Target::Android => { + let java_folder = project_dir + .join("app/src/main/java") + .join(tauri_config.identifier.replace('.', "/").replace('-', "_")); + if java_folder.exists() { + ensure_gradlew(&project_dir)?; + } else { + project_outdated_reasons + .push("you have modified your \"identifier\" in the Tauri configuration"); + } + } + #[cfg(target_os = "macos")] + Target::Ios => { + let xcodeproj_path = crate::helpers::fs::find_in_directory(&project_dir, "*.xcodeproj") + .with_context(|| format!("failed to locate xcodeproj in {}", project_dir.display()))?; + + let xcodeproj_name = xcodeproj_path.file_stem().unwrap().to_str().unwrap(); + if xcodeproj_name != app.name() { + let rename_targets = vec![ + // first rename the entitlements + ( + format!("{xcodeproj_name}_iOS/{xcodeproj_name}_iOS.entitlements"), + format!("{xcodeproj_name}_iOS/{}_iOS.entitlements", app.name()), + ), + // then the scheme folder + ( + format!("{xcodeproj_name}_iOS"), + format!("{}_iOS", app.name()), + ), + ( + format!("{xcodeproj_name}.xcodeproj"), + format!("{}.xcodeproj", app.name()), + ), + ]; + let rename_info = rename_targets + .iter() + .map(|(from, to)| format!("- {from} to {to}")) + .collect::>() + .join("\n"); + log::error!( + "you have modified your package name from {current_project_name} to {new_project_name}\nWe need to apply the name change to the Xcode project, renaming:\n{rename_info}", + new_project_name = app.name(), + current_project_name = xcodeproj_name, + ); + if noninteractive { + project_outdated_reasons + .push("you have modified your [lib.name] or [package.name] in the Cargo.toml file"); + } else { + let confirm = crate::helpers::prompts::confirm( + "Do you want to apply the name change to the Xcode project?", + Some(true), + ) + .unwrap_or_default(); + if confirm { + for (from, to) in rename_targets { + std::fs::rename(project_dir.join(&from), project_dir.join(&to)) + .with_context(|| format!("failed to rename {from} to {to}"))?; + } + + // update scheme name in pbxproj + // identifier / product name are synchronized by the dev/build commands + let pbxproj_path = + project_dir.join(format!("{}.xcodeproj/project.pbxproj", app.name())); + let pbxproj_contents = std::fs::read_to_string(&pbxproj_path) + .with_context(|| format!("failed to read {}", pbxproj_path.display()))?; + std::fs::write( + &pbxproj_path, + pbxproj_contents.replace( + &format!("{xcodeproj_name}_iOS"), + &format!("{}_iOS", app.name()), + ), + ) + .with_context(|| format!("failed to write {}", pbxproj_path.display()))?; + } else { + project_outdated_reasons + .push("you have modified your [lib.name] or [package.name] in the Cargo.toml file"); + } + } + } + + // note: pbxproj is synchronied by the dev/build commands + } + } + + if !project_outdated_reasons.is_empty() { + let reason = project_outdated_reasons.join(" and "); + crate::error::bail!( + "{} project directory is outdated because {reason}. Please delete {}, run `tauri {} init` and try again.", + target.ide_name(), + project_dir.display(), + target.command_name(), + ) + } + + Ok(()) +} + +fn ensure_gradlew(project_dir: &std::path::Path) -> Result<()> { + let gradlew_path = project_dir.join("gradlew"); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + if let Ok(metadata) = gradlew_path.metadata() { + let mut permissions = metadata.permissions(); + let is_executable = permissions.mode() & 0o111 != 0; + if !is_executable { + permissions.set_mode(permissions.mode() | 0o111); + std::fs::set_permissions(&gradlew_path, permissions) + .fs_context("failed to mark gradlew as executable", &gradlew_path)?; + } + } + } + + // A gradlew with CRLF line endings cannot run: sh fails with + // "/usr/bin/env: 'sh\r': No such file or directory" or similar, which + // also happens under Git Bash on Windows, so the rewrite runs on all + // platforms (https://github.com/tauri-apps/tauri/pull/16017). Windows + // builds invoke gradlew.bat, so there a gradlew that cannot be + // rewritten only draws a warning; on unix the error is returned. + if gradlew_path.exists() { + let result = std::fs::read_to_string(&gradlew_path) + .fs_context("failed to read gradlew", &gradlew_path) + .and_then(|contents| { + if contents.contains("\r\n") { + std::fs::write(&gradlew_path, contents.replace("\r\n", "\n")) + .fs_context("failed to replace gradlew CRLF with LF", &gradlew_path) + } else { + Ok(()) + } + }); + #[cfg(unix)] + result?; + #[cfg(not(unix))] + if let Err(error) = result { + log::warn!("failed to normalize gradlew line endings: {error}"); + } + } + + Ok(()) +} + +fn log_finished(outputs: Vec, kind: &str) { + if !outputs.is_empty() { + let mut printable_paths = String::new(); + for path in &outputs { + writeln!(printable_paths, " {}", path.display()).unwrap(); + } + + log::info!(action = "Finished"; "{} {}{} at:\n{}", outputs.len(), kind, if outputs.len() == 1 { "" } else { "s" }, printable_paths); + } +} diff --git a/crates/tauri-cli/src/plugin/android.rs b/crates/tauri-cli/src/plugin/android.rs new file mode 100644 index 0000000..b1cb74a --- /dev/null +++ b/crates/tauri-cli/src/plugin/android.rs @@ -0,0 +1,141 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + Result, + error::Context, + helpers::{prompts, template}, +}; +use clap::{Parser, Subcommand}; +use handlebars::Handlebars; + +use std::{ + collections::BTreeMap, + env::current_dir, + ffi::OsStr, + path::{Component, PathBuf}, +}; + +#[derive(Parser)] +#[clap( + author, + version, + about = "Manage the Android project for a Tauri plugin", + subcommand_required(true), + arg_required_else_help(true) +)] +pub struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Init(InitOptions), +} + +#[derive(Debug, Parser)] +#[clap(about = "Initializes the Android project for an existing Tauri plugin")] +pub struct InitOptions { + /// Name of your Tauri plugin. Must match the current plugin's name. + /// If not specified, it will be inferred from the current directory. + plugin_name: Option, + /// The output directory. + #[clap(short, long)] + #[clap(default_value_t = current_dir().expect("failed to read cwd").to_string_lossy().into_owned())] + out_dir: String, +} + +pub fn command(cli: Cli) -> Result<()> { + match cli.command { + Commands::Init(options) => { + let plugin_name = match options.plugin_name { + None => super::infer_plugin_name( + std::env::current_dir().context("failed to get current directory")?, + )?, + Some(name) => name, + }; + + let out_dir = PathBuf::from(options.out_dir); + if out_dir.join("android").exists() { + crate::error::bail!("Android folder already exists"); + } + + let plugin_id = prompts::input( + "What should be the Android Package ID for your plugin?", + Some(format!("com.plugin.{plugin_name}")), + false, + false, + )? + .unwrap(); + + let handlebars = Handlebars::new(); + + let mut data = BTreeMap::new(); + super::init::plugin_name_data(&mut data, &plugin_name); + data.insert("android_package_id", handlebars::to_json(&plugin_id)); + + let mut created_dirs = Vec::new(); + template::render_with_generator( + &handlebars, + &data, + &super::init::TEMPLATE_DIR, + &out_dir, + &mut |path| { + let mut components = path.components(); + let root = components.next().unwrap(); + if let Component::Normal(component) = root { + if component == OsStr::new("android") { + return super::init::generate_android_out_file( + &path, + &out_dir, + &plugin_id.replace('.', "/"), + &mut created_dirs, + ); + } + } + + Ok(None) + }, + )?; + + let metadata = super::init::crates_metadata()?; + + let cargo_toml_addition = format!( + r#" +[build-dependencies] +tauri-build = "{}" +"#, + metadata.tauri_build + ); + let build_file = super::init::TEMPLATE_DIR + .get_file("build.rs") + .unwrap() + .contents_utf8() + .unwrap(); + let init_fn = format!( + r#" +pub fn init() -> TauriPlugin {{ + Builder::new("{plugin_name}") + .setup(|app, api| {{ + #[cfg(target_os = "android")] + let handle = api.register_android_plugin("{plugin_id}", "ExamplePlugin")?; + Ok(()) + }}) + .build() +}} +"# + ); + + log::info!("Android project added"); + println!("You must add the following to the Cargo.toml file:\n{cargo_toml_addition}",); + println!("You must add the following code to the build.rs file:\n\n{build_file}",); + println!( + "Your plugin's init function under src/lib.rs must initialize the Android plugin:\n{init_fn}" + ); + } + } + + Ok(()) +} diff --git a/crates/tauri-cli/src/plugin/init.rs b/crates/tauri-cli/src/plugin/init.rs new file mode 100644 index 0000000..718d847 --- /dev/null +++ b/crates/tauri-cli/src/plugin/init.rs @@ -0,0 +1,329 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::PluginIosFramework; +use crate::Result; +use crate::{ + VersionMetadata, + error::{Context, ErrorExt}, + helpers::{prompts, resolve_tauri_path, template}, +}; +use clap::Parser; +use handlebars::{Handlebars, to_json}; +use heck::{ToKebabCase, ToPascalCase, ToSnakeCase}; +use include_dir::{Dir, include_dir}; +use std::ffi::{OsStr, OsString}; +use std::{ + collections::BTreeMap, + env::current_dir, + fs::{File, OpenOptions, create_dir_all, remove_dir_all}, + path::{Component, Path, PathBuf}, +}; + +pub const TEMPLATE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/plugin"); + +#[derive(Debug, Parser)] +#[clap(about = "Initialize a Tauri plugin project on an existing directory")] +pub struct Options { + /// Name of your Tauri plugin. + /// If not specified, it will be inferred from the current directory. + pub(crate) plugin_name: Option, + /// Initializes a Tauri plugin without the TypeScript API + #[clap(long)] + pub(crate) no_api: bool, + /// Initialize without an example project. + #[clap(long)] + pub(crate) no_example: bool, + /// Set target directory for init + #[clap(short, long)] + #[clap(default_value_t = current_dir().expect("failed to read cwd").display().to_string())] + pub(crate) directory: String, + /// Author name + #[clap(short, long)] + pub(crate) author: Option, + /// Whether to initialize an Android project for the plugin. + #[clap(long)] + pub(crate) android: bool, + /// Whether to initialize an iOS project for the plugin. + #[clap(long)] + pub(crate) ios: bool, + /// Whether to initialize Android and iOS projects for the plugin. + #[clap(long)] + pub(crate) mobile: bool, + /// Type of framework to use for the iOS project. + #[clap(long)] + #[clap(default_value_t = PluginIosFramework::default())] + pub(crate) ios_framework: PluginIosFramework, + /// Generate github workflows + #[clap(long)] + pub(crate) github_workflows: bool, + + /// Initializes a Tauri core plugin (internal usage) + #[clap(long, hide(true))] + pub(crate) tauri: bool, + /// Path of the Tauri project to use (relative to the cwd) + #[clap(short, long)] + pub(crate) tauri_path: Option, +} + +impl Options { + fn load(&mut self) { + if self.author.is_none() { + self.author.replace(if self.tauri { + "Tauri Programme within The Commons Conservancy".into() + } else { + "You".into() + }); + } + } +} + +pub fn command(mut options: Options) -> Result<()> { + options.load(); + + let plugin_name = match options.plugin_name { + None => super::infer_plugin_name(&options.directory)?, + Some(name) => name, + }; + + let template_target_path = PathBuf::from(options.directory); + let metadata = crates_metadata()?; + if std::fs::read_dir(&template_target_path) + .fs_context( + "failed to read target directory", + template_target_path.clone(), + )? + .count() + > 0 + { + log::warn!("Plugin dir ({:?}) not empty.", template_target_path); + } else { + let (tauri_dep, tauri_example_dep, tauri_build_dep, tauri_plugin_dep) = + if let Some(tauri_path) = options.tauri_path { + ( + format!( + r#"{{ path = {:?} }}"#, + resolve_tauri_path(&tauri_path, "crates/tauri") + ), + format!( + r#"{{ path = {:?} }}"#, + resolve_tauri_path(&tauri_path, "crates/tauri") + ), + format!( + "{{ path = {:?}, default-features = false }}", + resolve_tauri_path(&tauri_path, "crates/tauri-build") + ), + format!( + r#"{{ path = {:?}, features = ["build"] }}"#, + resolve_tauri_path(&tauri_path, "crates/tauri-plugin") + ), + ) + } else { + ( + format!(r#"{{ version = "{}" }}"#, metadata.tauri), + format!(r#"{{ version = "{}" }}"#, metadata.tauri), + format!( + r#"{{ version = "{}", default-features = false }}"#, + metadata.tauri_build + ), + format!( + r#"{{ version = "{}", features = ["build"] }}"#, + metadata.tauri_plugin + ), + ) + }; + + let _ = remove_dir_all(&template_target_path); + let mut handlebars = Handlebars::new(); + handlebars.register_escape_fn(handlebars::no_escape); + + let mut data = BTreeMap::new(); + plugin_name_data(&mut data, &plugin_name); + data.insert("tauri_dep", to_json(tauri_dep)); + data.insert("tauri_example_dep", to_json(tauri_example_dep)); + data.insert("tauri_build_dep", to_json(tauri_build_dep)); + data.insert("tauri_plugin_dep", to_json(tauri_plugin_dep)); + data.insert("author", to_json(options.author)); + + if options.tauri { + data.insert( + "license_header", + to_json( + "// Copyright 2019-2024 Tauri Programme within The Commons Conservancy + // SPDX-License-Identifier: Apache-2.0 + // SPDX-License-Identifier: MIT\n\n" + .replace(" ", "") + .replace(" //", "//"), + ), + ); + } + + let plugin_id = if options.android || options.mobile { + let plugin_id = prompts::input( + "What should be the Android Package ID for your plugin?", + Some(format!("com.plugin.{plugin_name}")), + false, + false, + )? + .unwrap(); + + data.insert("android_package_id", to_json(&plugin_id)); + Some(plugin_id) + } else { + None + }; + + let ios_framework = options.ios_framework; + + let mut created_dirs = Vec::new(); + template::render_with_generator( + &handlebars, + &data, + &TEMPLATE_DIR, + &template_target_path, + &mut |mut path| { + let mut components = path.components(); + let root = components.next().unwrap(); + + if let Component::Normal(component) = root { + match component.to_str().unwrap() { + "__example-api" => { + if options.no_api || options.no_example { + return Ok(None); + } else { + path = Path::new("examples").join(components.collect::()); + } + } + "__example-basic" => { + if options.no_api && !options.no_example { + path = Path::new("examples").join(components.collect::()); + } else { + return Ok(None); + } + } + ".github" if !options.github_workflows => return Ok(None), + "android" => { + if options.android || options.mobile { + return generate_android_out_file( + &path, + &template_target_path, + &plugin_id.as_ref().unwrap().replace('.', "/"), + &mut created_dirs, + ); + } else { + return Ok(None); + } + } + "ios-spm" | "ios-xcode" if !(options.ios || options.mobile) => return Ok(None), + "ios-spm" if !matches!(ios_framework, PluginIosFramework::Spm) => return Ok(None), + "ios-xcode" if !matches!(ios_framework, PluginIosFramework::Xcode) => return Ok(None), + "ios-spm" | "ios-xcode" => { + let folder_name = components.next().unwrap().as_os_str().to_string_lossy(); + let new_folder_name = folder_name.replace("{{ plugin_name }}", &plugin_name); + let new_folder_name = OsString::from(&new_folder_name); + + path = [ + Component::Normal(OsStr::new("ios")), + Component::Normal(&new_folder_name), + ] + .into_iter() + .chain(components) + .collect::(); + } + "guest-js" | "rollup.config.js" | "tsconfig.json" | "package.json" + if options.no_api => + { + return Ok(None); + } + _ => (), + } + } + + let path = template_target_path.join(path); + let parent = path.parent().unwrap().to_path_buf(); + if !created_dirs.contains(&parent) { + create_dir_all(&parent)?; + created_dirs.push(parent); + } + File::create(path).map(Some) + }, + ) + .with_context(|| "failed to render plugin template")?; + } + + let permissions_dir = template_target_path.join("permissions"); + std::fs::create_dir(&permissions_dir).fs_context( + "failed to create `permissions` directory", + permissions_dir.clone(), + )?; + + let default_permissions = r#"[default] +description = "Default permissions for the plugin" +permissions = ["allow-ping"] +"#; + std::fs::write(permissions_dir.join("default.toml"), default_permissions).fs_context( + "failed to write default permissions file", + permissions_dir.join("default.toml"), + )?; + + Ok(()) +} + +pub fn plugin_name_data(data: &mut BTreeMap<&'static str, serde_json::Value>, plugin_name: &str) { + data.insert("plugin_name_original", to_json(plugin_name)); + data.insert("plugin_name", to_json(plugin_name.to_kebab_case())); + data.insert( + "plugin_name_snake_case", + to_json(plugin_name.to_snake_case()), + ); + data.insert( + "plugin_name_pascal_case", + to_json(plugin_name.to_pascal_case()), + ); +} + +pub fn crates_metadata() -> Result { + serde_json::from_str::(include_str!("../../metadata-v2.json")) + .context("failed to parse Tauri version metadata") +} + +pub fn generate_android_out_file( + path: &Path, + dest: &Path, + package_path: &str, + created_dirs: &mut Vec, +) -> std::io::Result> { + let mut iter = path.iter(); + let root = iter.next().unwrap().to_str().unwrap(); + let path = match (root, path.extension().and_then(|o| o.to_str())) { + ("src", Some("kt")) => { + let parent = path.parent().unwrap(); + let file_name = path.file_name().unwrap(); + let out_dir = dest.join(parent).join(package_path); + out_dir.join(file_name) + } + _ => dest.join(path), + }; + + let parent = path.parent().unwrap().to_path_buf(); + if !created_dirs.contains(&parent) { + create_dir_all(&parent)?; + created_dirs.push(parent); + } + + let mut options = OpenOptions::new(); + options.write(true); + + #[cfg(unix)] + if path.file_name().unwrap() == std::ffi::OsStr::new("gradlew") { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o755); + } + + if !path.exists() { + options.create(true).open(path).map(Some) + } else { + Ok(None) + } +} diff --git a/crates/tauri-cli/src/plugin/ios.rs b/crates/tauri-cli/src/plugin/ios.rs new file mode 100644 index 0000000..ebbf8e8 --- /dev/null +++ b/crates/tauri-cli/src/plugin/ios.rs @@ -0,0 +1,155 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::PluginIosFramework; +use crate::{Result, error::Context, helpers::template}; +use clap::{Parser, Subcommand}; +use handlebars::Handlebars; + +use std::{ + collections::BTreeMap, + env::current_dir, + ffi::{OsStr, OsString}, + fs::{File, create_dir_all}, + path::{Component, PathBuf}, +}; + +#[derive(Parser)] +#[clap( + author, + version, + about = "Manage the iOS project for a Tauri plugin", + subcommand_required(true), + arg_required_else_help(true) +)] +pub struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Init(InitOptions), +} + +#[derive(Debug, Parser)] +#[clap(about = "Initializes the iOS project for an existing Tauri plugin")] +pub struct InitOptions { + /// Name of your Tauri plugin. Must match the current plugin's name. + /// If not specified, it will be inferred from the current directory. + plugin_name: Option, + /// The output directory. + #[clap(short, long)] + #[clap(default_value_t = current_dir().expect("failed to read cwd").to_string_lossy().into_owned())] + out_dir: String, + /// Type of framework to use for the iOS project. + #[clap(long)] + #[clap(default_value_t = PluginIosFramework::default())] + ios_framework: PluginIosFramework, +} + +pub fn command(cli: Cli) -> Result<()> { + match cli.command { + Commands::Init(options) => { + let plugin_name = match options.plugin_name { + None => super::infer_plugin_name( + std::env::current_dir().context("failed to get current directory")?, + )?, + Some(name) => name, + }; + + let out_dir = PathBuf::from(options.out_dir); + if out_dir.join("ios").exists() { + crate::error::bail!("iOS folder already exists"); + } + + let handlebars = Handlebars::new(); + + let mut data = BTreeMap::new(); + super::init::plugin_name_data(&mut data, &plugin_name); + + let ios_folder_name = match options.ios_framework { + PluginIosFramework::Spm => OsStr::new("ios-spm"), + PluginIosFramework::Xcode => OsStr::new("ios-xcode"), + }; + + let mut created_dirs = Vec::new(); + template::render_with_generator( + &handlebars, + &data, + &super::init::TEMPLATE_DIR, + &out_dir, + &mut |path| { + let mut components = path.components(); + let root = components.next().unwrap(); + if let Component::Normal(component) = root { + if component == ios_folder_name { + let folder_name = components.next().unwrap().as_os_str().to_string_lossy(); + let new_folder_name = folder_name.replace("{{ plugin_name }}", &plugin_name); + let new_folder_name = OsString::from(&new_folder_name); + + let path = [ + Component::Normal(OsStr::new("ios")), + Component::Normal(&new_folder_name), + ] + .into_iter() + .chain(components) + .collect::(); + + let path = out_dir.join(path); + let parent = path.parent().unwrap().to_path_buf(); + if !created_dirs.contains(&parent) { + create_dir_all(&parent)?; + created_dirs.push(parent); + } + return File::create(path).map(Some); + } + } + + Ok(None) + }, + )?; + + let metadata = super::init::crates_metadata()?; + + let cargo_toml_addition = format!( + r#" +[build-dependencies] +tauri-build = "{}" +"#, + metadata.tauri_build + ); + let build_file = super::init::TEMPLATE_DIR + .get_file("build.rs") + .unwrap() + .contents_utf8() + .unwrap(); + let init_fn = format!( + r#" +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_{plugin_name}); + +pub fn init() -> TauriPlugin {{ + Builder::new("{plugin_name}") + .setup(|app| {{ + #[cfg(target_os = "ios")] + app.register_ios_plugin(init_plugin_{plugin_name})?; + Ok(()) + }}) + .build() +}} +"#, + ); + + log::info!("iOS project added"); + println!("You must add the following to the Cargo.toml file:\n{cargo_toml_addition}",); + println!("You must add the following code to the build.rs file:\n\n{build_file}",); + println!( + "Your plugin's init function under src/lib.rs must initialize the iOS plugin:\n{init_fn}" + ); + } + } + + Ok(()) +} diff --git a/crates/tauri-cli/src/plugin/mod.rs b/crates/tauri-cli/src/plugin/mod.rs new file mode 100644 index 0000000..8e87449 --- /dev/null +++ b/crates/tauri-cli/src/plugin/mod.rs @@ -0,0 +1,96 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{fmt::Display, path::Path}; + +use clap::{Parser, Subcommand, ValueEnum}; + +use crate::{ + Result, + error::{Context, ErrorExt}, +}; + +mod android; +mod init; +mod ios; +mod new; + +#[derive(Debug, Clone, ValueEnum, Default)] +pub enum PluginIosFramework { + /// Swift Package Manager project + #[default] + Spm, + /// Xcode project + Xcode, +} + +impl Display for PluginIosFramework { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Spm => write!(f, "spm"), + Self::Xcode => write!(f, "xcode"), + } + } +} + +#[derive(Parser)] +#[clap( + author, + version, + about = "Manage or create Tauri plugins", + subcommand_required(true), + arg_required_else_help(true) +)] +pub struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + New(new::Options), + Init(init::Options), + Android(android::Cli), + Ios(ios::Cli), +} + +pub fn command(cli: Cli) -> Result<()> { + match cli.command { + Commands::New(options) => new::command(options)?, + Commands::Init(options) => init::command(options)?, + Commands::Android(cli) => android::command(cli)?, + Commands::Ios(cli) => ios::command(cli)?, + } + + Ok(()) +} + +fn infer_plugin_name>(directory: P) -> Result { + let dir = directory.as_ref(); + let cargo_toml_path = dir.join("Cargo.toml"); + let name = if cargo_toml_path.exists() { + let contents = std::fs::read_to_string(&cargo_toml_path) + .fs_context("failed to read Cargo manifest", cargo_toml_path)?; + let cargo_toml: toml::Value = + toml::from_str(&contents).context("failed to parse Cargo.toml")?; + cargo_toml + .get("package") + .and_then(|v| v.get("name")) + .map(|v| v.as_str().unwrap_or_default()) + .unwrap_or_default() + .to_string() + } else { + dir + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string() + }; + Ok( + name + .strip_prefix("tauri-plugin-") + .unwrap_or(&name) + .to_string(), + ) +} diff --git a/crates/tauri-cli/src/plugin/new.rs b/crates/tauri-cli/src/plugin/new.rs new file mode 100644 index 0000000..e693484 --- /dev/null +++ b/crates/tauri-cli/src/plugin/new.rs @@ -0,0 +1,88 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::PluginIosFramework; +use crate::{ + Result, + error::{Context, ErrorExt}, +}; +use clap::Parser; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +#[clap(about = "Initializes a new Tauri plugin project")] +pub struct Options { + /// Name of your Tauri plugin + plugin_name: String, + /// Initializes a Tauri plugin without the TypeScript API + #[clap(long)] + no_api: bool, + /// Initialize without an example project. + #[clap(long)] + no_example: bool, + /// Set target directory for init + #[clap(short, long)] + directory: Option, + /// Author name + #[clap(short, long)] + author: Option, + /// Whether to initialize an Android project for the plugin. + #[clap(long)] + android: bool, + /// Whether to initialize an iOS project for the plugin. + #[clap(long)] + ios: bool, + /// Whether to initialize Android and iOS projects for the plugin. + #[clap(long)] + mobile: bool, + /// Type of framework to use for the iOS project. + #[clap(long)] + #[clap(default_value_t = PluginIosFramework::default())] + pub(crate) ios_framework: PluginIosFramework, + /// Generate github workflows + #[clap(long)] + github_workflows: bool, + + /// Initializes a Tauri core plugin (internal usage) + #[clap(long, hide(true))] + tauri: bool, + /// Path of the Tauri project to use (relative to the cwd) + #[clap(short, long)] + tauri_path: Option, +} + +impl From for super::init::Options { + fn from(o: Options) -> Self { + Self { + plugin_name: Some(o.plugin_name), + no_api: o.no_api, + no_example: o.no_example, + directory: o.directory.unwrap(), + author: o.author, + android: o.android, + ios: o.ios, + mobile: o.mobile, + ios_framework: o.ios_framework, + github_workflows: o.github_workflows, + + tauri: o.tauri, + tauri_path: o.tauri_path, + } + } +} + +pub fn command(mut options: Options) -> Result<()> { + let cwd = std::env::current_dir().context("failed to get current directory")?; + if let Some(dir) = &options.directory { + std::fs::create_dir_all(cwd.join(dir)) + .fs_context("failed to create crate directory", cwd.join(dir))?; + } else { + let target = cwd.join(format!("tauri-plugin-{}", options.plugin_name)); + std::fs::create_dir_all(&target) + .fs_context("failed to create crate directory", target.clone())?; + options.directory.replace(target.display().to_string()); + } + + super::init::command(options.into()) +} diff --git a/crates/tauri-cli/src/remove.rs b/crates/tauri-cli/src/remove.rs new file mode 100644 index 0000000..b541b31 --- /dev/null +++ b/crates/tauri-cli/src/remove.rs @@ -0,0 +1,59 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use clap::Parser; + +use crate::{ + Result, acl, + helpers::{app_paths::resolve_frontend_dir, cargo, npm::PackageManager}, +}; + +#[derive(Debug, Parser)] +#[clap(about = "Remove a tauri plugin from the project")] +pub struct Options { + /// The plugin to remove. + pub plugin: String, +} + +pub fn command(options: Options) -> Result<()> { + let dirs = crate::helpers::app_paths::resolve_dirs(); + let plugin = options.plugin; + + let crate_name = format!("tauri-plugin-{plugin}"); + + let mut plugins = crate::helpers::plugins::known_plugins(); + let metadata = plugins.remove(plugin.as_str()).unwrap_or_default(); + + let frontend_dir = resolve_frontend_dir(); + + let target_str = metadata + .desktop_only + .then_some(r#"cfg(not(any(target_os = "android", target_os = "ios")))"#) + .or_else(|| { + metadata + .mobile_only + .then_some(r#"cfg(any(target_os = "android", target_os = "ios"))"#) + }); + + cargo::uninstall_one(cargo::CargoUninstallOptions { + name: &crate_name, + cwd: Some(dirs.tauri), + target: target_str, + })?; + + if !metadata.rust_only { + if let Some(manager) = frontend_dir.map(PackageManager::from_project) { + let npm_name = format!("@tauri-apps/plugin-{plugin}"); + manager.remove(&[npm_name], dirs.frontend)?; + } + + acl::permission::rm::command(acl::permission::rm::Options { + identifier: format!("{plugin}:*"), + })?; + } + + log::info!("Now, you must manually remove the plugin from your Rust code.",); + + Ok(()) +} diff --git a/crates/tauri-cli/src/signer/generate.rs b/crates/tauri-cli/src/signer/generate.rs new file mode 100644 index 0000000..16c4986 --- /dev/null +++ b/crates/tauri-cli/src/signer/generate.rs @@ -0,0 +1,77 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + Result, + helpers::updater_signature::{generate_key, save_keypair}, +}; +use clap::Parser; +use std::path::PathBuf; +use tauri_utils::display_path; + +#[derive(Debug, Parser)] +#[clap(about = "Generate a new signing key to sign files")] +pub struct Options { + /// Set private key password when signing + #[clap(short, long)] + password: Option, + /// Write private key to a file + #[clap(short, long)] + write_keys: Option, + /// Overwrite private key even if it exists on the specified path + #[clap(short, long)] + force: bool, + /// Skip prompting for values + #[clap(long, env = "CI")] + ci: bool, +} + +pub fn command(mut options: Options) -> Result<()> { + if options.ci && options.password.is_none() { + log::warn!( + "Generating new private key without password. For security reasons, we recommend setting a password instead." + ); + options.password.replace("".into()); + } + let keypair = generate_key(options.password).expect("Failed to generate key"); + + if let Some(output_path) = options.write_keys { + let (secret_path, public_path) = + save_keypair(options.force, output_path, &keypair.sk, &keypair.pk) + .expect("Unable to write keypair"); + + println!(); + println!("Your keypair was generated successfully:"); + println!("Private: {} (Keep it secret!)", display_path(secret_path)); + println!("Public: {}", display_path(public_path)); + println!("---------------------------") + } else { + println!(); + println!("Your keys were generated successfully!",); + println!(); + println!("Private: (Keep it secret!)"); + println!("{}", keypair.sk); + println!(); + println!("Public:"); + println!("{}", keypair.pk); + } + + println!(); + println!("Environment variables used to sign:"); + println!( + "- `TAURI_SIGNING_PRIVATE_KEY`: Your private key. For the `build` and `bundle` command it can be either a string or a path to the file, for the `signer sign` command it must be the literal key string" + ); + println!( + "- `TAURI_SIGNING_PRIVATE_KEY_PATH`: Path to your private key file, used by the `signer sign` command" + ); + println!( + "- `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`: Your private key password (optional if key has no password)" + ); + println!(); + println!( + "ATTENTION: If you lose your private key OR password, you'll not be able to sign your update package and updates will not work" + ); + + Ok(()) +} diff --git a/crates/tauri-cli/src/signer/mod.rs b/crates/tauri-cli/src/signer/mod.rs new file mode 100644 index 0000000..1b907b2 --- /dev/null +++ b/crates/tauri-cli/src/signer/mod.rs @@ -0,0 +1,36 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::Result; +use clap::{Parser, Subcommand}; + +mod generate; +mod sign; + +#[derive(Parser)] +#[clap( + author, + version, + about = "Generate signing keys for Tauri updater or sign files", + subcommand_required(true), + arg_required_else_help(true) +)] +pub struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Sign(sign::Options), + Generate(generate::Options), +} + +pub fn command(cli: Cli) -> Result<()> { + match cli.command { + Commands::Sign(options) => sign::command(options)?, + Commands::Generate(options) => generate::command(options)?, + } + Ok(()) +} diff --git a/crates/tauri-cli/src/signer/sign.rs b/crates/tauri-cli/src/signer/sign.rs new file mode 100644 index 0000000..e0437bf --- /dev/null +++ b/crates/tauri-cli/src/signer/sign.rs @@ -0,0 +1,121 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::{Path, PathBuf}; + +use crate::{ + Result, + error::Context, + helpers::updater_signature::{secret_key, sign_file}, +}; +use base64::Engine; +use clap::Parser; +use tauri_utils::display_path; + +#[derive(Debug, Parser)] +#[clap(about = "Sign a file")] +pub struct Options { + /// Load the private key from a string + #[clap( + short = 'k', + long, + conflicts_with("private_key_path"), + env = "TAURI_SIGNING_PRIVATE_KEY" + )] + private_key: Option, + /// Load the private key from a file + #[clap( + short = 'f', + long, + conflicts_with("private_key"), + env = "TAURI_SIGNING_PRIVATE_KEY_PATH" + )] + private_key_path: Option, + /// Set private key password when signing + #[clap(short, long, env = "TAURI_SIGNING_PRIVATE_KEY_PASSWORD")] + password: Option, + /// Bind the signature to this app version. + /// + /// The version is embedded in the signature's trusted comment, which is covered by the + /// signature itself. Updaters configured with `requireSignedVersion` reject an update whose + /// manifest announces a different version than the one signed here, which prevents a + /// tampered manifest from pairing a new version number with an older release. + /// + /// `tauri build` sets this automatically; pass it when signing updater artifacts by hand. + #[clap(long)] + app_version: Option, + /// Sign the specified file + file: PathBuf, +} + +// Backwards compatibility with old env vars +// TODO: remove in v3.0 +fn backward_env_vars(mut options: Options) -> Options { + let get_env = |old, new| { + if let Ok(old_value) = std::env::var(old) { + println!( + "\x1b[33mWarning: The environment variable '{old}' is deprecated. Please use '{new}' instead.\x1b[0m", + ); + Some(old_value) + } else { + None + } + }; + + options.private_key = options + .private_key + .or_else(|| get_env("TAURI_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY")); + + options.private_key_path = options.private_key_path.or_else(|| { + get_env("TAURI_PRIVATE_KEY_PATH", "TAURI_SIGNING_PRIVATE_KEY_PATH").map(PathBuf::from) + }); + + options.password = options.password.or_else(|| { + get_env( + "TAURI_PRIVATE_KEY_PASSWORD", + "TAURI_SIGNING_PRIVATE_KEY_PASSWORD", + ) + }); + options +} + +pub fn command(mut options: Options) -> Result<()> { + options = backward_env_vars(options); + + options.private_key = if let Some(private_key) = options.private_key_path { + Some(std::fs::read_to_string(Path::new(&private_key)).expect("Unable to extract private key")) + } else { + options.private_key + }; + let private_key = if let Some(pk) = options.private_key { + pk + } else { + crate::error::bail!("Key generation aborted: Unable to find the private key"); + }; + + if options.password.is_none() { + println!("Signing without password."); + } + + if options.app_version.is_none() { + println!( + "Signing without an app version. Pass --app-version to bind this signature to a version; updaters configured with `requireSignedVersion` will reject this signature." + ); + } + + let (manifest_dir, signature) = sign_file( + &secret_key(private_key, options.password)?, + options.file, + options.app_version.as_deref(), + ) + .with_context(|| "failed to sign file")?; + + println!( + "\nYour file was signed successfully, You can find the signature here:\n{}\n\nPublic signature:\n{}\n\nMake sure to include this into the signature field of your update server.", + display_path(manifest_dir), + base64::engine::general_purpose::STANDARD.encode(signature.to_string()) + ); + + Ok(()) +} diff --git a/crates/tauri-cli/tauri-dev-watcher.gitignore b/crates/tauri-cli/tauri-dev-watcher.gitignore new file mode 100644 index 0000000..0d6c4d1 --- /dev/null +++ b/crates/tauri-cli/tauri-dev-watcher.gitignore @@ -0,0 +1,5 @@ +node_modules/ +target/ +gen/ +Cargo.lock +.DS_Store diff --git a/crates/tauri-cli/tauri.gitignore b/crates/tauri-cli/tauri.gitignore new file mode 100644 index 0000000..b38a7f1 --- /dev/null +++ b/crates/tauri-cli/tauri.gitignore @@ -0,0 +1,3 @@ +node_modules/ +target/ +.DS_Store diff --git a/crates/tauri-cli/templates/app/src-tauri/.gitignore b/crates/tauri-cli/templates/app/src-tauri/.gitignore new file mode 100644 index 0000000..502406b --- /dev/null +++ b/crates/tauri-cli/templates/app/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +/gen/schemas diff --git a/crates/tauri-cli/templates/app/src-tauri/Cargo.crate-manifest b/crates/tauri-cli/templates/app/src-tauri/Cargo.crate-manifest new file mode 100644 index 0000000..94c5d34 --- /dev/null +++ b/crates/tauri-cli/templates/app/src-tauri/Cargo.crate-manifest @@ -0,0 +1,31 @@ +[package] +name = "app" +version = "0.1.0" +description = "A Tauri App" +authors = ["you"] +license = "" +repository = "" +edition = "2024" +rust-version = "1.90" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = {{ tauri_build_dep }} + +[dependencies] +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +log = "0.4" +tauri = {{ tauri_dep }} +tauri-plugin-log = "2" +{{#if patch_tauri_dep}} +[patch.crates-io] +tauri = {{ tauri_dep }} +tauri-utils = {{ tauri_utils_dep }} +tauri-plugin = {{ tauri_plugin_dep }} +{{/if}} diff --git a/crates/tauri-cli/templates/app/src-tauri/build.rs b/crates/tauri-cli/templates/app/src-tauri/build.rs new file mode 100644 index 0000000..795b9b7 --- /dev/null +++ b/crates/tauri-cli/templates/app/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/crates/tauri-cli/templates/app/src-tauri/capabilities/default.json b/crates/tauri-cli/templates/app/src-tauri/capabilities/default.json new file mode 100644 index 0000000..c135d7f --- /dev/null +++ b/crates/tauri-cli/templates/app/src-tauri/capabilities/default.json @@ -0,0 +1,11 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "enables the default permissions", + "windows": [ + "main" + ], + "permissions": [ + "core:default" + ] +} diff --git a/crates/tauri-cli/templates/app/src-tauri/icons/128x128.png b/crates/tauri-cli/templates/app/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..77e7d2338e9d8ccffc731198dc584906627c903f GIT binary patch literal 11059 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 IM6N<$g7Cm5Jpcdz literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/app/src-tauri/icons/Square150x150Logo.png b/crates/tauri-cli/templates/app/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..dc2b22cea563cca761896877ed8d72502c8fa241 GIT binary patch literal 13032 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 literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/app/src-tauri/icons/Square30x30Logo.png b/crates/tauri-cli/templates/app/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..60bf0eadf75c86e5d25cc1da13fe5ecc2c39c559 GIT binary patch literal 2078 zcmV+(2;ujMP)%q2v2@fKtSxpMXgijv z)fq>Pk#L19Fsu4()DBYp45dn$d|w7L$xim4(ghvO{2KV_ z+$Hm-4+=-lCvt6F`pHd82H&_b<~Ep`2qG%4yaCtnEHo$PE&9r=><>InhMM}#Bal70 zUf^jnRszc83E!|N`Rx5SryfTxDP-4{&|41om~VDuv~w6qd_&luzjUVa)#`{hUuj_d z;-TjOk+N;g#m|ICGUpTKol4b;)1*uMHFL8MKCv|ZGRhN>F6J+sEPnK@OTq0qxW^;f zg69Z@bb;=ln|bV!1*zxYUSeI~E&nck;GNdstHrvQ!1<(5!-Ux5Sj-IZ@~z9_hm@9c zE=m9VKe5%jFN9tTX%YuDVJH!eouF`WsM|Wyn`O<*Zm_?u*sYJ{@7&uK*yeKV5{&S) zf#D@;b0Y7ma@VA6+_UYN`OwQ}B0oEqG|om0!Iap5_k7kT+~I_CKtiX80<7AJ0rPaZ z$s@I8-V)2;;r=XZJJD}8sl};Mn=-J3lr@S-XWVVx+a1}{8floz^ZAs;b2h>Rf?{4D z^A`SaVe^surn)X<`3X?*m}zB|{wl#5hVBPZ8SoJ>C^ggHJ& zx0Sp1si3Y+b8e32LIJZ*~&~tlQ1KZVPQ14EFNvvQ_c$BZaG4nui zke92eo8#uY4zLT0B79WUpPv6pSzi13~yl3@aY!Vu178d^4!sM@D=Xy%{JJJL?)ngsl-cqztS?SeY zO~&FDDw{A}5^59%@@`tejg+Ez9(j=&w@AtmakDw%=5h#Acvjf}6hIna>VW==n%wuW zCZH7%=)I1>o{a}fmn#of&BN852^-5mIThw!4j|1IO27=2!$K6yTml7F-~ax}>{KQn zjE2;Xz?@;%>N~$rG97V&c+7&{K$p>Y#^~L7Cb$Xw{ErmEDfhcxH^RoXMF^p8&I4{Q zxRJ`)US#I2eyHR#ESu;RU?yxh-ZOLm3(FF9_x?mxK=kz9&3dZ0L&1z8uv*~egMsD8 zd)R?N6h_bl5S7nlEzr1Es~V=hBY@**wzB?9AT$HY%MG=k_y=&W!NR0AJkVX#dURK~ z_2?_Gpf0HyL{)>vU6ICNaT?DSsfHClwJam54_rP4XwZ=jbBfkyMAasONq0b2&-iAz zBvH5gX{-OjyP?a+_H(P4zygd3HYa(W*-1Y$Yw;KR?s)h$5h#jE-$y08_f<%$sdOf7 z_$KU5AS}l0v`x4-Fg$zlYTz4XY8HKy`fjHgzkGsvIoKYcseu!oae!Q=Y8%(}ec|C< zoEbp@x~w>~BB;^Six{6>uj#+KrjZ|wE4j8-`G01;LAUvP8La~Av50E6zu%( zZY|w^R=t)Fm)B;i7OZi~W-o%i6Ra$VVl8{NmL1voxX>*yAjzj}sjk!d6ucbxl|v0H zpBijfwOJHk8)-|xibe~OjME1Qpjh+UKQ3v0dkbg-j|+CHR{8pk0ANTN33RsnE7<-1 z>mZ_t(1Vpgg{5|`!W&TAnw=gBb)0;o;?SSI6~1`*ub=^P<-=l{CUUA~50uWpwR{i} z@h5!Hn literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/app/src-tauri/icons/Square310x310Logo.png b/crates/tauri-cli/templates/app/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c8ca0ad132e0184b64cd2e4a38cfceab3c4b3a46 GIT binary patch literal 28507 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 literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/app/src-tauri/icons/Square44x44Logo.png b/crates/tauri-cli/templates/app/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8756459b63d6e8dd6003b97613befbe2743441cb GIT binary patch literal 3419 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 literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/app/src-tauri/icons/Square71x71Logo.png b/crates/tauri-cli/templates/app/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2c8023cc824417a29378a68471b8fb3af33fdeb0 GIT binary patch literal 6027 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�rop@t+= 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 V#?y`xQ)U1F002ovPDHLkV1n!EW;y@> literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/app/src-tauri/icons/StoreLogo.png b/crates/tauri-cli/templates/app/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..17d142c0a43208db62ca28b86a3239329ac93aea GIT binary patch literal 3971 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 literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/app/src-tauri/icons/icon.ico b/crates/tauri-cli/templates/app/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..06c23c82fea615c9394c4c7f487392e3f0a2fd4a GIT binary patch literal 37710 zcmce-V{~T0(gym*oY=N)6H8W8{y1^_Vs*F!@3pX>q#0J#1e!@~NXOa%!5oWcVDOicfi zV-W!WWK;k^Na%mE0tNt(NAjQke;N_MNE-mSZWJ(?gwf%rr@KXO{llfDeJfxN28V1 z3>zs*pUdV7m-B$`o0cwB<|^5=;Quy0*Mus|4J4Y3DL_{)h)hnx(!z-#BOaI8mf7~Y z=6}ZPyC1*^M0?17PxGGb`2NnlHkc`#%-0eAB2?5(sU!rJyh)PWIFHWVcG&deE>jr+ zQD)Iqy;#n(cd?n-q_D_&G$VM4omatA93_zQoY<}w$*J$W#X3r@zCAV8L2odIr z`L1QR*r-i&sEKnd-J>Qjalz6Y?)JWAWm%d?y?ImX)Jy zq#Py*XMo&}HT&}__Ukk?uj)1m#oJ9|)6(v$DND^b=%Ph2Mp9^a>$d!3X7j{e0>bn) za;AT~)A_fCpEHjxoX8N==#8`zdLqWb z3IewfUV@jcmAi;muS$)soq+a7|1HB#Vghj!KR5@IBe-SONRBCuUpr~pmGWy+8B28U zhtnSvinq7`VKukIii+FVF2e53uaUb+%wBJU2Un`12)I(IG>{IF%RoKw5=Agm^@dM? z#9u?$TH?(xz%aNpJLM@k&567QvAY^D{NzH21EzpdJZgn(Yi zTtv*Q9FDGzN}dPDX(bDhYWO0n(mmQZtoT?4Qk|$Wt)s(_JvTKW_B`Z7?)aWL&ilRT ztmJ_#{sERqyC?(ARU*cUG3TmO`tT+;Dx0t>t>n9{4jK+v=~%SY{S(XxxIzJnU^IXG zA*2>uqKeYN!jr1nHH?b4ee<-)E?}EveetX5%g60%s8L|dRO@=F{y9|)>LQAt2xi(k zi2UOV0@WxiGz3hxKFB;13?NgOI6U029ds^@n>vps_cc>;8MF1Q0_%AyKz{%ABtPI_C^`%%FY-UdoiA^TQf*2RmVy%ecTRV!{Ckl+SS`!RQ6ndaRN!($*YE)A zP>*RNBZ$FI6#p;+O()M68mbe_|`;P1I zd-*HDp=_svoyqw_ojF%gTwY(6wAI2q(%Bx~5URFN@Em@Ch}QwCBsR@DON{k9%P#~@ zWUDogUk66qIlX$3C;*b+Ay85Qrvu4LQeDK!SQC6uJ9sKgWSQD%v1>8a*#a;7C-pjz zJ0&f%)q?oU3Pt|qt(h%_1r!z_(kZa=3&oW5Dl}9fv#Wh!L!_I89J@&TiODB1v;H;( z#=jwBc_V`m&wDXD|0qhp6BaAu!$rt;EUB=+O;`R&lb}Gfd(fT8B!0}D?wIYe6h6rs zT)$1A1d#nIiQ(!fF562}oF(twj^FMAhFRN*BdF8qZt;Id=38=9g3fjcd_MXfJT1$! z6ysaW#kj7L`nW~%+i%4LrkAGtf$I>_+hUB_;rwg+?Ok?FHS>cr7qK?nP!f#9#el&s z;>a;BqpcVtJM3Vc^s3;@L(wuL=bNX?4yZi>2*MmGIP-p@b9*QXMZ^85 z`g@nQf{`lCXh)5NzcpseyJ>f`b<0tQoA6@qW#HsD4+VUF*qb8|gPm7JUFVbe92aU9 zw=d4a?*XLnO`xbwcQfWZJFybf)Wi7W0*V(?u!&G0vhR4aVd0aJAp|V!8A2&DUjF1L zE>EZ<8FY;R9kN@tLAB^uE_fL8@k+Z9uedLZc#Nn&t|VU| zWXkmsS~|{YpiE%g5n>27+wyC+WlZCPq3VGfid>|WbRpm2aa@eN-kXclNTXaNM3oy{ zcfRve&z7>pcGdDGBqvfo+k}fbr2{Au*lnbvbCQLVYk>{W)DjY+%tD!q zl#FBM3+zfh(tNMxR!r~kTu$?>fLsN<&*ec6!OA1r#g|Q#LFn>S$*rZ29>PyBmRZU4 z6a|UbTaUHP(H zgT&_fjbrQbrKV6KZQ)$aXdTt_9tSd7DQO+++w=d>Mk< zticeHmzm?QI?bUB0YLwL9odb`@Boj*!g%2ATfG350KI3YVX;HlS{!jeK3 zvgA9^fnt(s{O*5s_YR=pL6U%OV^LwnRa+Y5fkS}ar9a=hFEn3Z7?zU7*4_n<{}LU5 zl$g9|EzmIN|0NyzPr`-t-*kxZ{@Vxu0OS2%I;5wpWr?PR)^ojFb#-Qa`ExnfB_@-M zRb&dDg9d(ZOw{%uX!ng6>*GcT0v(1bV!pi~!vC$pKZs}_!+kygdGncEY2MIyL5gyu zG5M^vw)Q!%jmLUUWO=kk++q0pceCB~nsELH(||rTp~MKxyS~c-(4)zcC`Wzo8_Kez?fS zo7(L7sY6})!RLH4S~H3mU%U!{62nfL9nL+NZMvJCoB1fO6UQez`kDpC&CL{;1#_QiIQw8dX?5aim8IG;!-mC;}B{pCSLy(Vj> zCC(4XNQ`y&RmwL~E}%~GWJXBJuPJGjj>uP9Hb{IWowUL)n~%*#jDl6_?I!d0^Li9K~725LyH|{|5#`}CG1MkeY*CfsL z>RWJ{eDn(Qx;+=Vc~AMTi73QLA6n9#ZdO$w5D!Lk{bD6dkgPnJ03*`{SPYtxd$!}> zWBNA~u`n4mgGisX5{hD8XxLS^=h>`E3v=fLO^#;e-N0S8x85EWdDCd6K9`n?eZ@S@ zh2>n&BBXl&;zlKhS|Pw(nppKml)nC&h#uu?li~fKiP~tS%bH+kLyVI-H>&vCn0t|A){02h?Ew*XMTK`CkA4i0l9PT&$YS zPc${OUUvfCrfat~2pSAa4~>HE!ZFgo2t*_?h!jJnHyQmRK}jYgwh(9>6XSi}-{5iy z;gYbB5V0xJ%NWwq$R%?a(lS{Q2r#{ENMzoG4*gEHv#3lOzZy#_o)FipOK)_Ym>i#I>dn6v3vNZXh{c0dKFg@!nO^isA8AZ^Y+0ak( zJIr^mXq~Ccl3qxnXY*cY+<)O=OUaApnK?Sw<#pe3G!pf*lQkN{%9E@}oZvXFlOGnR z3E9yCxeF37&uZr9^VA>QjD381IG*fglUa~$jrv$AK(q*b<}m=5T{olME2zWdLKNw~VggDOEeq)}ZLDPx zr>Y!@9GYwtxJgEw9+E~uPfbBZT!?!YA%ph~XQ(2T7{eAHm(jz=ca+&iZ9%4aKS)`4 z(52somfMadui$bLE^pvS;Mzt1y&SP}Bhz$_+cq)pAPuXlo>)&mH6KSTO1~dK?68lL z$-2nm+v6=Eebf`aM;X`|oGqfZQ)7{Z`)j7ho6p-^U7G4KRMu9Z7r>U zGk=@0Npfm5Gr|oX0p)_rPTdfdMkY--32cOM{Ppc6XBEfVd-=*>Hb+1ek>4zw6%~g~ zoGndfs`7NMYBz<9AW2gEs4h0f9tVp}{1>YA2Q&pPNQ2QUIAsNqRU@Bi%E5@+w$&gE>!qPCQ zyR5b>sX5*|{HqL-uZ4-jX=cRGd&|h$bpIv_dhQ3OcRx0UbZu+XC||nula^w+?-^~^ z;%5{k)v0D{m7NxOik9dM#nhh!tLWM(eYM1_PLE*P&6jYzp7jAUbf<%1UI%Z^VXA-t z)~)PSmg8xfKTgm%lRk3(wj#U1SOfA$6X)r4bXpk~f=^mU4k+KD6TT3C4e08PpoL6# zL;xTULE4aDN(DVGr)}c{4GJvbaQ-$F#4*S^O2fROVK|=0@qQT9+dX*8B2ZeuyY_b6 zEimW^qL*iMIF-=ie4*B8=dW;aXSMq=@k*8RpS3|Ct-KrO3JyPsMU$>%7#!RIAY-8l z%uqKp4;t*C*hIu`*s^@Jjtc3w@IxMjP%e?~n4K8LtnTeYc?jV~Z$HC?m8e!#jnGAh zd!YLJymL;v%RM4j{*O5EAF_+-zi~nX^fep+Kvex-oLJWMcEMZr?PYa0cXP9FZ}B3Y z^wMlK#Wj(X`(XIv5cK0$23Toe)=`M!aX&LP%pRMq6%MDB*E-NKaE_vRZ^*YK{4i%I zV@N3Ga)=K!V>di5R*vqKc$`tH%j?41!s05|r%!7#{6l<*Qf6}SIO@6 zjOIxyEb1Qh$B*M{2v`I$BO1NiWX~@pP#gy)3P%qDJ&O2A20&t%mYM`h+b$K*1Ga^^ zHswX;Bdz^cE8zL$(6W>xa5BVk4IDrSadzaX;b>bpc9sB5U*3?eucf4`OXy}H2CoJ2 z+e4pfJdf_$l926*;DUAgO7^@AYYK_ON0<+qg*aLl10e(cdT z)O&)yEv-V4GfpX>dFkj6Umv60mB(rMV{V&5X5XPDqIF(ahwei=na>4Xh#31qCxW{s zFXv=jj`m6ZhMp>xGtnm#Fb@v)My7Rgta2qruFl`&1Ci}x`sDxNj?SpE-7@E zUdDWng&XO$y12c?NU6!lbN}%#YoiTXVfla>{;UcaTBVa^$~g*Ut;?)qQ2SY7zABzV zWl~OQZ8w+qa`UE08H?9>p}>Iyv@bDxo-S6h8?jS19h+c{ESt~q$cw$Px?@!KvDiZO z1RnxzIXuZj$)m|=l9vtNtO7~FRt9k;PVvm<5zDc*9c(W52}LH77`l4OQCn?%AKnVB zVBc%R<99>8W|y_vado!SA{)&aBP*f!70*nK$v@&vXQG?0pFeA(fp@Ej~toJhqJqOd-b zxooc?^pkO_f?8KWU5HXl%z7IAb$8vCofXwHPspc2QdNWo>xXG;B#11VrD}CfT9>nx zuU(7It{5HCZrEJ1XU($=Z(T-P4eJXms1oA21341OK*{hPYVRckn?g+hoa?U^RK)9U z<27$eU^aKN+$>cXkeIXeN@d)Fh)OEX#79;WKq935xxZXNnaPk@Dg}1Rx;>9+`T4ey zO1lB|aFANcG&f7Uj|D1k2!NZHYU84Ql))61{VkoQ{Vl(&CxLQg2kRzwt6Ep?#mQkV z$XU0Zmo6eq250gIOO^_f2}VKWy^Zbf6}%&|Ut!A!;nPrX--WCd6x5xxU?MIGXlvN2%Af(!8fqbjKb5BQMlaF& zG)xO3kMmuS%O-9V*|iZj_1#XlTNSVFKKNF8zXf)y!y(Fb2{{_pxoYE5e;RXK3BSNj*4V^x-xowQt>8sQY64ix zb?HsQXkeh<`+za0dUvaw18d4}iDT#PIJa?fx@$ zv9R8PY;nX{C-alKeL3IrxVd8yrx9EW6PMS`RB?whW3kgj*gY!RaHU-}yXb29I+xk~ zcP!JRWr}$Z6%p@CmD`G38=c-(#~JSBt&N8NV#`EX#V{Y7Yulw_KL7(LC<{AHGTo?? z?PNf9H;l(vfE~Yw9(Oi6dR!~}$^WKAittVNiw**tVI%oqG!lE=Ct4j!42WE1FeY&g|oi zSxY=<7>^)Q2F|AV(`?sgLtqCj;et~zLc}7L82CWe+GhxqNzdS^CHTM!fkjX(a!FL{z+44g3VV;TfdUZ?k%i@~WtZYG$X3%nDA zUPqp)VIz8+;1}H3TZjVBTkE`^`fo%L{cXR+SzY*(Pii*r_ep`_eS@MW;c+4XbFkZC zw;ZIm2~zX{+zG>p6)vgNV`RN`CIE0=32;4R)x0d%j*s)u&skBRJo=^dJZe1F#yYZ> z-w=%nlRCf@Q;cgMQkVIcBELyTREiW4(9G7Jzsll`#g3`cBzU*rm2Syc{tx;ORtE;r ziD_`}b+{4Seud%zy?X20ygWtpS?mtgTrD)`l2PGBg)V&KD*EJ-qZoE zSpmdP{Ui~K7Jvo>4H)xTjnNdHQt6n9=4RYnenZs+#Ss1yghOdRAwT*aZ%Z^^U33mg zlyX_^z8~}!DMRR%2lgA1j-(_o(A0*p-JO&{_4OQ89(v6R%ZEPq%QC^5%wERx2hFlN z@cQ6z#RISeaInI+sz;;DRBC}Z5L)1jtC@*$ge!|ZmoA92znj|?vqBBWY3+TGK&zpo zAcghpEOLv~nEyWfK(uo$QaK|k=&vqb0Tz_YlwsI;T$OISuTJ5U-F@YFIHN{Ur+&W^ zJ-J`!!dw916H`aX6|mS(CELwiXcz#XfHjrLA1+G5V8}b^Fs3jHi1n(IuKpmxC!E#@ z+KOj78%49-^_LcJ&70M*k&X~7S5P}x|d<>>@yO||xLu^AYsUftgtow!l# zL3}tfAdn;8AaKCdYE9MqKaoX4b5Y9Pr-~01qdW2ZSfY>vPA8s5F{oggxRlTqpYso3 z_s|3pz%G|-PO#ljcMsllk9sodGi zkY5L%f70~@%s>AC=N1|@p+`f7V@3kR1DB+-z)=hrNi%HJI;8T+JM@7x~kgm#^f)C-S=e zH&W8*iQ*L!ZdhFI;-gKb!s!)BlnJ8Lx%hqOeLb9q_o-gZ+xdL%`puVu>=MX-HvlrM zJ8@0~9kH~^wrZ_EM65zcEH!}qmwmOq=TPp}nWBgX<=`jL%!kMNNTJolH0FJ7WR15w z&l)~v65$KbQm@DYO)i0j)*LsM|Mv+_)PLJgkb5d@&m}^0>^v17#p{p^~@XK zX`@9&(4g+37bpu`UFeM1r~~Sim5ma4R4D zgLLXkoH?DRllB;uSx1ItxWhSr;;6chdN5ZYQF6og>=dcM^8pN|3?&w+YL{ydOi?Jm zBxDiZjnjF!-bbM1>t60;{>Z0W5tTwO5vyuHtcvGl1nVkThBhbLX7%kK68hGlYuEi^ zcEH5dLFZp+ad$p^c^6C%MAC8wg2-~kL--`PvGe0!ufmPDF2#`Nhe8x+y}-0W)=#Kq zVn-7gzLZAL_~PNVu^EdvDT%()89#?2I!i)^o*hBlhPvDLdk^=Kv<9)I(NVxpGO29k zs<;cZp;&cBY%yj@XkMW>&c}C^NZDCz1`4)cR`o=$@Wl>@SuL-kL0gS|%Fa?Y1%A~& z$C^HaXi^2vcJrtic6Iyp>gjW0g2;8Lkx(+L6Q#lPZ|vywT~>&_?Pkg=a7w+S-hCSB zssadaOqORNmU3o&3N3DXzDb26^f_#>LT zX>NjlC1oW6gbtK+bhly%!nMpLBWtf`X@994?h}o03YVcPn+r7ic}24`PdqS(FX?|$ zLmI)XGYX%pZibP6dStW%1_RiIfJJZ0zCvI^jeBHv4zvdErVxSxyW$aWY`fhtfJiCh zF?XvjK+W4DI5_hBOR&fuVejEC;Z_h^$nCpr>&;2Fqo$SC} zhwZGVd3QwM{{Z7c|MWut1>==gj@tkL2FCxu_}?s;6nwSJQ5%1qKEJwEmsWWmm|P*y zKmf=93Wx;cU_fxuNkkDT7#C3i82pry1U@qUEm1-6SfKOa2~!c>ZwL_7RwUVeqzEY@ z+}vNuSF7*melj)p@0FMfHVZDnd4qjb`f6L==iByOJI`A>HF?gLo)hobILGfe*WMsl zIsR(pPB)nPP^8eH^0k|NHZN7s06TVBh2WfC5$2erDB3V|u=({jR^s1DYNVn4af8x>WX+PWj&Xl;t*)#nzB zhqUUN&Q2UEW>!UbHD;uca5OH8xMxv7kt15=6BkWL)x>5B|9gtn@O^w8Lyn5Z8688k z7keeaiE7Ue1GT%>m5x4%o!$!eyN~tHhSWOq>ScVfX}OGEW9ukk3kG`15O;of<6iOK zDte#O132D!YHIq{GGFHH z-N>)(7%lB8{+^3$<8p4Rfk5BqwjQleY*8ENZr%Ekf6yp_M{lx>< ze&PlG8MVg>q!<`;(eAohOL?0>wI3#oJQeT*&L;+q(jJ5fTpxNdB*R6hHF)NFUih{% zJSSe~Z8v(!n>j{aTlQX85V#ug=h!%48$!d64uX6>jPEnBR7=5h=Eypzoc=d9vJx_iM8mxjR2Mifm?A@bNOG@O$-6+W>Nl zU~P1#OfHUrQjmzWg3utMc| zE$r7aGM#oEmPYS#e!_4qK+iF*%Eu7o<)KO#@677?`kF!QK3*F;J6cOQQ2?ja>o0ak z_nGeICR4k0Zv7@ck)gkPGN#(|a?tDR;zLa=dg_DARaNqqGpF=Qq*$viSSLSOh=wB5 zzc!05S_Znqi-R1aydP=T=jW4{dP|9KCwKOz^z9o~T-G};Lyf&~p`{{Oc5Gy}I{&iX zVde{#y`MFfu(H=Xl6)xDA&b+um2as1daC{wC^mac^{*C~dBOto9<LMTnKU2XU~H z>=v*Uzm36_xAs+pMkm<|F4nmB8sc5PYy zOadT|key*{NP$MMM~9Kis-6kwS_;K1lP5LhZ|HfTa~oG$t5Y+MYb{2{(pA<_XeT{7?^{p$+U7K%=>n>Nky}0S&{mXXnYcvU z@EHsT(pZa4HK|`me%^LkRHd!gt`u8YW_e}7{K$HVoWyY5R>yxvX=PL>%5Gpb0*Qcx zo$At}wx95H-O`|rtJFjmq&bq`Tu~!c)VP$~{vH%8vy;{8wUh}ycl5LdQ8wNxSVvcy zU7y2yom^c}uTA*P9TM))$&8+(8mZ#EhrG2K0mPz}?HL^i$t5bb@zl5%VLcDC;4#wZ25CuFL+GbO2=IJ~VpFj0p zBLdxKO^bPladfkZ4hC`ohB#}s*kDMRKd)FdjPHp+gz?5{OV zHqPm)lsViW9f8mZil?cRxtTIHQO93jH7~`@sL}TxwxY68sAPE9T;)*tHZg+f&ksLk z@0UW7G>QUiEGZ(-pk*IKBGS5l10`LluOlkRx8Z$)x0L6c3jqzL`bSvsBWxSdoi@z( z3K@w8!cJt^rOl7z9e4cGS}l}c(DNaA-k(E~8RZbrVA8)Aqc%^_%|UQm&^eP-ofk23 zk5X3XB(F5R`}Uj8(!)fABC|;OdkyVo%LKUl&Z`z>HoaikCwNva;=j+XVt*N!&nKtW zM<_xv3uCz)h!^m6F+R=xf_v1lN**!>Etv+2sKwE>Daaj!<9(otur4_e!&(Y5gIAiL z^j2jI+tnphXD)X??Au@KJ1!n=5%!*TTwzNXl%sp=qWm$Mfwo2FUfOLmX*Bq3Gou&G z&vU|%WR|6RiGJ9bK7Sj~v*759Iab*4J(BwbSYjBm^9WEhwM=^{VMuk;UaI<~p0)Ev z|8&9-5HV#0#X=6|!KYkG@Wgs39#_3@oc(Zkj>>Lpq5BHYzBGOGS$0kt+Fm7~z=CZE zhCVUb*5O$e_`4Xr-ba1fI74Nn;QMn_4_at~8!{MFEp5%dS?3%n zH)fDn3WbCSR8GpHsiCJH|1ar&t{jNg5KD^Eg}G3A0}>XgJe+J#Sg|Rw z8T-m@2fNC4nNES~7>{KRgM`7cLAN>quo5L)Svv46GK{@uJ5aSgDclBYlUz#ji_=0T zxQK9b7rIsdEPllX;M_ULPsEh80rX%37c8lOayX@gx$65fGSUvFQRQ$E`)GbTS z4}rWg_cD<2e0T@VfO);1$|MTKc9CHK`uzq#gAD~u*`qPJnfJ5BU{M}kJEJ>`tzKn8(1Im z;IqZVhFgc>?4-i)H0^tJd%ow#`dFX_6`I!*+<@S8^fdmO_E9=wAiyU^fV6+9#IRiJ z&RbhHvrEziPL$(-ui!~fxKv^3icTQ|T?!6$YeYl^F@OVzP}S2zd!G2CIo@&`K}~f5 z+)ziN=PcmG?|GQi%p~}+*=2Qa^HG_7U5_43Rj)Ndti+HC9!MTHFgg>Y8!}f8u;MdJ zJ~9is%$`O`L&L)o9@0Y^1lx(-^vR3frl@FnTw%fVZKNIWfYJfUe(6T!9TG;z82EJp zNL9Jji|*{Y%swvX&Cs^)x96G={npnCUTc5v$$=!jbN}$yVnL3E!MEQ9 zLlVV2)OTNXJ*u90G6Y5m+l_Ah-VvIeTZy8+RpgS77oX-1V?#bp2PtAJbiS8n|Z|br& z`rrPmDfS=J_!)v{atR>GcrhTvy+iO1?C;gf-ycgUqzpVo?{CF4QDx#TSBZ567aKL`TN+ToOwxYe&fTFa3*FB zL)Byl&hMCLXO*M;YRpCko-<#>rf25&${&c|60cRX*A6Ds4enz&*{^;=KdC}`v`l8* zS5kec139$js7!;D%Tf|XMR*?WdBM2^dQPmqI$S##sf>v)M8~s&!U&`ipR6f5qjW{2 zBtU)1&QB#6Nlic~z*sAr8M*Gg1dNIG58;RQ3T;W(ZYE6r`ukmAFse_uz+p+A@cZT=?S9j2TqujDYRY85_RV3vvnMN(jWY9O z5Ss`c^{PN;yq96pm@^VTC2N$!qm6jy7`ykL{SEIO(@h_NXM??e2;Q{}QJ^pmc}|jj z3~WnqDkt~xs#EkJNc2489+)fF6-%?!)KB>oHHIxrbsviVX&+s^lhLr!s17}8DL^xV z2G~T}_5IcL^e6ru$a*BiTn(9LF_N;>ig{JP{>1TSK2$wdCaxp=-0R@-H1dnpi{a+wCa8mGGeJLTLg;n5HR-yknV>$Z0jZY@ z$54MtcU)tZSB13cACE-wU>#<3{IL|*BKSD&YRQ?DQkZVIht{2!89h5O!+q*3FEy+y zko4Ci^K!Oi+viKZxU6Q7(}ZUL_w-X3iDH<>aQk&?;EGnz+vUBc@mAV%jLfxGG@g^C zp+$jx$UoC`L+_uXq87seRMhk~+?Cmza+4;!#=ocrp%q)Omk;FZdoA7Au83}bD?MtD zC`$6=aGxd%TI&2O&Ug&9hAOC?iWB8NmKpG0X6EOh+OD@6(GfF|M5!(@xaAZpdVj*; z@ATY^&ESF_@$111bc7SZyMdj zr)o~luxHsk>kJb`geGu;<44bQv_58_uUdr?q?1MA^M14@Kgak;%txlGu`~32Alent z#TM{+U6~ydDHb)LJixebS1iI!UcVaN(_#`Vdj{Kw6Y2~ZI28E$1OOLYuFp@;IoNb? zBPsSMT`CbGjfS4VDh6ozM;BQ`_ihRiH)+j}1N-L8!TTexJN5bS?FH|u{a0bPl$Wkw zPN2ifv5|d%iDu4f8lTu2dzdGQoxwy?$qes4_}UK7YtuG%{G|7?dRmT=A-u9dU@QRa z5|bgH`wH7}Z~sZzyX`kx;auV%G_Po!x`Sv>vSgJVWR zy7!s6;|4w!zH4Sa@z9eM0za8%{s^(YEC&bL0Z@Ospaw_vGadNKNdzv<%X1z;i`YR4 z6WqSgSuXwb&q+@T8di(~-hmVV z?CX&x9i|-@7UWTzxU^lVZWT>kCY%%!r6lQ}cP?)=#*X#x_XinFyb+l+N*|I&<)g4O zCmlj25o3ETvEF+YpUFLvH|8Iy=`+Aq?i7X=LYZ!qF=Hy_L1nOIH_FW>YfQHoVZ zHnEi<4oWR`aRhpL4tr+>xy|yJW?S2;+fST6rr<{E1N7x4TGOVi*sxLP40~xp@77V@ zp$TE2r8LG+>Zty$+~GAjVpz!vBq@wp0h*wS_4d1!mu6uNkA@ zgTX0%)zwoh^c~)nf6x@J@Uxtrtz5zPDK9?V&U=q|4Vst*o-(WzdQxR?R$igT6$QO% z9N()t&bRLTwfa)W(Iw{gZjc*KQ;&1!M?1}3pAfP@GnKrOBPltGm@FDesnno50yGvr z7q6Q-Uu&CzKbpB|xd5=ExcgwN$qqUOngc)U$$^NfC@}2EwW-OI^OFzL#_JwECodMz z(0kLM7Ax8pYj4ibI3=P4cvDCSr|MDV#^Zm8T5LPo&p?L z27)1mq0LQipsb`Zv~y^4CYVu|(Gv6Cc;<@emu+?0(Qru2sgDrK3_%nU^8T(jT{EtC zer2!YG1DS`9TowT3r$?tO!qpzs*@R}QUz(=S{6=tVz&QTlBQNAa(2rbKxF&@1ENBs&wmpin0p%lc3_gFb}Z5opl+rWG#+!e7HF_32fFL zDFvd61S}~x22H{*!}1pDjxbDT``fCH30w_d`}h{r+!w^U;qLzk#D=`yRO*~99s9o_ zm-fqlpr~NpXMp3S0f5lIxgK8v>Hi(5(7w~+)J_Vz1tCUC3bE|zdTMueX2tML@h)N-zf9lS@Vup9H%_Wn{(Tg=w0^Nr@U%OR%_|ZMLiD^K14__3C z9#|x>vek@U(dY2ic27h#HJTaHw!ToE9svHmP>DLVf&<%xUIKF#F)#$JA`6!RcVaUcLL{ysn9QFa;@}Bv+OI z!5{V^aBeV7GelvB9=mnu+Ie9yj!|`rC+M_rA8Ajr{gNLB)n_*-eeO_e>tCH6%89Y8 zWO#vruS3M#G984;d5a#+X65T5>2B4}Kby(RuI-3R)!- zXtq^xK|r1XdI9ZuF&+hDg1kR9hRyYt=dgtJE$QnJ8`UCxaW1KglTSX&*Usnq+{SD1 zVx0Xw_%;rF4ld%T38YGZz14rRb0JK_FRE0pti)Pi!{}E=uce$@d;)~6)vTPyU$Sh` zxbA3)bk$4;>4~lOSEfq}FvvMbd7mKe0nf00(uxu1Ld%ny+O}BfJGK1!ch-pNz<0Kv) z>CDui(84%yIy`%dZvLL;EkUn5k$2i|ein`fVlEL)X%3k@_+GWngi1w$Hn?8___ip# z$qL1H#wT#wxVs&cFl06vT+Ddb^{V2tmTRu#@a`y(?#c?0Ie^uGSD7R=Ue-_FhI?=GSMVI;^5o&1(?L^M%5w0|2Lfs@=px{_%EHT>r`$I0BBSFk50byh04)W%l)Qq>lWxy z@FFKBOF)tao{-g(M3K-nVk9)~7jf8|S+AOwn?4-C>lk|Y8F zfTR5Xb^VJ*-wqMjox8iIfQA0${b_)Y=6D0Q=Sz?9s{bED_ASIlZwN^Sr5X6dj%59y zLqZe2-8()$I2?qLByetb1Bn7*exI@53ja(X9C*jud;|bMshb*uNm$}CrV|oiXEJV7 z(Ni)|0*xVokZyxnwGy~MH#8W6B!B!=LLM)*$UVZpB?HIKTq&oM`_+ba6oalCjoi*$ zkZ9E1wL&~f`OL#X1A?jk!(fWg0HOqCYQ7U6;nQCvPT|CMtO{$w#Kn-}a*iu=c+RD4 zVp|pTN*q1hNAguHIV+V+mGR1DC@NVIC$+@Os=$HlzwzKAf9e-7CS(dEpLmaiIIi8? z%v*f*Je@T*ko6{Zn*q{zwX7aa|EA)OASEwAf?>=&YPGKM{+m2%*@IPLC%yFOaH2YeQh9RPh_9GJ(Ke~kw@(N87J(!)vA9t zRa??f!>EAgSpy3%3rhf61cH@v69XPCJdzi%5d6!X%gLFceHBYBj4Mx#G)`YJo!89B z_Nuxf^{OagZ;8qb>X&I=k#nK(M=(u1Y~J9F=kGeDCr&pI1Ysy8Ni4jxb(09XUH=8l z+YjiA&o9%otL;QK^}6#oz9;4Q+m}3HTla}4zLa{50AT>&-x6slOQ=tlp8XxQA_^|h zI?waB-Tc^BLZfW9HoA!v$tcfDCiGwd7iogP`?WKu=peCu8%UCX2nJRH&Q#7RrVrRv zb~i}sz0p;;xMI(p_vUcliv>XLVu!HH|NCXZ+w6BTB7ms==`LAzw?iwvAYx{o`Eg~w ztl9kg>yP;R=6XA;&S{3K$M5>xyc(z~`5S2JcU}O5J^(@z+;U=@Hd|r-OStoCJZ`8) z;R^B7%3E-2_E15Ly_~~P&Tl;E&dNTJkESyjy*L7PZ&hBJgrx8dVYn4HQoN5HuhB zgbD-)Fdd;2O4~TwRIfBqS}+JYD}UWt4p}MJ_C&oW@_sa#?D@(Jr=?1hrC1a=x{~sQ z197jm&^xYrebL)o$g~8CDu#$ZYysdPfS3lTP-0Jta+mwmeL^i0xn4{4Gnq2kF<;PA zz8E}g)JfTI2L?m<9atrYS2H!`5y-|O=8WhIe}}f${(%c!14a_~!26 zKg-S_s`JQAKJbc_Z|`gT1!*7AD_-)G(i9~SKy0y8R=(;J3lxzHLTu)eOphf;zprXP zxAWbGYR?Ira@z}OpvWXKdWYAt{64038D3Oj)9@&52XKn`9!?=d4BiKdzIZ7#HYg=E z@>#-!Z&Qb#b%6Gw)f$DfAWnFjr!!8;z03s2M@YLV%HdBB`0gRX6;_V zx^ebm;uO~n{=}BWCx}6$X6^t<5&-x@+h^}80MRiSW&6|gx%_Zg($+MfzST%myv+? z#h)G72B#?!E|w9zYUNVs77-PiE8R>u?)D+;!tq#k!k=$^swUez#+Iq392L(43qfC;LF<<&(5w*z9?KF9st zYXPpRZ<{gn6z4N#62ldN`Zuwpy?Q`*V1(Uj9yhmhN3GTPaVOBGeis-9?!woK3I$NC z#br$OXNu3|cKx}4sBqmVUikE#z6fFx;^Vi3Cl%XfHkMH%PD`2FSxMUZCITVAaQG;yY5{{A$`?e{XH)(Z6(vT7Ldd*NCi-H$ld97#nc zpx~Sy%5jyPSm=9gWQ7$>AZxWrWkR7b5Mq6@k_FAm_`fo57L5DSsYX5-?8Z6fmCGc?je2s&O!b8 zeJO2wcb4q$&ynsd)6!XsD+}((f5KSgHE&plt|k;lpw9_|6H^oM`Y>y1j@pwZCEPia zE5*^%;<*L&F)@p3%uvCCtGEf}SD5*MwFrK{fE5Ooz*imBd7W!jr*3^YdUI8X>k*Or%HGopO|nkc4o zs6xY0VlI+CBJehV-h$@B@#NR%sA;^iy^L57aL@^r(pmAlYWm0?*r%* zKmY(mSAL{J8R_CV*-Nfo)PD=l3rw|kDzgyp9zLb_q zFO?mIw1X?U1TU=UD%pWUg`^_#){jVKf`ui}mE*O@d%X1MC9)lDE*lyn~q-JrM;3cg$6(t06-wGReotsx8c{P zcW%)&RL#96pAqfr2Yx`MBnCqLL)y+i?TCH*ue+l+_T(rZX3h2McAZxVJp{_h{E00M z@3b1!@Znjxe?E6Y-!)is!rWGTDSV{wtITCMn#uI7z0rrRd8}qRY(2(k^she;`T&Y? zaAD@0BW~&xegBlfhiW3$Oqe>OBiP|L+@M1ZPFrv)J38pSw5nckJ90}tvAUzNa1F$oo5*CkO)oc&|55T6tjd`M?qG`!AW>`(tJi zu8mN}zYyzlYA|guZ|?j4b2T6N)rQy)WvxCNGm$#Jm>&cbtR_=5ZP#BRo_U2=31jRpYl-i{@4 zWSadymJHn19LvmSjJhLXiNNKjlmojh`}v0YPyT3K;y)M{E)1_g#(FF>+la#2a`|5M zeJQQ`!M(9xHAYZkT1|e5thYc&z)BqwsQC^TyLAa`IzmRzssqMzqctB80KmJwanki4 zv?;r2j`i_#r}tgUEIK01KG2D#>8?I#g?3-Ns_~=;c4&`Om`o$e>f>><0Y|=99-=ZP zmzCBB_ky7=sJ%#&6V<-^t0lZ5KkaLkc zqng)UIk)ep$cEZ=>F*^ppJ=+H?-)!xb=4#F^ImAjTdKoc=V0~68I_f~A3~K`aiuKJ z-MTGt`Z0VsJ5c>$4smDJX3jW&M)r~+8*@6!$Xx;ec!vVrb193sr%kd>IiWT458Rfi z=@NIRspg&#(;nJed&B3Ss=rvq`_R5i$LfgcyAc4w3RCEN`bEb-Jzlq{FRi^qb(r8o zB?bYc+LbRmqW}975-e^p?7nVFDgc17BvSc!G(^y+<_>-jxvXG>O8NVw^2-?i!98_1 z{MV}36(M0ESsN!~Ez4gV5I|1c6k1za%U<(%-4YK3O6v)L=O>?$6N<2gPaK)~hO7#K zQjHo`=KuiSfq;9La;W&wjLaunD|3q&BPGrH!-1wDu06WH{x{!T9ls(%9N-Ke6Ful9 zyk~AqRznIPaAIGYZlD^|OX)2sx8Ejc#iXIn9@QwOXPnX}Ap-yyVF9 znQtIRpi(|$uqv8#X*YI+@BHrS$fv@x;7EX&D^UKozjspE>N_}m#C{_Rfhjo(#%`#0|f9uMMaCSm5tp7pwUFkY9 z?}HWSx+rjP{nxts59}Hs{ZRwyEJ*mhClG*MiF;lNJ@ChE)&E9Jj$K}whAH4zi;ZIRlSRcs+7H;=&n?BMW_e#*+eF+ zsHr|E(WUa69No03>YB&*=+9J!_?V)fDwi+`0zguF{Y$ZLZRw8Prx~ndEWe7`kO*|% zjQqcF1oc4SYaI(%f!`k0>5xs__s`5-SQ*JpVYYTytTcy^Zm4CSQrR(;3O`J>?b;;S zoi}!NWVBcIru6OYY4UPUPH*eYqPCrb`mTh*@OW?XDWXy;4l*G#tKWRK>XIAIbaq6j zT>~q_i=D?&T#61sCp4r#GN(H7we17OzOawkg8~3}JL@4IoOHv{`=(?*jTF7Tc+)12 zVXAEm>|j#a4=TKao$j=@b!RI4!qz_e+_u5c^BpO)vnQ|jN;k34PT(={0s|&+^r3OM zs=AZ{3K-FazI%`k{Cq>@WnWm_br-ih#?c~+st8DrE_BZ1>?b$%n*Scrlot>Hz-YAo zX&bW>TkXY@E3zlb_)fe&vnwEWe+|FBP|Gj+vc@Yf_i0aT=rJDM(r>QaJEZMQ+Eo3p z^dOK<71cJ5%EN)R0+)7yfj3C_l}5m$sx+B)&kpnMrOk=kk8c^eh}pF0JcGmCfu~Q* zUV6*R@$V#UVrf{-3;Pb#0|0m{C`aHUQnX@n_ELC!vhpu&wJIek(oJRQrP9-3e*HoH zxiy{PKd9>23v=6F=EKmY)v4 z;Gnw7y(37YGIcz(ggtUCIaQSoJ-sjTk>a_ zrc>=$)Y0X^0T(}?qg|`^#%_Off8_Ts_L%FE7C}aVUmuYE5bGW&ydOb8R&`=JN4IT@ z|7gXe%oSA;Hd`>D6DeLoP=x`~Xw0s*&zM?aPk3d}*zd<=U;qGbBG59TNndeEt9>yj zy>7synX&4@J&|8N(H6dIe_Bt$X^H3@R~3|=7fOA>-ShjiMM9U3nheq0TVJXE_p6WT zxs5XoUvN?+@SowN(wvu6ne7 z=}j*uZt2NssS4@d#|#c%{2|P3D1>k#j#cn?^6nlwM55$X#0Pvs8@GNQ0 zeJE@IW0UaR83_R3O)La-j7F(L|6^6vssHqN{Rf`dt3L(zOImvkcIOq8K1kq=Vxa`n zRF0v)?$EyvzA(^?72A+NZ|C%QcIni(H8IZ!_giOR003_U0Mh@5)ma<5wH=S_4X=qA zf+h^*Au1PtER$}&okfEK41+J;htQx+8b09b;qMoA{`;O30{}cKa{>45j@;Haq;ChJ z)gpTV-jtxh&EuN#D{Q8iLIMC76(Au0GbqAR`4#!bcM6aOc2xfZs*j@3)Pyx>Y9%{rLd>3SntEe( z1Vz?FW=hqXe4vd6Ru8FG4hK6abOZU# zTZV2&NLUS8)U66Jw7eyA#>uU@3+LC{%M)QX0TF%>ArBtoMtsn$B2IZvnr>a&W#0Qp zTm1JgbsHPx+eOlw0vDSJAJxR+T1-u7y9R03b3M^}PH0J9%xyKrR}|e)?7r7q9);1w@#&2WLkbBu{ag{+qg&SYpS>*j(<)5&FoBtN+Nv-*Hk z8jYH)ZbqH8WckGOKQ3xU#}B#s&R6=$KKLC5K_w&Rcz1w@ab8}xK}7bX37JdedlAJ9 zUBJsAvIN+hF?OzLH&@F167|db4?KWUSbB`9^mjRy%1)TH^qP)XtR|7InYbulH|?Z+ z?c@`4wNp-t4(xlZ>Avs1&&9)wd&_y*3W7+#rMU|mtvR1PX6TpGYcoq>{F&Jh0;qWh z0xm5*cy7d-8&)oX!opl?kv6w#aK$&5Bv-7Ol(_MyFIHaDKE%^9sjB&f)mU{0Y8*Y^ zsXvj*Xl-$mw?gM;u@ZK}d4SoKx#JBQhcA8O0R;e1B>j=zy?0n#ixJmx!L_%lTBleo zN1U2(J@VXK)r1plGts~?K@u2b_9=v7yVu^wr@I03*Yr}9G=lO-+t6$>)|_=!3F zKbJ-exjEueFY8a=`AZ^93s#i7)CK%l(wh9_(Y3j=zWIFpho0C&pRFw6T>d+e;bdS} zR?BbbH6K_qDf20A6Hbfd1t^+uSpk@sVDqZOJTzpXykCZc0swfA)?UCl=gcK`MxJF^ z5UXo#O12#Jfn@W%3#^(+OPCQcq>GQFCxEb>m31cAV?lJvtYo--%Y$4qzCh&^WA5RK>_ROYEL~~lcxwPY%pBWVw+_o)#C-hbL zp?gx%G{}$VyUd3G08o*KgM`4S!gMB1h+5NL%!W4Wy1;(Og#-Zb?zBcG^Q6glZI?3J zWswS;nX+(2a`NI!bB#03cFlMcG1wYqAtuBo*A$Ut9Pn)~(XExnhyISq!LXepMR$k)_4mkBnT(|As0|bieQO5q0EBWlTU3hbWDX8pufB*pAq2jAv z7$@&)f^n;&sU|b?#7`zCANQ$zqG6WmA9O54aw)fv3xFh`_FAQrKP01Qvunr05DFmE zAmu6rO5~wQS9ia7Z1NkYxAaNnr;aS!>81UhFj*nfgbpSW1a3_+z3<*vLJw7z*%JV3 zY*2ysr;U!iX=DB5irk6ZRh@gq8WB*|eo_TnAV2-dFGo56fWuRK-`MRl7 zb8}9 zn>F}#mZMA~BXb_WC02Y2E~Z~v*nj7afri<82XT8yLuH+oqly3qxjXvkvy-dPiN%c3 zaly@)7;|Q7K57UG0N`yjUl5rCb1YYru70j=+LVDgXM8z5dEuo@4{OBEDHmUWXW*6l z-qy|tqG6gF>LPt@n-JAeIjsNicH`;Qyza9L`hSNkp_Y!I5o|~R%L-(jn(oA}UD$i$ z)sNLL7d}|83zx8^-+1=SL-e+27$EZibzygMxC9-C@A^3eO8#bdMk=)6LMp3^sv2tg zPrmf~`_H*{+t7?9SHPq&C_cFJo{NtU?>@qFry4TTJzMnLU`h}fZI@-Kv>=Y4=<=h| z-_Z=GlF2`oqeop@TwRtJ-PsM+Ss$F4yRh8bb)hy7WG=ghjE%tmSj-)Nsv}UUd`*b8 z#tji=90@^=L-+q-6*`gy6VI<+#cG8A5H|BfCY z_B;R-O15vrjw}TZmkvkR`ez;NE@(z`mbPRrmFa(#oGaS_RKytweRRgaf7C@daX8Ru zlt(cj51>l`=*a5(vNr92RBy3db&vEBqo~1aq!&=@aE$!0MK}O}*VsjAo?S=iZqBu) zA9wP;v%kFY!2I)n;$Xd6T72i-D*o#%0Wya=H_ksCG zK@!l5^<9@~+;KBUoN{jWG3Wm(AF6Dm`3#J(Ntc}#%RPpEe?pk+$TDGvmo^yL_#a~0 zytKB*)@RP72CHBeX4D4@EDzz&J9%1#J#oii`Ic!w_yKzwdNXLR@&Jna@+zW`s$Y)+ z51{-4%8m0`MokmnikWzv#gD^Z*KVeJ!7I>T+t?Ox*ADOe6T(&{J6Zf?S0XnH(9A9~Y?-wc4f zAG53M6A2c#mitn=hj>q^Ub`9(B@n6&0H8d|FEj6)3F_qC^t`kFzH9MCx7Z9RFeswC z|HkGQdW5pcA?XEh7s~)+1VaF|;+$jA6lQhRAwCJhZa-w94_;p(70?{#a|~Km0!{Ty zbmR^kymLiHYMFSA`sKcluMj}R`?iejx#(N=iT~bz{Ksx}ZHH>ia!acGha7XpSP~}h zFdkU~08lVkOIB&P^_s>j3ee*p0KneJY<+|^B}@(>0WU{6P%RRb7czh=a?l3wR=XhI zgGBst-^Yyy03|+bw%5(V<8L|5y^*)QdbwMM2xRt{}jZg)G z_5&thd$rF(0E`UGq8j2Md8N5>r6^znR0f3|fC8+>6N>(*e4}^c0sz3eQ!C@J>Xw?l zr+wzpd_~iIk{gnVK04wJFi=n|+kKD#5Q@b#0079j`6vQ7C|m#_mu*Dippb&)FWNYu zPi5=_fK+mlPdlt-*0he(WNlxIwQBOuq72GkZh`biHkq_#9D=Z4ILKcAxUgVY1q0>X zkX|C!KLCIcwRm9pWpXduGH>pI(?7GC6D^^+`EjZI!`dVm8-(S?-e|MW44kz6S*E8f zPV*R*r%dS)gAf!$84W4k&XU|7+c$taW{>-8m4A&?`UH@7l&Hk_4o(;@%RN_Zu2RAJ zY@Pvt^NjG`hJ0VpWR*sjF6002t)09Aue`cUPM zW9GQ8C+8@G0S5l9sH*&l~wr_YpATlxrl_q39Os2OL)ZV+|c)* zawyuC3_S<-l_Tt@r6j8A_hL`zX|Fv3mld2^;-aBcuie2CLjeGk^gmSU^Nv;+$uH_2w86~- zp;aao-r!7mE=566h2gZ62ILSL8p`$gMPT}II<)t+e|p@~V$CYySFV^J znDM2yaiVL?Fo|xZbBTTH?uqSu_BX`J+TJX{$PNQ3{(D`Zb)ClJsSIt48^z_ne-GEi zh%U4ZouNne4dPx{B__*(WnoH|aR^X^qsAzoRH#K1ofXItw5ME2&anvqYVjQ7FwWn; z?25bWipII+EA@fJmx@mfjcEo6XL|O;Uw-z6_|Db8GX@T{gUSOdaG7kIfUP_oX_{Yk zAoPb7Gt<{{i!xfic>7hGE7k1&-QgQtj>_7fz&K(N36gbTHVJtnyG6Sg;x!qf18Lex zi9dJ&#{>YV!5%m7_MUS24~80#Sgz%U%Ax!MmS)MYLt6Y0F^4*LR=oW9cPn?T`5ny+ zI9y(it4B1|;Ua&ojLOCh>0;l#Gj`pIDcNhJ@`q6w%Ynmm*?aqpXIAbvAByR!4Y(}# zM~wo@r&wdSl{Ka_u|V2|i*`O>83-fUj6-{gPlB&ep93h=`rXVi=YFhr`m%qaxgj>v z$`6VUvwp4_(?V{xt#RG|UOC~O|C&{^@xB|7opa#(LIeJP?v_*e3kt`ji>+Tb=vT5*IQN$3yt8xgGc1Uku9`|W# zLqy;Kyhr#V6uSJ}$@7nDpZ|fIa6W_7BCC)?^|4lN~n`6?= zcSCUv!`e(xd>@I-{<8>W=(n2^Utiji`*5{s&Ekx>qy)=A-by6!*qZ4YtJ|u6^VnYF z(YWDKR;Jj$-~kZEF`ri9%!BO(+{G>}l8%6~1G~h4tsTfXBrng4FIIz%UJn4`e%Fyn zzZ$*c#E<`xNz+Dy*H>Y;hEngMcR1Op$Hip5W3n#PU6T;!D>J)gMRka{o5UY7KhG|#Nz$Csr2 zN8ab|6nZ7+ptA8Eju`-exSuiy^&Efk^_gh>OzK)raT$ExLiss!m}XK;C~tb{|32G% z|9_uK4{U!~y81eznr{{VbqdJhhR~nhudV(0hK7%$FoBu{8i}>l602QGrDf$f_y1Pc z{9RWXS*8S}a=XM33dKzc0M*WkkcB8FVT$mq2HKepZ}AHsAjqf;0R#a8&$K&qD1F@$*^+?m4 zXFEbKR+?OQeW3iq*QraN=eWlFIbH*hDC%oN>-9j6>}k(vdvu>v07f4Gfa`Ci-R!I- zD+XGR{s-#q!2QKY8iH z4Kr7^)%;f8S$TJ5a!`1zz1*wMzwN<=0}sFf{|4M>YD$ZWXzJDtmH+bTM{1Yt8AKg2 z|Dct7CHNuMA6pEw)~H=Qw=#1&V#@t5S_q_;i@?gUbZGscrIxVLU^(bJeoBz_cU=^# zB0clp|04vewM(5!k;>1hF0H(qZh8EdXN7jIUE_ojx{CFEo#K0U0t%UsirJe+L;tq2 z;<6K(^j~~**3h-{8=VhQ>S{{;IpChkaPl_*18~JCEJeqfJX`Y6=d-kT^}guMw`{BW z-nKs4nJ`=|?vgCJMD_d@ac@?urQTHW?f~noqAATAp9p%084F|)h z0f6Cdzk&H5x;7hYm|bcXUwlycS*qo+|2e_fw_&prk;*>?T>iHK0aup?)R5u&GwtTO zwVibr%&ryFmbGLqSXiH5J~f`7uNkNc5&p&s!YImGq^m>2+aeN^Oib)oJOlgALx952aICyS*(F+PQxS@955uEG)o;RoE2<*I>HOPgegB z>krFP+1xsJ#*sDIvxG~$O-04C{#+)lNHVMK-8n!unVKl(zQOSU07A|4GTyu3+^^d- z9Mf2)gybFqcqyr_YeKsHXM*J)Ps;x~%cv?FVI7f(42(7Tp`I7(IN3I|ABlBKPvvK))`i%6TV_<{*#Ex_sFPAi`WsVrY&_rwseM8_GC#u zCUYv>NB6`303eh8#Ll^H%j}t{3CCPc!TMuTs)ir6zf~9^RJZ!356YPT8R_;LQu%$u z;Z`Of0LNHaErSXyQ=sHcNHZZf0OZ4hmuaBq9UU#Kl7H^RIE$UxlKdo6*1x#RQimrU z*L9iqG44NxVc!FQP~H84_su=?OAe;e>w8l@Bm0-AheonMyv{w2Qok-JsGZ=5Ut z8)!Hv`Kau9c)dWvGAnEID`wc2g~D#5O#VshUsRp&h)!%hy{;$vc!+X9QeWLWf0qFd zvHpWCM}3moxsq9baPeVkuX^9Qn=97+`A4o9r=zX2FOYZeykYW>3AZspLLZot{5*0{ zQDc7)2$_(K`8N-QpWZu&yG$y|-tIiA06?f`B2fSA)4wQ-NAPt?*0eyzpXnhzmg-nv z|MZO?XP6?7n?8`SDg+5F<5G0qjQo`q;p_yN^>vHQjHH|;d1{|-kNnYbM}8gL=mr32 z`E_OHzjo5Z!KNesfjT9#{sIz;RB%?e>ZXfCHYEciovKJSkTEHR*x#9yVDSs5^n3+5 zxD?ZQ;Qni_MU&5U=zsB}`|oQ2P%XbQ@t!&2tJ$;-j;#Kaj&c^g_!qfOO#lFOcm=#h{lm$%cB1D z0Dw|kH&JHFu^+>p=U4k=t{imzTin?npWXKpWRsH6atW3E zypc&Dz(Ll3Yy*IF0CJ3?YmZC)qB)USz+CE-hyhuZ{*ZkAyS5oWGl(CEPDc{}NaK&0 zjdIl!=jCG!3y327DJ}r0bzPc)Dz-lHT`iXsf@+%I2|NRN_oCwdJa#{MRPM@?C-h&% zY_$ogBxB@ip^3EqPwWr>dShQ`cSKr$?xV>+-va=2{t;=6t>)QhGODA3`(Mo8pmz0z zGdk7P$8{?+D1S1KoTbhABfhqi$tJvTyBO`apY+TGZ)B<$(7Lc}n^IL6M!>NmDZ| zN3O^>%s9o0*B`~hu_l*>V$#Bs0&pVgS{Y(zdqSzsjp6Q{kDCWxemLB>dlRy2l8@I5=Q8aV68i01s{V6#AMdRUA&qhWTYp~x zfKW$Spltp0lZD5jP}Izv$}hHqo{(`Za@C3_0kR#4|64w z`@7G$`bH9t>ByF^SxQ3Hkv_l}$j1d9kjj72tjtHgv#jSCd8#XTiQRq>wdTy=bX8mB z%@6JgKb$Z}N8-GhKvWmq+X#%^L&9+VV2_H>n0kbhM9`QtN zwEL7R?@iXuIGI{02=4iN*GONf& zb%GG${j4K>9(k*-U7Y;Jc{2yU4$MB#jYmpv`8n)3v@EUvx))-f(~mjF#?A z%5qxy*?yqoQI_|>^6xwSnkTJD%`EBi+l56ZC28o9uWi5zb(d%rou_|h6>6F`5da94 zj$ye}R$sGRB+`!No;S-r``2f7ZJkq_K8r~(Ad1+h^k;h5^?!PQ#f|suF#n`n|51|l z_a^{AQc5NO-O9!zP>~yeR5d}K@mT+!m5^HyW$%~+wm}&wYC7sZ^{Tt9c*AU&`3L1U zN)NI`mmz_}s8O;1q$}OJ0qd|hk^ic@=i94Wz3Q;zx-aQlYNJrC83sv%0u*>=PEDi&mza5 z{-pXEJpcejEvfWQq@uw!qxD#Y=S8zf@_bN2oorek?ARvMGNCfY{{@=!TSSlh^=I}zb>+f= zn?f3IV7B7$9}$=TRqKx#HaPmtmijAJwTIWmO`(srtpDr92M@i_x*n@)7O;$y<%(AU zkg>kdG^`Kx?$k2-sx4-LBbsQTF&i}h_$M&y( zK)9}3?nq!bhIeH=opcD(^ApjXHuD*$@BX9u?PNa!Xc0Lv4v zVU_h~*{KVUPuI_UpR~F{l+o&Xyo!A)5jEMt3g-o@Nvm{*z% zy>Dt}`MJ|_pPgHqJsS>%%<#ie88LVNP=TbMX1My!WYtsOc{XuHh`OV)_gsWhdPg4VAWxoH?^ zyrmoO_L6>MNBMmL02mT=R7X~{rV1(mqNs`iGX}0qCHM6~exY3AO*&$ftDk<#@T8-k z4gm}uWoxG{L#iM#vPn`Xt!eTU?oZ-Q$fxTR=@^&yTzbmSDYO`W<=K|AR=0;XBtZE& z8goly{NMp7!!L{BiD8>mw6i63O%{r|o&Etl=BjZeF*quoAgkm>372)!rGZFD{tQpvbkh;E+yFA6FgBVJY zc#h+cjmy}k0q(tY`=zT$#J{d?JZ+TpR_xTHKxN{c#mCv=)A6>Z;0mE*wj4~ri>fMFfAOL3&Zq?FYIP_;;mtZXjp z*mj2CvgWwn^d=E3Yz_027-`7FYGNDdAye0MMOQXbj|He95e;+w`HsXNKlO0KqV_cH zPZ&%aFUs#102n?8MZf6;NJg-3q_G(xUgK67fIxU7WZX?tX7>xP%-1xMAo9SUo%E^k)J9}*<{FqZGyMEK6Fbzl9$%G!@W8*)e!bgb3+M*{$0 zj{*p%i@yV075;STdWXsw3J6(Gro_r1J>9nlxCwm4%&WR^WYsG-v|r@X1E>twCtxs& z+hHVJLzK{2gNg$T8c|(Zd7%w_`iKH>h?V zhPrpIHZ#4uEVE)N24}PkefNKJWteEXok>RyY<`$~3t30n4OY)2VWJ6#U{Xg zU>XwIj=tDaKYXF;iuIjjt4#C}Lzi}El=Ao$Q~WUuBtN40{;pCXvGf3@mbV-z)EaV} z?sud6*Z&a~lVDzcW@1>z!u1e}b#K2{8))wmno%~w)Rw3n zRP~Raex!EU%iU-zH1~{Wm)~~)Fl?pWY`VQrAS)&xprRXdW3@AdXWc~k-Y0A)(ok&2 zs$XFz&tTaHDmOo&eEz_+S-Er7&tO2Y939HwqU%(k#)vx`adidn`XWH_VM{NO*?gjN zymD{s*PnQ}W!kSd#;>RH+Yud&2^OC*1pw+0NKZJF8|YBG?~3XcV~=kzRy%{65!H=@ z$`}R+Eu@9IckQa)^X&gHBO>ik`4)4)VpOJwNxXa8J?5SlS8*dU;w!_{=s!m+rgPLB zb!UK0em4McsNz$T({25cN3MOM;n=UOs=B;A3ClVaix3}=S^iM~0A3jiWd_@cYv&3S z)y1)luyRa-iW8}v%wjbY3oFOUSrKFqXV-4J|Nn*ZgWd9Ly-Xyf@|R)__`hmixa$iN z3KTie$fFR#j9T0qA1kZDym9Z~Qw%GebsMK?9n+m>v6>CaaR z=>>TGXd|q5((@3Z7;4yZ|L2Xim$!2xtd)ZfOddW005Cb>9)WcpE5zmat&fY^s^Gbb zQo)JA$({lI@$as#Ip>N;>QB69ckBU~_#+7k2*UsosJYKQ02vPxSdL-j26rpsjw14` z50~Jw)l-%Xw+5H<-GcJ5P+SXbdGfCMSDyF*k5o|kJ!b@ezwLzLdgX!j|C@OJt{=Nb znBcMzfeH-7h_mX%iG}6}{w7MVtUb8-m`D?0a!tlC&WjzPKYsPex+PaW+Hjgw{QH4L zs6r|}@<1YR_YXt-F$@GW;C7B1sh%yOcE+h#rJ{?Yr_h8$x(s9wnfQYU>?y0 zI;uLm46)b7|Ak(Cs`Av*nP zk)E*NLXsU~NKFeByFWm1S@`0%88>PNcRtbd$j{D_rGJ4HO*$`&rG!}DW|+4`o!V)M z{34mcQx^*Ey}SWbngl|nVyhvMJndWCnfS{SdyU)Ic7-18%2H;^%0XBY%3b7v;*V|s zz#cOdG6wdqFYNCtW=o*-8X0rVSS(jRZE2!!-<@1DDUe~78Syxx8j$*etzu2igu{UK5K(-{Jaa1Kk01;&M$B+jAB{G>E z*9JPa(0r;(mZusp*n(oN2!rJ#6Xu^2MZ|**XBa{+X9_6YhZv3*@`O4Fs$9(8l zR=B!}*cn71SqI?}euo}`e6xy&kPyw4@xEcFhw7jG{b%BvAGw8rhG_;Vr}C>ylozQ< z7+yZrx)f;)W%HrVeSOBpP5tJ|^_}#wZ3FrXof$0;N*|Ky9@Ci&RxsoO_-J3p6#&2+ zomkl5lN(HhQ(c=V+QpdJgL-Wu1(!k_nKVyXwmDshwBS6C*i|`6%`1RT}+~fq_lLS-3q>@u7=T^M( z^l$1m-T!sHXK$DEf+#QmA|K>IWbDo=O-7&XFz>qa<%XZ^7z%Ib&fsL;Q7gif(gO!S z_UfdZkFzn|krF!n_?iB=5=NELqLJwlB4o%Q)P5n@Gy*^% z6ZkSeroOM4>I2wEv{dgaRlA?LzV6xEF0I=9@GaQNW|$e#aJkl^z2Os4O+aF8hrVIY zkg?Blg=G*V|)MF(Os>f&aZr%+%fOn(-&<}u7sAuPsxC9e(>J&+@H#7CvODcA( zybb%2pNq?Kh4f5Zs#DzG-c!?a+qbLN-}7y&ZtDD8{q&RaRTGYLBNfx#aAlRu7Df?- z&6ub4e5xO(`*wy0+Fl5C?|fSC+4H=XOENCg2*5^+y(FM7Eog-ysPM|OYSlLj>h|Lk z08~IMOJkwFeb1ZLP?t?Z4JzU*R#{A?-Sa3@H~nm@cFIhnd)M}nUW0Xbw?PQy?#oJr z3&~2CxyJs@>qC3jua}noIwW%?+>B5O06^d=v!xJEs!^KiO_{xWRtpIRMXuRnY4u&EnK(1!#4Aae)?mFO zAOPisGi-#3Oy+4^4^x>iz$l+(T0WiCtaL_(43H_r+z3sHEjDw`XfNM$AFZU6$K0RUik zKi6~=Y2WgY?qo8B{rN>a%xYJl>&O^-aMr1xhRQ%Wcl5uP8}-_caV%goyl04son_R{ zx^7L&g#J@L^`rJPu6Y=#A*Ub$7%c!G!vKTo$Ez76-%dFKwv#9#syd7qY0JqKAdIOC)*C3dX(jn=zwpU^|Z4-GPbysGjZ zNdYD*AaG z6Hxm2D=*!O(*sOEIn?K+&;S7b2KrU2I7;k%<~INUrJ&IUK#{=02U?H5C_4T48>8D- zJp^<5<)rBvCwh*(N361Qp0Yy)LuTrw6=#a8h)~4CapF`?nUkrTzSL?tVtKx5!V)V~ zQKx<$+N13Z^P0yBxqxBwfw4wii$`{?{R2?K`>ESA0D!~RPA;nfAuOu_PeupZUdqIq z7LcO01|I@7vJ%jH^ap<-f7fiB&pN)D5g2=lPhd^5*XzfiKCd>n6#5*1wyn~wYMN5USYy?C%HXp80i4i!@=_@lxdO zJ$bNnSw>?0)0cia(z)%S=$6OtX5oZBk~CdoS-kt3afTM0bJ^e#r+>x8G{VeiJtsO0 zHi6ZanHLvEpAmK}ArpDb`*)alIRSilXB2u!WC=NzJn(#E|M~~z*?~MyVEx}leBBBw zG+jf3-I>UaXRe0@7A0Gz0WFAj4s|a2$jwgO$92JNL^4=aHp&BT^HEI4aoeMAc zb!FCWv|^7^RFD$8My)8dY877+RjnO+)Yht6MQhX+vy@P! zX3T1g+N(At2%=A(f8x16+#l~b_ndp)=bq2!^Lhb5p6kth=+ey$DtSrBxIFWggC;VZ z$M{8FNUsC%^1is;_UU{>H>^ruCS0PXeFFMqtA0Bt)0g&+WPH*ctZy%$k#SW`-T z%Xoz?Q5DEzylaqy|3?(UHB05juunvebbilz#J)=T(N8LxBUgx&ki9)Cte_>ctYu|k zD^kF5nG*8PpX8T?&e+IE*~HR}cg=}u_|{kL^q|j!4u;J;C5OJ$hnpB8JR*lXK_Al> z@=bJ`)kwNNI&^=BaDk#pybA8o~B_on`9wii7ctvxZy72zg;$**n+&^%s8qZGZIWSWb3MUN=dJ7HDfW^ zdex9Bvg7tat3BF5|BPRJIWNI9Ndb%6%q*Ihf%f4(^SmG>GXfxEaYL`oioN>Yv?l20 zyT?h`>TTt|1k33aUZhq3rzWqu^@p*W$~%>h;v?cl^ zhF8KDf;v3p+_p*a{=vU=HpuIUF7}&aBx_zwvw7zh05rGE@~2f0SW7w7Z{6!A=kXZy zQoN2>gPk1ZpaY6jE}i#nqYsS{aGm5+?BH8-{=o+bC<^yEIY8o#?yCHhM*_t{A*eA* z@l-8vP7dlM(0KJkSeO(Hn#`E@pin&no5=lO5!vO9-s{fE-!Ps`@~|rlyQsO=Pn7Ak z8rc~Z5PNDl{R-lBo$Q7Ds@%BQyGeyd|E3IYF-SCwBL8CQe|WsdLTtxFqPG?M0;pkK zb5CLf6F0?kQy-eBUNi84PojffcEWb6OuhOn*qz=|MJR~|n2u?I582cA_s-T!1%eZ_v@GF| zi`iA)POoy-*~NNLUjYAB`1{dC*P-qrov26jr|=17tU156Ay)JndNDn!DAF6brN zjIq0;q1B8UubzcQ_qQ&kPrb9vv0M zn9+_q0vL4siyA~HgVt8CZ`6IY4)WF))0S*xI78Xomf4T$?L&JW-U)Rb7}YlpxN36^ z_$+g~K!tfWlnj2WgJtPUcjmv&X~K7njiDP+(<8@-)=eF~z(NBsGxrNtx5_&pMTNa{ zljAo1NtgQP4v?w&6sJ19rvPhXp%_|VSm?v`Ccmfa%2S`~?nqhB2JUKW>^U4>A8g)g zy6d=11E5@^9P2;T`mT0MleLJx5*?g^dG(zJ#ISNruE?NYtFet~dgnT<6}|u9t-j*Y zEBT4zxv(d^=thL4)su~3c}vdCvQiMbWIm+FSKc;XPM)Mnh9rQ-&=YaqC3Z`(BnQi& zZ(rI?BHQk5Vz!?JZDgdLNMjE-J9KN#$^6p$>gyC$kl|*(pD^7L8&|rrN9d6WLi?V2 zi0Ik-oX9Vo@B5sHn+r-h3kf&0dh6jmta-k;>L{t>fA%dCBmHm77R~`tB^-3-HMtWV zztYn8XY!~2O$Z*5WEP6fb_3}*%?1OPPzkSZ4S7LK7b0K>p?mlp-T)^bkfa)FubgzX z%#c%=8>Hzo_%W49agi3$xhuq6sgRkV6#E#IM0_k;N|z(PwTT+pKwA7?rggw z*dZ-2=>gMM1_~7sYuJ~<4Hl?9^h)X1k$>k(6Pj%>sG?p)lGlo)oMe(C zN`jbM^Rhd;A&qYLuhc#E(j}3g?plW*m$x$)>2IIRHg6(5L)O|%rmaOeZfJM?5&(2L zwF_^Z8fN{9?;XH4T(4&r1?*sQ?84X}f2872+JQieKKbktJvl{*ssAx(?sPfbb8u#A z?0Cimnl3K6oco-Qqx4pB+jnwjwR}mzanhFHQZBg+ueejUz46!zx5hb{S>0!VzlJYiHAJh?N$6_A5NtKBC2d*x}nR+=!) z4eFggQt*OenFFF=*^u;ZbEw|)0V?V6fA*G(Y0tm;OH7I#1209CcSXyw_q3GfXH8$# zuGTf4_|*g9;Zv_Hum(MJF)KuC@9M;7f{A#ejr1OBRg3Y#Bt5lgUQb<6-2vqcMlzG@ zoS=_+{BE)GuOVte*oL_(|wFLkUG#$;1-c=X>?PJX&$W&GZkf1mu7)2+* z1$cmAQb$<0t(-Q!}szM zS1gwo8fpSR*FK#vlPWlspldfy?SO-?=iuNl#(AM@%HWcLZuf#tMEUM8vuLCBT&!v? z)wEG;UZ~3i)QAPgv`oU{6guJLUXZa zsUV}`e$>zwC0}5)c_usDS0u$tN#i^@CsQdbT?>>!L*GdTDZ(k^H4&``tGbwz7C30BnmD z{3G&_Ib&02yUJ|CG5Q$SVBGXcG1-2n&NGiuGjd`{2BQ3QnL3%U7gSHc>==);wznHe zJ!Sq8L{7y=Y~f6AK);$IQuYT!NB?DrdWyPum4Tw7(h3#MY}>A6$K#S@Yvxb?wA?7T zn(8xkldYsvi(DWl+Mpk#M05Ly7qa0qiSrp!8hNt+CtcQ8O6id1H5iq;PW$Gy z&-B*mJIl^cbnitldI&>fmqw9Ae#@>Jg|eA(PBiy^HU~O?3|&PAx~?XL5PDQ%sEf?^ zvFGDK?%(SDjPTLyH>WP~^FOb82x#|4-kP2l>aeZj9V%z~qmT6lXg(?d>0Fs3FD`jIP70gxe}= z)d7(RZlT%CR-TXbHK`J_O4km@6^mhD>^epjm^EOvS~$gO5JDCNvK+NjWEYmkx}CH% z4Tvn`#;>{z+dn$BaaVeC1HrIWKDxJNj3UJ=ch%r4p{fP{W$uUd9;2^8`e$c;t^~QX zkJK1!!RW<%5RLetAmzTabU1`FZWW?RI%m9eFF5Ue;kwhUHSHG=q2`{1U@nd;wh6%8 zky53+xYgi(Hpy)ZB3eBiZM(!c-*nTY1FQ)~tk42vo5NvmEG2aI^1?f!vpo_u##Hx; zM{S$T-$hEm)|dMtiII=WrfoE!6-;|yyTps-_+wOC7R+0m*I5Z2I|YE=tZ-PGZkj+3 zk$gcXitYb4C?bYEg)0gDoU~>6DA&@hcvfW{Xhg|#Hhkx-l$la zxq+mHX=ZLg?Ash0c7$HKjrp@f!pK@nZo2SStg3-5HFb7VvuXMDhmj}6FGJVb!{8MsxdvbnyRtb4aY;=VMKV^?<8-NSo~ z%$zTi6d+u5x?O{n8m13i>g_R{=J5G;e$aox_g*@#+NC!ez1r%yw0~d>q9`iA;zOcD z$a0~b}jMkM!xOEMSs$Y#k9qcRG zjP5HIU6XbQyD%L9l^k3t#;C7?Q6Bu~|J=1_bTozVuBhdTyl`r2!6Q@XTF=g%S?*-N zeE4^EEA7euc*^s-yAyayo2awG%Q zlhjcmNku>u=pJ0z%5`hpMp&SHFt)OiZO2>vqdpZ^Jq!RowdmJ?Sw})sVS6m#w+sg#;ZF7%sDr-jXWyiFA3S=2c>QsX zQ`2?DD6Ei8I*pT|NxW-hM!8_?HQg`ZKM{%sy+2dDg-1sA>Q?7`H|IkIg9~mlrTIc1 z!E^hE=7gT8Z8ROxjNT^O69;4H$dF>~+@7{v#)2}!b9e8)Zd^7~h+_8Vs#eHV!fICu zE7{Zb2*~sS)Cm%xq%V%=c-RQ!AR9p=$ok~}pZ)RrKPubJ5Zv9}-Q6uX3ka^k-Q6L$I|O$L?(Q07EjHgde_>zltDa|` zneOW9(zmK3l@+Cs5eX0h006R#w74n&0Qva{34n+F93H$DUjcwYNEvYvb+7D;9JoLO zjf{UvozFd-PCg1w#YrKlzh?QQ(M6ycYYR_wwe{U=fLaCE(#E~5HUvh z?;-yV0QKJpU_fL2e`7u*#Q(kcU*iX?901T)0Jy%3e)(dDDrVC3w&DqTk5#S%RXzZR z-m8E3VP|Xu0M7Y=5T73k7I}1l(dnG^7xx(cMJ3C+M3#V)5*0_J{}HuE*-*zm>+D}6 z-zzvAP)a^NQ#3tbgXj~}(LvGP9I`MUb@QYI`@%t?0Dv#tm(LCSfWQUL>n{0KO6Mz} zYid_51jiQ(f4}+DE-q20|G`XPQqH`GVz-eF9T_ z6Ll)ij19Y9AU#ctS*um};slkGeswlc*7aZvP6pRHd3HBf?uoBqWC)`LG4Tu{Rm}4 z)K~eUz`aIfyjEPWRzG(gE$y(JzfuTej0M1k5&Q^?Ho*qutDZAhg3-nsuZA&g@pXcYKC+cqUP{&0N3_3V7%*=kohO za#25IuH_;_C`!zO)=pU{Uuakw51Qae`G~wKy^L$fL}j{Af<39Ino>-AI?FzW zm9=^*%~&rTXHy54!&^sEPoKM7GFGm3+zBxcP!2^@O&nqohi2r>|(TGoZ@m%n4!trRyZ($9e zs-PgTm?s)um_X~(LQMJ|bsaJut^BxHL(m=5=8Hw04>C*bUrp`JiT$irWY=wI#*hF) z6&!KIh?iLvNCb4q{I3>JcmRO1Wq3z1w2I29-2Sfb8oT9MXPpal=DavcM zFTG4WVctWTiMll+#)+icT_HG?#y+m0Z#bEU*=ChkB9PJxLH0JtGz1DcoD{26w*VKD zqz#LQ6&;a!Q>52c&S@NYn__r=x&(Skn()bK2Ek+!yCeMe(C=FYZi~Y^-)6y~L$2Bg zpIE^BM-MDqyLJ=qzxE#Cu09oIN>Ud*f~j9|&XyiW#kS!y!i$Z+$oQ~& zzMtOgaA_wL(kdBx*3|LS9lk-;Z4IFYt3%Dv2bwYzl&|=wwK&N(;I4dGeeQ1C^aiK6 zPfcu!_3W%U*2@@wF%C;n42Is88gPIjOLm3&)C&01U1H5w-M5qa7CP%tN5l<+y%0r* zQrbK=2G3fzh;gQU;xBYeBjM=!wk&xews*H!M9YcGPYPnI(lIqFMSE1edp z(~8onSznyU1D3M;9LrU_9=ALl@WB~GeFb?EQPfMg%;70Z2QvXeB*Xr*GvvJW)S(Xg z9%+nMrvQOQJOTqTQ`*?QvfT8GfQbv502TgV8uA0|=)I3`WXdyT8oSk^k zYtyDw()jtN^dZi!2J|m`jRpxq0?kZ6W;*O$9hAmbP$lxyOIX=Q0ukq-F;X$`=I)?B zcZ&CdSK@TGZ4l%wf`=qr$QGOMhsObwmOY=@X)4d8gJ<%WO{6jxF74<66~p|te^AIv z)+FFwq@1%5(u^zu1#RnmBoA0X3r;I*k+|0BP^dYX__Nbt5<+i#r8&OW8`=k8-TO}w zB+qughW(@0;_Y&}I`-&i(bhdB=E+<<_r$1=E$7zj&MXf$kqWOB@Z&QTJF zIzGp`WJcjJAcbiz8`g6?MQwg%ru>;XUSc?;ui!a$o=f9Zd)L&OG#Q*Ah3y2Bt{9n! zj)`wPLgu!?aXOTjyM+T<7_6%V+W#4uS8t;mp(;rJwbMc_4ii}PH& zwt4S5abFWztQujkmVigB_Jxp;wOEi+NS_XV$C{*1mAY5+onapxzY#lg&QhVby5>HU zeH3S_Eai{3+MlxEDj(@zdjfvSuau-3wq(V~d@|?@JOuVdHK!*~sP;{c-K|wGS5H@6 z=O+(=afre`0B+v9|2z&iB3zyJloAr8+d`S3qm28}$%c)V(v+XA-_H&-`WexlrzZ_( z7gQ6v10<$O>o4iS^0MXaEkP>db>5F-H5a2m1F?YSE}`XNEG{ni@-1w{FAzHsW90u) z4NuahYLZuRG-Fd2pg&)xr7+fIC1MBuS;GM?ENpeE%{6mWg@jKI&?(5PVkOAM5J{-% z-P!9egUbvEI39V`GupVQQQcSw0~D4X=Cgr~hXV&0;`;!AKuYIXS(1} zLHDtci^m`8lfr9k$wat%kAZtEzF}fuEJjUvRPOh`;8LS^i&&3|M5e8%?^fmGi6dyr zkQ9)8yNPnfpZ?o~-?-EJosS-@WHT(p41YgCY!ZOTsOgd(r38AX9u%1))5v;TB@JjT zJ?kY|BLG(PrLaM#Z%HUq0*HKGpg+SwA`OD-uw$h8wY@h7oWqspRFDd;7o^#jj}fIt0`PUFE|Nx zQnvepo%YMS%MZO>Tc&vlxaL@X^?1`{FRn5zEcl$h9DmKN0^x(I@b*36(2XdHpqbJEsLNE*Esb{O>n zDu`V8Qwrg!sR%I%vpxIUE<6?pWTsLfUhbA-atdTACLYiOn*h#e{}~`S#)d~!waJwG z$ea3_t#5+t&-b*Cpo46epGJ#Oy)w=VNu*dp^}zK?JoLUb*nHlEp-Z;N#Q`~*qq^9o z#RJab99-Y9wg^_4pS>YuOn?ia|CrxTL`L**mS&STcIx2>D#PvA?(p%sp0OqJj=5>b z7^W4ug@O}dowQj+BZJqNawO_H30BEyJ>~9CJ^Z<3he)DjxeO?V2?V7Y{Wlwgv;`5o zrRCi_f?VNqUZBw33uXtHn>UwN?cZX|E_PG-zk)tgZYsUn@06l8gR0{AP695~Z_}7r z?`9p1{|>bl8U*FMEx)0MS_A6q{@e4LueE#IHm>#&PwKIJW8B9`@U+m3)9D);26t)q z_k}*VG~Q>3z5*In=wN(E649yKjfP`|JE6t&dPF&&{h2S=zoL{O<^aaq=J=lmqTUHh zfK>v{KblLUV3iB!2k`u?!e1y>l}*?AEXnj1=ld-L3LnjH^CMAd2WJZMY#R@;;~c(7 z{r<>qAD#GS2ssFe;Jz%#Pl>yLiXltg*G{js$5mp^elAh_2F%jV>5;Z>j#r?>%|Q{) z{})6#87C?t;*BlSn#<1s>cOAVoVo+AQm!w|$Eyxc=816wRx$r$8X?kZ{s1sv_}`GN z&~4}o)Hd)fCP>e8z6LY+rOC^aZ~)Y&vRO)VM}>}^XvSa4;BhyM+2v`jZ2!V2uq^37 zOqOL*nID8!Px#+tTPSdviwCcb+2d|yI#vO0BF9ksXB&-e6VM~#b^$PkLae5AB{1%i zfry0du}GulwK;Rbl}7kcLi1UB`~&r&ti{lQIp##4%)0SO*Sw<7i|pW*;9_#^1u3bu zg7ZR?ZI1S{+yVOMP?B^d6e7gPH`pXb_Md;d9{C=zE&jphTt()%sPmUl0=-Tqy4X@pytc`_rp+=^aMRI)D`1b{)88u^jprvY^P_08V~CS1@((_Y-@B18R|1|6wrCUbA63~D zqsK`B44JA1ff(LnO67JRf0(Quz%`tT+1c@v4tR7ZU$y?i%K?%fVOO@0(-7MeBkGtg z-WJ10S>D>my`_EmgUQhB0ri-x-v{?{2S=B%w(fwq7HP4BD~tTF{>(>ml)a?l0xR=`jj_8@J9mUfb zOg9dkm_XO0gwOwV9PNWEj$h_$?ZjKVB;NhzRv&urAd`~&df)=N28%~R%zLI5ih7&s zk0YaL%&ndeEAT^LB!zj#WIpBKA_P}t(M))Lb-cr={gIFa47G-qTqx~!?{6yVyhsVK24 zc)xm}d!gp>7-9E|(LEH3P^G)ks5YK@kyw?#Nw{d$N zj0)mOZR74%ynK&G!}}4QvXeOt|k$?i+ylFnshJwsaiLW~1x;YjE2`2E{bfk}*vjD@v_BpYXXwwG7biEIm+4T{Av9iBcJ9cCUMznF@)o(o-Cj4L^rVm1Rfkc^4@O7pIQ9l3;|?+a3Wn%_c}S;=oCQv>FhIL$+<_3#Zsk1q%#R((kbu~(%eOG4 z$Od5dt(SMmkZz}$uQRN6tP5L% z+kjs|6!Pf^#OWVDI>^~OS0FAx!M?7v0ZLLY*5|0;G0EbEPDr9mE{kp{rby)~QslIfIQpj-Vo zTR^q~`_I5gloV9Fn6Fynq1TzjCYK_5zJEs7M> zi$N4PN|z5_4r{4*`TSJ?+sh>^wl1YRAhvwusJw)Ep0|j`cYXt>-C32w_q9BPi`aw( zKbhFpOjw#o98SKU5D)4tjJ|*NiFhf@6kMgWLi!vZ+py%J@@bYLK9foK?b4-jqhq25 zoSQql=vT1sr%9u2w{y945tTiU{rh*pDkNjRJHIE(6J@Rp_nG?RT@?7pUiah3=;{wI zjLQ6u3h0?GF~mlN1$#@Sq&Mj|sY7ORu z{(r{qGZQ6fb@@w9{vOx-Jf-OrKfYt%a*dw%4WIkB`=QvIKTXtg=34LZu|vgu@9eHqe6WsG%i}z&+LzJEn%8bR ze`2;3L&4=aSKYe{i}v_H>JsH%@5Wl;jYSN+Xe)J9-e{UxcAOS-mOO*3_5o8vs6AdP zvhp!sI|(?IAORCc2<7n_=2pv>$7h;jly>biff7nd_*du{<30dD6pDKFY=Yq~lZ_MQeS4m1#)d)&-*4!x2f43ApW z1XvEJlyR49G`1Ftcl-E6S&X(C*#h2F{F~dBd~Xg(K8a35C`^Z>h~!})p+|zzmK@s$JW}bR<4)Y@I6in z-d?5RAxJE6KO^S?H0?E`qa$XFoNJ4PTE<5M6h+U{!DCFZiAw`Nb}DhM{Lpso_lbQu#gJ zF&2D%y{ry;juI@(vXPSW)nBBeFeB?@<{k3=t<55=uxo5s24|au?heNC9$u$?`pDvh zR%8tWq{S>C)}h10a%DKmC6s?OglAeZJK&xts!Y1CLD#~?6@$LO0$5eqe)_30-__+M z_3RonhC8B@a0EA-UcYAXt?1T&vqf-=WqCeXUIp3>e+Wt5W+{3#kS!v%gD5Guqnja< zk;d4wVZ-{o)fU&ccwF9&2`PS#lILX{iCXrmsuxI91{KIcGkSTQN|BH@8ZQ8t#WO;( zASh)t6{LJEAk=xJS{sc647rE#ZR{fvcC!pERd~QhJJfZFV;rd*( z_*@l5X~d`)k&5_EMXe&=@tTeY5)ui1?S>_OC=a#BTkU<9s;`CJv3-+K0Fsy)r=ewd}uH9X4=KC(hbY% z;;Q|rf*xKt+q~~&Kk3l`J*C1+h;$(Sn3{w6B*7d3saP9=?@o@^@nb$FY_d7HQFh|3 zYA1k)9m)sV$I^Fj%E+(w#X+f>>8nRQ8Qc29zLfHkP$Fr5+#ls&=VWKQeYEpVPrE6D z41Qfd`yAgW;$4qKGYu1gukv?_rt~*9X0EtGOX%GWzNzL8VF(xdMM!vnz6g=<#=piS zw-c7-YIPYLH*y(TlD{-3H-0P>7%_T^QbrWJnJ;?6{6t7 zG_*I)-5OO$Ex)P<vnv^*jGY zx9be$tRDS;LkFOVp1<+!vV)3zA_pQitWl3?r)9J%PBn2&=x;&`h z&<)Lyc+(Hh_T#^Q+&>$Y`^p9F3yyO$#?CN#N|c-VYm<={0W+2!fw9dh2QTwYs$oHf zQV^d;RSJ9m(cJ&*Q5?lS`10S?+pmcXnJ=kt9u>x}1iPs%0UMR~vKYIOKe@^K!puk{DsT(8{1+y#MZDwvI8xyp7=9IvK&!K(&t) zvZSDN=D7I-1q*Qg4ikhg7++yg@xkqJ@}!%Z$q>v4wAhprD8J(}Q-jmEoUaqo^4!f! zy$i1-E^q_kxq`4mWIojYMtNA+JY7vS_Yz^cY$7~f@n~nFceGV!!NF7j;%*^x-1U-WD)VJ#6Z}!?q5I~7F@{tHQ?9cn>hiRgd`WOMb&OuvN#ef4D#iRx`RUR7 z+P7U5@9X7e?ros1E4eec@i6}L+*&Dwybw|6J+$ns=Owm`kXcf`1OZ!~@aG{vKU@}D z|DVU%(PNaYwQQ4 zM()@BQntL;yT;hOCs!WqRy6iqXA#AnKq=N|V2xUOhjDmo? zH?J)Xb|3bk3nO1g0YezcY9C3oGD1ZlC4IOFLSRuLT65?5P~pi4kHAM6D8F1)WUEs@ zrPnE}FyC7K{XOLPVIoAxgn{6u14NNvM5t2T$&%H%oA4T)cWncL zXfAH22_J9rVCHmS{?&J~|B1NOkDyx?{w{CIK#)?v5kmxUC#LV^1;-zx!_q#L?DK-< zi)Hsr_v$WCy3mg%@0eE1SOWGEO4SV4E_!sU1o+*c06!q>xnfML*A8%?RXm-f-@8j0 zj;6{<`9~iXP<78z|LP!=!pdHZ7J;=bR(=R|r!dFa7U8QxNC}e+VgRJDO3I%C=0=Z8 zz*)SuO>~=~EUm5dF?X#c&BJ!S2WGl2b!V+Uwq~n)`R_sg!l_3Hi@*3m(`pZdy@s0i zhK1c`#0adQ`-xg1apm`OwTD^+Vxt_DV@@lOu{Fj?+2EJ2tj-50JR;vq-2BQEqI0|Q zo3D9yDI_LXADoi;oBhuZ)z`O$?zGo+&R=cFPw6ZvAnTC={f8G}tXZ@dS2te!e`gwj zd~_c5A}FVDZSV;ov@?D1zMg+P{sTL$e@}Nowh0{ktPS-g@NB*g;z}=zQ@!pS#MNbr zlhPDli2CV=e^Xff7zVfW`oHcD>2>umV|lWM9trjZ88dt&bJE!n*?^2~rnZ6IO@6Te zH8?76O|`5|&pZwqPusOYNJL1~`^^8?bmYvKdZ=!|w%P0BZhB+SOh;SZMF7T|$>WiH zX$4<4#;@pqDvbGG9HwS3m_VpPYv}w!^m=(h2n!;FKJ8YB^4|x-#xqDJgFupk(#+UmbVgM0^c{2TZ zWZVe>TmkM;3`3XP?G9O_Yef;l8noO%)&7;W-pysa#}LkFx~YW^)LuChI$-C<=^OFL zgiDMdzsI_Q%eU_$bcMjOam#BMe^5qQ3J{47xKrQoEDhjHX!@=4E5y3{z{LGDWb$}~ zw*3-us6cFI7lH!Nw<|D-myjJl)YYJPK5FUIYw*UvNnaEP6j{xCStf9LhsyIQKB7ob zvN=>j*tms5p{$dDWYy&_V|r8UM_4|VO&x6imLTHB_s8{pOr+FGA0;Qn%Cr%+dCe^#74cLa!bJy?7?w z_rYAzbBt=ICnUemeNr1aUTj9j0oBhR%xr)8vgpDTptn3qR0~5A<`D)DaBhQ_1o=pl zaFO#~LaD7esr3vT?t!ux<&qgRZ*D4;_}99u4TR{azKWZK2{{kV@(PK<__y>rx_I!1 zSuLXE&_m-`6w);ahhUybj&j31Ju8g)I#kjvLK2zcBY%1<&Sa`TuCq&OYFy+xWU8dr z>agS6>V{c*$UUy#@(lTJ6S2F$rnyfllPZJz&^N+IR%qpUuK?{Y^yd?daFsw&2Sd)m z5_v4)*Dq6!kAn62973G?m=T||8?yxsHa0+Bliu%-{I87r9~(Phgt; z=qBpCc$qTwH&E9hx_W_! zt4=xwPV*+a5~KpS`4eqc-;}mP#Dh|!pIN99sYGW!}6`;-xbP#pg$@743G#?)8usz9}O)Td&}L&iW<1Y4`q^SAW-=UrVh59vlsi5t@FpgUt{X!C*Dr@rk!BT6{f! zJvk$-Ap{b~Nn1jjmppj}OJ~nVWR&yBmeq1xbU2lIe zCy!t+UVUO=!hfsXA`)o{o$~1Y-R26DjB2@~E+*f=mpX1F)Xn3K6iY^-esZoBRs_(v z&d%)U7abttxBlWzcfj!#!r8jHgd)P``Xhajw&{6B^Z=AqXzP}un=IH?QMzoHf~Bg8 zYw4DS0QVJ98g^XPgWTan$Ij?r)a#|bu_F7TU5AkH;Zqh!OJ zcdvadAIYXiC^Z0Omweb;FX<=h)r)RrH7(T;@5U%TOWHpxUZIaO17 z5IMdFw^Kr} zKh%X7OGuM(QkA447E80H3cDUw4jsjIm;aRUbO@v2%e#nh#16fHjkGz2%A%OSDQSq;=j*&7JUxXX*z5D&xjm=r3#}3OcnCK?2YMW*+o^?O z7w@6qeWKT5!T8I&uESCs!)COx_+E7tv0UR%@Vzm+tUU}pRN}F-WRi2|<#cTGO2!Ej z`_ud~)m{Yxzr$_@qJ~e89& z963Q{{#CBP>TV+U=0PK-h?3+*Pw%^bfGglf3^`=!N4K=LHTqI5m&WAKirf0O#3Hzj z?d`&sTdzZklh9QzE4BNYYI}IS~N(T`&u&%Gt zh6i+J`~U0MANHqEYMLWd)iJAucCFU6dzY|^o6dP}jg7Rbj0SO&Avyy-5iX)cv5fm< zZntdxRqbe{6z@wzBHrt5YfE}4C4F>a4YKrCs(Mze@du0QrAwQqWX&g~E?)EQwvO$s z?+2$h-JGIdE1_OiMN}-ih!t7aRgjDyIv2@R5x`K&SI&{%u}mkIb{Nky4wStD4c>ah>-HD zJts?pUtRKp=OjD!UZFRB7atY%@-}|(J4kb-zr1pXaP^ANT(arUY%2XHE{KJwhK4%t18icr?1{Cw zfA-vkx_~9YjwN;8uhb>m8KyJ*E7&g!YU_p1)hBMtrG6j->V;juNQ$8nHn@FL0b!Y4 zD8NlvdSr?Q3PrDO%KM{DO~rFe&3GQI#H=QlyHDQ(5t~Y&x58B?A;q4M8&Os=>?-K( zzqGa=5nBaQ;w1U@OiUWBiDk1r47(LYKAto1 zzY}*h2}UQrtKe?s@P9wC-iF-OE4|;|`U23fZ-{PQ*22#cUxrh2F?NLQD zq$47Z+yCye?Y+Ik-dkvS3xNTp>>WcP|MpYA)p+1Dm%xOC+z0}QWaO7(+a>NhImSqH z*l0dnYTl&^_UiMMH<94a!0vvxF}Hh{o{r|g)cKz>{cjMT0t11FmNQv~!&}1iX2|ZN z&Y;;RE#V%KM^W?dO>J4tv%2bat1%~`w~qApJnwTnMr!fe`bw=Jj&eIHKnm5(N^peA zw`2zYb#vKeyUTJQ>>5Fd)R3qar+?5U@Dv&BopkOpxW>Fl*o^i7y(;yo98ZLp;AfA2 zz0@+MZnllMS)CuLl$5+DEUBoAo1mNJRa6p#I+BgtR^@|c-&dM0r!IeOVE`V4G_goL z1}Vr!VM&P@KdFl?NDULq)FcZtFcd7}QeYyI<-2_6@bH9}c=R~v5a#zl0U8GM=j~}7lxyfJ>LO-+Tri)B z21KJJYv%9BLK!^_%%e24Be=j>6G!Ou+rC~swMX`$TV;AgG=@X`4cK3JOtuq7CZ&PF zN84QgnX+{Fq>&}uU)ykjH>zFe`KzDc^K${y?KS1Z#uEnhf*lp7!ttKjt)-u_<1^Oda5KV4{)tS^2?XZc6qbV-pf=MXRs3x(IS&{+Z^ zxC%HVQZpJ8HT59jVhG^>%f<8A9`Mtc<5L;dLxY zc|!S8~yA+s1Xr z{dFyg&{>0`+Xm1*-md*<9)9oFY1HXH{G+LZg^_!uA&5@nPeCHRdNw75(%ufM_eb0G zN7hwN##h7K`bB&8BFe7?4?7-bT2(0@O${!s4u`_#gl4QnC$axEwgy54W4S#HVQOPN zNU4X%maTX@Ag$jWGuciVv9-Xm6t);e7k`13qBx~ER8gO&?L4{IqvH*gwE@bJFCXmK zFD&wYml?&H&ik9W0;E5-y0@NU6u4*@Z5;;x+xu}`QG>s3wiXe=CZaGWyc)G4i6N=7 zhShipMFO>Y9Nn*yd6? zJ(>dVd>bE0n8l|-0$S3ENMExgPkqc+?I_FoISy8>OlPiu@nCF3>P-*$cB2cv$MxMm z7nqSkSeVX5gFz318WeZ^O_Jy~x~2v`USwiS8O`9V7G-Bx)t914HIMc^3-H?isX@wd zFV#oq_p|Y{xT1WuHvWf0O|7;$7%byF#$!4^$+FBa95q023CF86$#3McfaqiSQwua* zaeA)g$mF*g7l9x-%1Gk1>&MOH9DyC)@HZ@lkB!G}C?+MSo8#|DFW5wE)05PQh4dMb z`CDwouLdEqcV9r&SA-UdyN~J1b}M`P6AJ1KwjFCa!wy9_lS!F)t>%kC)Yi!{^E;6h zRhUlWnPYrj2uO=?7YH^^a+8|tP>KPyRmSwIP*rXObq*gtJEQXp%+ogf(Z6B_RiVc) zp3{L`+Vgvl0;vpihFX||A3lB`3VGQpZ&kOC;6tok-2%4{-j7PzmG_4=QrANxAAG)A zbb?lOSrHi3AzODp$MLF5GWLb5@oyy;25=a^UB z*1B3{N%&Xl7a^>nl6OM(R|I?hf(@)vkyo+M>2NmLm7xI1KYt8}NN-OPE%TDR@r)%e zx(esLSLz;Uawt8omFA|`5xZHom<+gIZW1L$ax8K-_j>6qOgwLfMig}u!k7$n(?eIM zPv5&~0kiowdGC`N*FHLFGKFxwp%IkA^_yK;S3q&W^|U3o&94=DkS-Es;=FaQYmgpG z?3jd3FIyAMdFzY&*tSI7#O?%;QTF$hOtGtz3bniDBHTmWvb-Zrj;WbZ z!ZLbdY&6{vl`9H!Re&R`Mjh~scHkIZic;hb&8wSgNd|-Sgl#KzD1n7&`_carJpkYm zDIVmE8O+igD{#h=Vof>RWlWE^;v1Z^G5^fsHZ$D~3TMpP`Z!Km{)w`?x?G3bq1nSj z}i|r_i9wK>qLVS7m&)sC04m4U9kJl`7(}>+J z+RIHIYqMtH;2Iyc;OrSFV>k1$~vOVNDQSl8V63Fqr)F5$5%Jit+4 z07G{h8dqtjbTfhU_HmdUnUST^FEP;SN9@`{Z@e%TEFG=$`g6ZL{v$pHiLuY9B6B@Y}PD zvBU`KH$H=4weUr^UsV4!yq*TuIRhfa_18j==OIN>YMfM!wr>Bn)_;XfD0+3#rl1=C zXl5$T<5<`wzLN6FVs}4M!l?p=wz!u4wX)G52{V#AjtWSkbW}T?t+Slr63* ztFdXFgnvInV4eL`TYDzsB$6Or^R(KW>Wc2cPEme^+~sG=;JW@@Mm8Di9FC3__3qdn$Fx5<;ZXBU8^if%XP^ za35Bf=Oz2pjG-@xi|8G4j!q}Pf$g;puI_2Aj_6HlS3~zW0~;XPP@WswsExB&5`_vKCe>j@TZeNAkj z)I*x$f@EK#2YP=<@Q;ShqLk8@Zzae8s}gq?Ts z|0Cf5-*^2iJcS-zIUn?MoQ{5yjP$m|TMzhs0)IBQ_tNIjDQ*94a}2(|r>*{(q#GBP zzm2-3c05sCcw*H6HAeFZC)M4Jledjw&f=b6+W_dLPpzkI-%=l89j+PL4p12}>2W}H`_H;L4I(4EQ1=9Oglo&=#nrbK6B zBioW{@9)*onFsZW_TCWnnheiiPn2=TuMBtM>ff_uvAhi2J&qc_WT_YLVjUe*-QI7! zYvin3g?k7wFtxd_8o4|8$czc&sW;K0$@{xu3zf!kZG5*|AXR2|T04!D#YjBTE!tVk zTRiB#?9dVWsPrt%@JCgdGe}rd7Po`F(BLiJ(&~Ugi)vK+ij#U$Z2pW@3vc})0LUWd zZo+vWZ1jjnqYRFWP4u6p$*F(HdA|BjDZT&hs0#6?5F6p2H z>Y}RFCRZO*HSq0J{2nF9-n%cwdp6I|OfRYIjSad)qCup%M>Oz|UjcS6&beMM9;kjNU_={YQHa`FM{7qt2uODt^bTALs+$o0ndMRkYEko?6ln0gRk0 zgdt!9s%>_G$6R`aE9`-lmz!T4FFZ|zT8zH^vkJ6dHgp}_4&*Q|V1Io5wVcyqm_Rf- zgGyRn&#yg%ojGqBuOJzTZvyf1vH2Ogb3=Fj(LYWSKmI2{g(I=`KC#t(y2ny`BJ@-! ze4B*@U{E|Zqq9Eu4S@+#l~qR4c&s%t@z!Z-We!TAm_!oXOUkTXUBdM3i9;MAsj}xW z?$RM=t*g7k(HBVT5t?Y8APkm$rYx6kU}5#?68OFGRipE)x*5XpfrGeYwaXO_Gk_0y_PhfDvfS|;6=IkwL;~CNz5iLI+0~B7c zATP6PD)y)=794Y^h-gOIDY;#6{3Z0m`)U&ZX9mNqB{G+LEu-h1t&qW91PWFHT`uul zxB1Ri@+|8g4$3BDeZa4pX;b>CuXp>{P;Y;5g#x{mZYydO7uu&bNtY7m=z5}hk=eTV zd9PR-7umi46OC@op0<#yrI>?#hb*W}mz}oJs9|P# zgaa6^KYX?cfG_bbWf>!xxTZ7~+DgH#erIpz)Bg^l+#l0he{K9Sk3veq;LarZaKzIo z@efFO3V&UXe(qve0I+Jc@!J=*DoywY=iAo^CsV>lUIk_jaOUN+j~obbR&6=V@;F66 zCM%-N(b8p2KalVC=yovTN3l~uVIS)#n3o4psxm-B|A=tYhBV%8f*W~d)oJ^Mvlgu> zMyn4_ctUd0-BRHK<(25oRt{kvO7H;Z+aQJaUk*I3 zfM?~_3*Jluq#RKwT^}nfYG^R^=8qbq#UBWT010(g1ljqoQH7s(baA?6U)6V6v&oYn z(T;Gnb`ba!A?u+&9T*obfH%3$Pf5aj%c@Z+XTjt2ICquzd7QKjlH+f{G}m;`0HxgR zwbv!Dt_;4|X;8nG8^}L`Rg#enV2rrggeLApyZ~|(z5Dcs{yFe_cvE(++v{lPN0NYW z35Flg@`u#AD~ApuU&70?d$_NQI(X-VqvnLX()q4EKRIx11!tyqfUCb<+FR$5SXUYE6{k4dadbjO9z-!J`7{8g+!dBswv4C4DsG`v5tgtkQGx|?{ zBl*SM3V#6wxw;XYYGNGK2cFEUaT-EyqM+hOhHD{96eD{29Kid zznc0PyBH!4x=7m25ifpd${QKhfbVVM2&IO7j*aefa+i5l%z6XuSuWvD16dCxQVMwm zg1&hJ<(lwb>x5B-&wU4G>1#6NpeW(lb8tg_uY4(Q{(1P>S$2C~{VO)==CMHLk=J;o z;;|4txm<5tci)&%m--)$u7aVhZV3jL;!+%n6)#$}xVuAfX>ph05Q-GH;_mKVG&sdw zi@Uo+l9%uOfz8d`&1QDa&K!(ZM9O2IX07`0;Ur2*DS*euDXi%2I(}*#+tr%*^kutU zcxP_jJQM!Qv@%g$kH)CEH?s}h>baz8a{r4hBo`@o;6zkHVVyRc8YcJR{rk$YG|+PJ z^^?@^3}b-v#pd@FB^J}2zX#83p>9g&h;Vu&<8!U$r1R;ZpUxADOO%)&*(^UWl|k2~ zL|EZ)$Z;ju(iNk=8)2RZU+$W0eu4Tw?i0d>Hi4R;Ndeje{Y^+WyVSdKts+m!FmsQ= zdjLnj!I$$pRr{91`HwS(OpzwR6wr^SyjjTBW4Ze6kH*)y-wP6{KSdPbM3kVS4~wzQ z!6QDC51yQ6d-)6hUP~qZ1b=p?^b#&QPT0jWF zBaq0ps#ENBeNIm@g#afSK%qX9R3dbK8mEAVXQs)VI}bkM=qVI2xwR5KkiKl5L6yYC zjb@c=b;Dy@xwUz!>*hhJH%fo@u=ssl{HZPX;O?v3N* zp|4${yM*qpKBWV!0)V=cT5(M7ewqO#jPK#JT6P#0rjO$`aJqedZ7u$zBtj}nLr@Bz zwecr&$;tMF(AQEv#%`WQJC7%{nbm^M=)CH~yaki*{6h*k&*!vN(6bN=ff0iK?jA}s z%%m360Myvt0P?gRZUyj~6k4VUeGSqlwt^4n9+_>GhC`DYwDIlOgEV?L=Y4{b%720zAQi)^ej z&oN%?^qLM&i%29+>VdHsQWOpW0YU*!WE9RZWE%o{K`Y3}?neH=0bt%^(kxl)3}0NB z%U5)Ik%T@ag2Eeu4+NFGL*E|#o+h3zF{H>@{)+XH8{rrgeoHDa#2J&}ov zr7q9r<-VrO%0*D~nHF$)Bt$#^3G*Al2yFP0E7-zLXy%ylnC(1Bh%gK~S2^Ct#s5?* zwr_G>uo^4H8t)I&oML@#v0%7H!!_LE*QEI|+EH_w^VlM!O&!%FjdN2I&=bI2b(?>G zoUbnrU}S+k8pHT!(TlO7nD5(br`^i}r#n>KeBTHH(<2btHY5LG3qCW^EBx?0ln@*> zD^ioc=W>#(#}<=L6;C4E!c7_LcY=8oTu){S2+^m((}Zi@|h2j&wAi`aeJ)oWOT2 zGvZC>T`QQ+@JqQm9-oU>RJBg0KQ555Rm7^1S8fxfiw|mH)xjjYA@>{?M$eM^hxY>t zAe|%tUt&DlubG!~W8dx%yqOy<=rn*=$vTtp^D6p3e{u-fJ=HjjTR(>!M$lLX-3#^r zPj_d%jJqiS7am&`VmN8B|@Hz-D(-8}!0F#`P&QJ9>9a!zXWBsp0PZIqQrLh$=i zYr2408_J)wzQ8{I0x{@j)AL?8E1-(dl(o#XrVh2nd+NP*k&x_61Env$`6z}P+;*#z z95+81T5~nO#(#RgG?y(8{h@6+U_+0A~6PLal8%2Z=$S3YZ7FsSrra2j@tC^5r##{6q2?Z`1 zYhPk?l^NwYPLNK?E+N55Ju`|->X}iIY~6Tui)%0V74+X*J47_^6;N0v_J-9Ha6((O z`psVr;?!vw?u**0L>yY6tzQupdAk+l?IC#ER<-Vn_RL(Z8$yHn^CD_TEnM!dPYm4e zEsoQhER5Zw1a+a8#xFQtO|fp*Kbh)xrqxJpGW5(j@VS|94)?CZK_Y{_N@qr{-4VR% zH;aokZ18K!uWzEZZA@hkn_>M^@nXWvDU2T!ojka)S{rTvt9Hq&zQtrHHp4^Ihn%|$ z8MVC^UfiLjO}`4QjcR?Qs6=j)6?92^+h)WAZT;(P6-ur5a_R9O=HAz(k}92I&*As) zA({oJ0uVlsTg7qm(fS;8-T!K4y9I^Z9~S#UdU8~|=0*Q3x=X2%Q8VzWykpfUM=|lO zyyNKr)d?65={pp z$|{r&X@9VTpDPF0k3Kl!fNS_!#8~`n%pxd_&o^WEC)C`s$z}n*(#&07l?v#EXFwiU zE@qqq%wY69c{pbW_-uX|WihSLkq_uOnW54|XJ<oz~S4NHWjhh*AS8y^`Xo60~F$Qx#HSdHQ>E_9CXzf;XlJztS}H*Y^;n z2FJh0Yy97ZdGZ%Klw7O24F3>ySy@>1L3!IfDY_6&#ePH3iD#e>_R36&=N|JOqTk4j^D`}F&KIaF@0|;>4z8RjxvjkG?A72Lue6# zv*%t5%<~50i-!N`yulWjKF<)B6KO4*b?x!psQxqv`YCj)$DBVyQw(6FQduA2OOUbGYm2qA!dT z1G2{2%2uhRw7h1D>Is@}2$VU0s@PAe%95k}pr`gO7<4Hsm<~-iHbLYAcpwvuh6_0m zEB0#h;E8t~`W-EO`JH8Z-C0X$T&(+cS@iTHV3YlO;%Ux{*?9$uQv|mC%8-9-5(Pb1 z2G1a*U{)Bi#FhiuRvT<7VuQ5CTsxU?(%!#2){qvb>~4`kM?2q}g%J22U&Ls84psFy z2l*4w0mhGaS!rB%o?;#OS<0?m2=#Ab)%hL4R!?5jk6Tb1B?&Qv{) zwbG)eYn4do2-RYZ-&Ts;)R03--PAX%{@8EKPz(Bv`%T@uzHmm(qqtK%P@B8N%g3m4 zJsc!2%xqen=w~1QlkS`Q_a ztCJR4IZDT#v;WFQLG0VXU-;otBkzUj0VEUC=V*;&zjf90$-+onM=<Mbu%Y z-XON^NtZEo#^l{loNj@q<;{bfKCW#+45mK|(D43d6ZDIh^k{Quv;Vu&joz0Rw!_pZ+c#FQhFL*I@}>t6{#!)yl)_ynYq|8e+R?Jx;KX`d{ z9_^eCSX)mQ{`>F%vg!fw`>1_Oe#p6ty(Flr(35gy^K+&*eCDPGZa0=`({r&5i(Ky_ zVPb=i+>CA23akkiC_&iX5ZbWmHfTspl`rp5ioetDnP*|yaX`Pk02$os*dy`n$^qoB ze=d;DgMtMkL(mZIe!D|s9QsY&&W)$C*94~oD{x4SS}JeS&(*ENVBrD>SIIl1v>gVu z$dk28G4{E?@a>M@zRFC1QGakZ(A+={j6vy)c*@hO-9JE1U9Y0CZaSj(S!3q!E#EI1 zi-x?$!Iz+SPvV|ov+rF#Z4c>P4qMnGTJK+S8!TssX6)zsH9Z%I$5BU-oMUfquXDBq zm@s~>m~?b-M|V@;14!g^YpJb^t9{BeS;=)xi2ba=ab=8s2gg5~R{lAZl>y)KvxC6J ziOH74Br)PXyHe8LZw~GP-9`52vDb{04e)8u?moC z_3}5wLR3~Dmr(-g)4e9P(9avskmlm4fQKQg%ufp5-O-%lp+OZ;;T%1-^e^%QE(_q> z`oUeA+D14!9+hwLH=Ah6afT|@bHE2Seg-1K8yzl-Lz||m zYUQX)`I{V<>J~KuCt0B8;!W?Z-8kOtRM&TmmeerlStg!*oi#I3{mi1A_BWbI99GL| z@fFvV#>b7R83q_=W1a4Ga2_Zc0NFb+GZ}Ajtci|2it=`}%K_t!vpFsu2=md=;VK$0 zbK~)&Cgodg;(R-it|G$_DlegQ{3(@QT%|Q)np_W6YUE}Bc%{S*C!s4ueuxmu;%8Rd zA^SbkH%|NN%Q)x`ejHCu>H})z3ULyY3?~FCfTt`kGrZK8h{JA-hB5Z7RL~>z%Z|fT z{XQ>d4Irp$1p!t^_4vh-P~#kf!jJU6rMbS`%-=_a?woGGtL^#M%xj(3`WgEOaPb7eT@mIzNaEAi4mSfuiRm;1uq13sQQSwdJ$ap01at^<0P=>u zIZcu9bGd4pDTd7nTAAD^>(2026#m|Chlp1iQv}X!6p2ELH;q&aso{*4{rBC@ zXF3$ugPk(I6I1p>nJ{CAo#jK{e*tn7@=X%-#nwIHgk>dj`8ro=tA9y%W|U0=%w`KN zg`I$5Pu}L-_Ep4J$dBTD#B4t1%9ql`vJ@F^cJ^PvC+fbxy2|>YIkCL637sZ!Cz8MD z`SB{1%8nmWb6tkK!G&>9)a#oHLrJL2vs+(UfxYQ`V(TQs6l6cno77@pd_Tl7AYbBe z0&JV9iO&UHLfNJ2dfu-m+SC5d(&#@%+%zOdEFZKW#Sm?!T2{a_Ix}~!E$XuwTo~tV zo&4&qjc5>pDC{7H+qps_BmQc#fkSxoH1tM4cnaK}Asp>QDw)m@w{ijg{1C-x4flsA z5A~`p_v1PDb51x@F5%-prTjV{AkR4tc7&JT%8v~$P63RuxfRaf= z1P9X3Jm}*?l^GfqyMioD`6AGtdkVzlq&<=%6Ich^p?Kd|aVtC(=YRM5u1yER zs9$^w&U=#tg(;sQo2io}RJrv`0r$;bRx&9a((0?YUCCzZ0pMOWCYroR%{#1)9?VYv z&_58hfq~3|pcnYAn^Qu}%a@-1Le=MgFU#gpG`13u0tplfih&bzihQZk*iVMGq|kDl zQ0PPy!%Nt2m8r`0Hw*oFFpl@yI~+LvD*Xf;ikCz8o2(fl&B-^K#SKt-6iY{Ic{0cu zk9D>6IBw!`(@$G4H${O3i17?*=wpQa3g-3qW)FqVKTmn`b^*ohwliU}j+eh#I`@>> zhZWpiOZ3Cy_rjmve@mFX`x6|AmGtrlh1xHggZ8WJivxjHiNlf01+azXW(@X^0j;wM zzYa+rUQovf4M1^Cx!>_2zizQrAc~fD;&Vxat+8?a`CD*wKw#9(A!4ne@ZtBVdQKTZXyv2GKt1njFiGswdOA8YzPzk z$wgZ*ADiPa`eqDOf{D{@m8F;xe~P9OMi5Rpig^mJ;66tMV6tPiAs#P<*pd(4L8F3i zL^Al4VNUBfH_{UU;awaQ7n4xjORF$(Mvce7{%kMv;M~Xb669dA|>2MX^k@Tu| z?ycpNkX$z1Fj24+?^nDzEsa7cWS$idi1)GkTF05RXU9V5BbeIt|Ls3~`ZpBdldAbT zCx73Z80Rop%2u`!u(&74DQZ4;U6Kmm@*Mf~BcdLgexY3dd?5A0IzbHVw!4W^V_l!6 zh_qxOymT?O|9nH#fv+mELD^=fU1E9M0{3Zrle0Ge6LWad*EB|?FWJFfH9mtA&23Qz z9UDefC6f6z5d6FtX(=Y3;&;Mq->+V%p-|*NIOnfCg%9TZsU8i;KPcQ@i0XAYkberu zS>Qu9C;k)OA@)CQjA{_mUozX8kxOQ|o*%QH_SJs65UYi3*goqu)1K{SpeOFW&+${r zH>Q^6ki5nz0YoO~1Rd08@`P+g?kaR21^%OL1qo=O9$a<~kB>|f1@ zax6@_EZ+&hRq-Wq4#>$Colaamg-;L3&>_=h@Bm1}7;yD~z2Elf0g_78Y!5=m1x*(D zSckhyaYEyc4vep*tW3H$3lRIDPVlfwc_XlS+*}e-xqbTj$Z6$|35||S z@L$W24`U%Scexsx+=1F#BTPsNP>#OPy&nr`?k^!fJu_qgbbkj!7=kD}H5}@{h%R5H zHchEHXk|#KaR4>|90086j|20w*U<56BA|p+#YZl_{;}fYDd)u8k4bu54{f~M(m0ZIARd29QVJO83{Iem<=PAPC>uW$3i@T5?LnQJn(jJ{Hh zeq_W|DZ|Mct$|3QOpfcP+oS|p_A%Vzv@S3S|zclBUBP+7V&9Zv22 zZ^JPc36lIK(D94``-J^hZbV&hVDPePZ*rjU8(h_ddJn;PdW3gsvwHyQwdsvX8Ko4* za3l>^yac<`@&#LSIc%O20mKAi?SJIi0L5yRSmxX&kb7i|m2QH#1az5k?hUA3uLU44 z7Uy|yaCu)imfn;OL7GE1ZM$S(MI?s&_E#rV4JoNS-3CnP@`X&iG97~E%>)dpsd1ix zTa_!$DSoUCmrsII<>#mzWLf&mUEIfEymXcR>)vmTFi|xKKn)AU(B?2*?`W<3?3*Bg zDJfriBSpp9JDJGzf*A&(Z=QvD^~J-xVOq6 za>?5E=tx-!w~r0jF90-^=H7FN!Zdph;{K1#k*EBEI{B`K{iuxO&N-?%AXp3Z~PQ<>G<$kM)iQ)n62U$El&xh!58WFO=gZ|~(T z9ILGt{|JkN_crwU$eGKxC}J7luWH=|&F+H&PeZPtkV}NGLBTyrJiCPk7qsjg5-mLGXyWaE>iscJ}7>U`%y&y&*P{eXgj8*9GVJi${k87pYY3W z+#d;Q3DY!WKoULyZN@_#znzlfOw!Bvzx(`DYO)v2B+$n>dBR52?34`j$3((hh z!~m51Pq>drmF2SOBkx2bBk`eq!t`(0Rq$<&7(e~%;G}r7(9W0*b+n}QAg3xzwLu2 zTQUwrM|ZHD6d|$ihu{Bv1c6@RQnmn|xmwGv_DPPJV8@ZRdYF7kbmQ*1^nJ|1D-j=S z_pKeJWeExD8Z1+;cn^Z&IgbRnBBSMPezx0C>ZPKmzH$VmQxhe6TU8sg}Tvm8?J*cgOp#?!GB=Xmwl_#)$dj<-QD-x}P>m67>>B--!ICwRyxN zC4mxca1-P}vtjkq6f2Opt}qxQl1{18+5J$u&RS(KTMyV!IlI~%`B7Q=)#5>xIn0jW;(RvDx%V_yv$H0JZ@zEyCouYZA>I{{%;{rl`eJ{?)<<96KvM&AYX587N9zt#Gr?`-j;u107ej? zK>dKhoPveL0eDE>x&VroyEGnJ+70JO{;@N+&u=UV>w*pp5kZI?zP4Zrs1x7516YKN zTw9@!<}Q6D2CL4Y1OyvK^ieXg8-pe1Clc>ftH+@9woF$LY?#CB*()-}1;lE2vP~;} zk5t0UKC!Y3*cX3^IS-?eUzZz#KLuX7!{sRheMB*9<#a+Np`emZ=YQ0HyZMIP!)h>cpYBtE+259oJutizb zZ|rzwnT-VF6%N*8G!9bmKFhu+4m)%db7tt#h3qH7+Na`%sN1dkE!7e|gncxq0qAUq z_21LZjm%yHDQ+G!7^cPsNn;sivAc=e-HN(^XGAaNEL335pb}AlnsFbwqMh2%(~nXq zQ9ljML(I;=P@OED<;^9P4X7t?F)N=0ArC}A%NIGtlxivkQr_x#BOt-9$MiM&A!rr>L&W6U$znS z01=4FEBX0DGz#0Evkf?RL-e1szd#9>$S$);fRfQUh>iJ6!GcBCKyMOgdgGDGGHye3 z4}#5P3g}$Mh~&o!cc9t2F?%<7y~; z&!4q|me}8JV^!#6uLVtlGdItf2#l#o{z5ymPKLj^u??2IfW~XQC);-}=&~vnwwyN* zI`*sbI`W`e%;}z{1TAzysH|^U$dV|EiQ6F*@9dr+p=;X7%yi)=6{Wj12K}c%c?O@dZSou}Q7mB#I zNDX8D`BFBCT}-AkkY#z4`EiMS55kyj3zQx<8`LIVtO7oK!QL*Tpj!;=Z!)<TNq`7En4Nlre+?0^DN2Qr3$f@`ch%lJZQ}Ykw-6WyM&d~w4 zU;uB+q(8eS312f{|Aw+yD^iBd{c!Z97OM!oY=~N^Ozpv#WF`>qYKUQC4=P<_w&O=B zuLN^9m!SKC9H_(eTQh3DsFkh!-0OL}lMsyt{>l`)nunL0rmnnk-WLh&QyErWyfp@4 z6~BvpN>4Su%zc>@n{e4&Tkntv+kPh0kQuFLezV@2b#(HOYk7+-mxd3!| zOMF9~FZAvXRZr(72niw{*`&`-R=+bD77sS3jP#91O>*ff&eIBfv zUN2#k|DpABPqPv*#|%$^$ddWeY;FZdb^4_7mURdnFDo>iZ#r15DZ^%p0Be>yVc>u{ zbmN2mv1a)?XP+ANipJm&xtpQqLRwX0|W!ne{>zcXv{%8C)%C=$$WKHUv1i-t8Z z{maH2<^(+_2NO_Hg;T#|k?*Z5!@|PgI}#gP)lkCxRa7=oR~(WuMx|8I5r3`eQ7E_7 z?4_Yv$D2u0d08^BZNx<$w|PcYcQHU9@rOEB1udoN1LgZ3fEO)EZ5La%s4VzCajIA$ zi5F}Zhg&-vQ6_w(X_6^x4Is5Z$Zq0%QvKn*ipV*4 z%N_E0e$C-Z%V%u*)zl5mh1(PKbXD3$cHBl1-ifRp97bQI$<0Fv9~HLQ9H{4~DkG1= zX)~bPyS%K8((Ai&`sG`M2~lrTyR&p#hOrRDOQmQL#j%%2kf?XZj z@~LppdezK9Uzx|;r&R(Z;^XQ_*O!=wxW2*R!luyt_*0T<6+bE4&J*>+{PycZOcl}B z(`Ly`NXn6+x9t6k)y3BOIpxwX3S#*Em`U~ocv&uUgQGdCQG>~9BNs{a=S;Kj6y76XQ0Hy0(n@5AW>l>%C zE8HbYT4b>EhktoPpf2M%iW}R0qxrbQ=q*Gfk!_@ANc1SiI>ZsU<`Ia0!K_+RZw$V$-}GF z!xtSb$iDVDh+CtBwPf^V@rMK7L#h9Fbt!REihvLBb+VjfhE2(36*Yp1);X%~bRU#< zfiX8+DmDe*R*Ya`kXS+IN1zFH5-uo@N<_xLwDYoLgQ*nQk005PRo=HGPaq*XnocdA zs=y{#pPAxqKe6%!+V!{?C-M-6IR)J|QPu}Y?^tf(hb+~b4TqIHk`LX+Q=K`=Nv{%3v zTcFXD!E5xh*b4|Bf@Y|RHsVkm`H??*mJozMA01ZQ`8QqX0=^#vwUJGM=WjOJztuS3 zT`Ci8YLo#(-f1{w;V7h~-V56+f8~U>6Cta%->l<~hw>oX{}ueILt0WcwQ!IzMF@Q> z7#|h|Id7@cIcqRSrB>eX$%Q5#KSKMHm7xvgOH;BA19W$m;|h)e`?c9~_%-3?|i z8D;!n0}?9qlF&_*3SZXyi|>nX?K{W4G&UmiOP{l^+LRAB z?+o?_;#KT^;&S{Bscqw_pxp}YKA)nLj(c8M$$ZaFo($c><<`BB}z$Jq^k{4_)kmZp5w;&W_YF@RoWVe$H`XP%y zNUrDahcvgTVp2R@m-VxJyKYa9shc8gew9Ce30_GCO%@Y`YzBTy&Op0oueKjVoEXi` z@Ly2$ehh>n)(QQju9!MtR83gik!p#r(!^_}@vb4;;(O1gJz^lXL4^hE*7T*m{6xriImUgQ7YUtqL zLf58;kF33pLRzR6gF9vI<@FQM8yJdG{BjTY@1)(jSnw~zQt8G+ytYlx8{|SvO<1}Q z(5|&_H^8z}_vB#rHl~cq5YO5eDybWrdA`dAlfMyeR+WaoB!_$Y+qOzb@$1PMe!m;3 z&5D_NJ6`#h^2z!FOq76qaf+@FXAI@AoTZ9jz6;x*Q5Efw@63wEdnW;vCE@^h2sf<0 zrqak3So;<^;Jp(B{3;|KPFwtF@pBH$eh~Ict=@q32x>$mkKA>Otm6HM?bYN%n$T_X z5efL}x1k)Tc>MnFP~yfT!y=F1uP+Wb;=(=o$ux{mCk!jY>IX~>hevl z-#HTym7vEx&3{2FVjg<>Z8^@KxHv?42>GW&2cjsHU?_;k?$~F1hxj#UsSw*Lc`;kF zuSrJc$hwwlP{`OjqF@V)6ZPK6L(pP*BC>DoUCl{#?!$&@kV#qX3&oL4ZxZUot=D?d zM&ib`gG^*ht!Xeh&C1VY^|$YT2m4CU{R09vV_I{Tn$2y4A04;3zLi8NFy8$m;XVp8 z?JbI16@C5QFJzeCPlMQ(NGrkGhj&Ip9{j$i)8T}lu(n2~_xl3Otpca9PHK6gljgf~ zGTaSRNLY7oUw}fmxc%FKG*q&)Y9vCp>SqyZy&N?gY;WOJ&T;7Y@KpHQ3ET%lXO18O zl->O0_+hCKrstIOvn-RW&91$mk@U+C5vTOSC52R<4m-fU$;ukCf;6~yxrzp#)>Ej1 z)fIb_K?w~`W@sL()f*`4cK2%y49VvNNG zy@Kk-%@?XH8U4G%&Yj0@60aj7wc)0BpqR>&=C~p0a8l!#EDH<*QFD^qzg1yRHu&@7 zRL1e`myN^Kz&w$m!xLxFirwg2wt^wmr}_`EVF4hQd0A*fzO8&Rk}gDM9ui8wEFFiu z%fExm`;jpZd+MIR#<-ofD_IbfiT=g7tcuMX9qN8$GVzsPNXYi#et9gt_GfF9J9D6K zmM-7aO&sVbw~QBgKAhZx2VNJ8(CtlnFwSCf_ziX|K+|Q;%HwS<`*NqB7M*_%qgpv4 z*EWP!n9FA%kFaZsCQ^8F{Uy^NcN|*xYp+ZSF$szaUjug@w3uF!xN%gDuNF>vgan4N zkqz?J8r#@_FZDR}Z(La@%jftLtO*=)y&J55^Mc;SlFAp$aY0E&DrSft8tM~`;4&vaJg-DsJRlL86L%lNF_h4ql!}!k%erb z{v&3#4?aPMFAPgj~Pp9TR^5B0W zZVtPMX=o)`(>2xjGd=ivwdhMf-Nyf~Kdbffw*B>nLr)Sp@I;g{`Nz*&RkcD8-W!?# zI|UWW_F@WApi5-TP+GJI6-${c4iiIXPMli!73JRQv`$JZeg+fuQ{eX+i-&X0F_fHq zG}at<>Bq?uHZ1_q>}si=kbS;%_yLCg$>tuzD6IVIG~vE~i1&!~3Ga0gGr-ZNrU;jGNXn<3opkEDJH#yL!MILvX%iVsTE$MeAT^(#1O9?!yiwbKK0 zW3xSMZ9eC{JWV{}>Ih=S+hqFl#te-6rztei1h4O)-D^10e)WAT3c9?$e5e2XI(}fU z2=bpArm|`0-8cWPu0(K0jTV*{Lhq#vP+(G@J8B_y6DAEEh#3;kdSB?`RToS{64^9p zHtcR_^`5kf1jw1vYlyhK;necIm`AL{XMV-8p;#Jd>>?R-r#H)K7pYSbfq>fE?akeE z^O#0k73=<{Uj4={?~5p%PBVSL_XWlrtsct`*`G(XG>w=y@$iNX39{tENUrVFS#mXHR#s0Pejv=;otILzfYU9pV0N+QF2Ke1uo0>A7qJdx^%lCfTg~v}(GqzV z7d|FH0@f31Qvrn@gvhku{r6o26wODW5YfR=OwEz%?3MI`TT@oNIw!j@&iq-~|0t>B zqlnm|zv!7bU+sZ%@n@37Hm1}G2(pN~&vRz`P$~82VRDR7(E-@Gf^0Eq36b5WkC~P4 zx>;Os`l{@I(uXG1lhVm3Q^d8QJVU_S0;qqQTnVDUP|Hv zj4SJfF?~pwNz9D!rjB#Ve=r2@geC$a$=v)B${YzB4 zvB29cS*kMOmJarJqoArup*o?@`SZS4Yms<{BqS>K6wj(+h>v0|xGCbbzD1-gldlWD zVO+nEJK7tJMTXTaRAY4v!yeOj4Gdn+J47IIFqi$f@U&2|=Tux68*jGTj~AltZ^kMh z`?=ZyO^C9Q;7*!f>@zVrd})(wvL#ZQlt%F-8GSSTgn3f+S?%ix1C`Am2^JuUMXxYW z4Pt>v4DeN>twY@QcNWJrm_{Y;YekVq;&U%w^Dhi?JX%F;;BT{&JmrHt*Q+mCe2Jz} z!mWnc?E%x_<^bDwsn|a!u{8kV*C6x}o-o|bjH^W!)bcK0e|usV#FzVqc5Lt^p1WNP zw;Ts8`s!;6gDD_s`mBT2Q?f7=%wADzW55CwqplE<~_KMl$2dx){Ql8k7G zU%Uqy>idtxr7XO4@iM1NNXc23Xt(^A35k^5zJ7Qzta2OpBFsP~C#O~RKO}ha0||Sm zeSp`-ph!|()emb%?Invuddvu{I0woV7+u|I6@H75%{*W`_R#rAF+#@(h24f77+)Ka zZkjfz(uAcg``eFFN>X}2lg^|D(y=#58NKK3WK9yJ7kzk>+ z8&&<2w#63=^-SK6O{&%*b5?>Cj?dEwF;*bK9%Ne@y!O|h4VBgs+Q<5LFD_ec+mX3Q zc}Y6@^VeKu9@dH-bScJ(=*kvUTQ9f@Y9hKvm!~Hx=|b1Z1fpm|a45(z&-%=Mc5<12m6p{tUjV`3{BokGZrIGBJ+Q9SvOOO(K(G53X+}U0{-IP+f3c5OX z|IX3B!a1{_V9xZlUkKx+;6b z=WS#D?|mB@UJ(u~IpQJ7pVG7z@zOs29yzHv(BQNw+XSi|F(ennng<8$g#vwxybm5t zoCD1$B82*__xq?JwCcrOD=J( z$DO%9-U>!LNc@`~w5NKnV*ZQ9*av~^BtV-WlTmx#nCnJwY_7EFzp{K=w5pY0)3Llg%87{YH+3(1US}#$}?OeR?>v z5AF8q(=!PefOs*Ft=k%K{6vK<#= zP0Osd#tbgVZ2ZW$rpu>~u3zvEwCCl7{lpMjk6m>`DZi>A1V)ui7QK>z<#?BeNt&3p z?Pov&)e#xpPYeucVxB*}W!i?>l3Ltp&q6t+1gp2Tr#|}-ubM|fc~*TJ<8zT(0trE} zDzESN_@W7YNy$43hNWAJ-%=8h>0G#nXa$k-qRp3TQ|lNDD2|VUP5c_nHd1q{Ao?54~_VbB!sSxgyf&`hV?x z7LW)eBBb@+z+Z@ujIASGjsXanfi78vL{)8BHGQ&G8vo6E1bTEBS8v%Xr%}uktI%{MqZs zpXL-c@ICMb7z$eS{s}8Y%E<~JgI&$j;Tf-1UDAc%_|P}!Erh0&CdX|Z6%dBWV8{21 z#2U@Z`iDv2omeDkIK$xF=2yngEC(Q${eu({m?Lu!T|i=8zEzh()chDNl%LlSC^ z-DzfvUVOL;PU?f8tFofI+0*BD9M;_Ab4Z8x>$?z0bY%`Y$3lTqu9pD<=b{26g( zU#u?U-UiN2?q#n_xm5FU2J$l*rE~H(mc*~&3NCm9D{UzZm;Ck)!5?D3rs%We=;xR?o=pUw|40F9 zN!5PTk1)z;@WPV~sJ(9-R6!KC1b~0**;b8wE(4Wp#5$*fInAguf6}C=9wLxHLr#*? z3{14uR@T20ABVT|3<1 zdS1lp^}6pT2aB2%^+}yZFVOMP*^hZ z`o**a%6c(}K7$m|P%1T9W8MWlan%NdG&OF6(0YGBc~3#*N8iFc=uAOtmX)jh>$stW z?~u+00rUBDJ?=?_9#+n7zRw=GVG09{(q4wS`E>sCd1-R*kH^c02)+v1+?>yWaUR@z zs)au8((@6y3}%8eDy?bzDTPNybJsyFg0JBx-Bb<%p=!O=FG}Bv0Am;=jF4GPiuVBf zwG4&yN0gyyCCT@?!(C@2xopT9whR)`vQ1afgxA%F` zlMSs2jjPOhtl|;zEtv>f#|`fp_YR}{^Bwl;Qmq*&DW*W7>`<}(q|g~>i3!S5N2lw& zn8_h+a4zNmcMaU-B^Ou0JS?tYbX`^9X~GV$N(U$SV)7^Inzh7mCMp+KLWwR20|A#m z6|x0HkNgO<I4WiijAeu1@v!9}+u~t3Tpo+C^JvpH3jm-W_7Cv`?0qIF{!OA!T6UFz7Lic_@B+ zNaz8ysDrI!7+ShSas=?hn;co5Eb;cP$fhi>}PNAz&7odALN9a%`R5c zUXjyEgNOSBMGZlL8wZ!;S>tPgj0y?Q0^xX+a}(Z8rSu6YDjBO|=*DqB(G_BzZa#G` z?>oa5Tt6k?e&I*LKiw`*3^ADdCwQHP(dl&udv?;r2ro_PTxFTwOOc63+w6UYA|}S% zatJB!c=r}#K_mz$#c#(~j``!(e&}N&2*@0M8(061t0CcOF6Ii>(0jiq+eY&i{z>~3 ztyR!V&Bm~pI%>#Vj`d``@*J*z`_T zoM82^4vYxMEO-ieN-wwMZ3ecpP`-M6jSk0Qpot8zE~Z>)?pIwAF5*+lj^k_84d3ft z>t|32&6Y%lQ&|xU+6+Ex9sKKb-|(WBG&Z3ZH1|i#4ORxGoY%a(wKRb$C+Fvx3*WfO z`+5EFGp5^2gc!riCTabJ+?6t))Q(t2jixW~2Q_kIr*;68 zc4f2-!|)J)_4|0j(fCYPr~OhhkMMVISJIo=qMr10wS0~ACMygP;h@d4)4==2uCA)b zKj#_OYD~g*bjOJ#lHzh)JGuk5QRXW&>m48D_3>a=k&TBbe6tCp#jlR0e*leQG_qI( zdTwe4AL^rbazpoCDSS(XmY}HOe0FYCK^X z@d4OK8Smn{kOr2{L_w{+Cd(!YFO zaWQm=ztEhVjX@`k%b&p$^Y4b}HMWb$Up`!4E#@a+;0}VNB{fUaWM1!u;IY{pNu|I( zX8wm2v&NUq=iOsKiR3ftL+Yh?ory@*VRXpljV7?N3j;p<6CzlT9~>2LhW7-!fFN3g z-}S1~+i~!x;{&=!uU<-$i%d1gO#F}6VR^K3P9^U^Gx|D*EX>98>2H1Gs;HVQ92ULs zExRhiWv-)#c~t2<6-oU8?5j^!rKFM?+}OIh(MwaQbUYPWf*GTW4YsfgTc$FPKUB62 zTYxF#%|i{n%Qc0j@x$&r(J-Z1nC^nzQKM_<((1#WDOJ_4pPF-&;Y`QaImUuRiD-v1 zp{K9}eTO)HBKzZjD{&FcTx=JIlmA_iq4oB=YOWyI0fwkFg}^yyvuq+XIZnrMA?AN} zBe~Dg1q6ZDl-*WG-MO=DTiUE#@hD!+t18@zSTl6`BL4~~!+2yW1ZO0FswAAH9Ovv8 z8y8>~9PpV1=DQ+fj_Qa3FEguxeVy(g2j__}cPn^OJcT6*t;T6UMfs@MN$}Q7QRlu zi`{C~*cH<>T{(d89%oi;DNl3hqr&2jQnFm6%0ubQ2`F zX%1;kKH<0r`}poX#PDO|_Z8MkwSS7n53|}Z?DO_Dhrs$`6ZI*VjXOGRk4L*}o~*$! zRq)?m-JSAnZM05aOV*d5%$UGR&!W0W#n_>rLi)~BG|tAV6)_O$w?P5CtNbvp5?6dQ z`kDyrU2MM1zMl&Dy|H|c7U*XJy%~^&Za}8FQn?@E`E=E>=qa9Ty@qdR_iSt0D?wup zIh<@lURTmNht;pfp69V-)~qo<#r@Ac`goCN5KGq_K9xasjH_QcYx|rCX7odfE=0Dc{;nZ+2uwr}u%zrU@qp6#wC+i5 z@I%{d7ry`5?>Qu)t?V8-eH+h2(cA9!3>6Wmg&D~v#so=|i@NH3!doS-9K)ogwt2Q1 zuv0flHD#rRrsHK0=rNk5~m z;`;gd_4o-MNY%wT8FiE3xm~7|Bz;cHTOE(OeMo7wkdP55$iW{f2!`*?0s&3 zE0s{ODkG)l=}3!oV16l08_S=GS!YH{l_RfY!@G^e3?znq_L$^aI&m!M?(l_Le-&mj zRxOj58nWOW@=Cys7PeysK*T5j(U=U-0TxJ&t?c8g z%=j6vTTi2_74r#xfI&)x+Tk6|NPCpQS<0&5b8~4{T*2%0 zWtNxEcXS0el-e4Nvruzp!h>dXox*fhsxBoEEd!4ndQAgF-0RbkccP&fgx3H&WMFYM z1|VbvgAm|SE7rsr=&@XDb4??i#bP;P7PdM_MeHI>C+7RHvOJ%+Xd??Efhhj5X=7jQ z!(<}-0h2~5bWs8L#UYr~dkeaXr*{V~Qa0(nW%yVA7h2IchzLN(g-kD0oH#39NtkV8t@$6%xjk$AjshMDj7M;%yh@_f zTptlFF@)6=$crZ}6{%x%r>TQUA8c+oWFuU0Gs-aikXj`+(S#Cu zOaO^5J7wi;_UYB+dp>&!{||(oAh$GnRFODI0VVViaN~wbpi-4AJDz=zO$|gUR(T`&b0@+;=!IVggnt%me~DW zce`3A_(|~L$CgY(gqi^rG7|J8Tm0gwOztmm%1#wYDw%V8kMFB-Y_hE(c4@?b-N=*J z<0{HrlXIljdp_@lEeWd@WxtWNWmgJ4Rl|t|&^6c#f#l@Ds@AfdeOFz(`ZjpsPJ0PnGLs*S@}^N!L?Do`L#~|W z^|a;X3xZlUb%Z6kKn|8H0(c5!4VwT4Sr}C&5m8*HT#NbMy|Tplk7NIQ{o^odDJZ~R zF&rEROnz$)EA5k!9OQ5V)9_^^;#*F84uMaaN-tB9qroBLU(w(9Q)6_oHaVOd+Q>(*k>W&9~Ad8 zo4$Hgp<_)e8EU#w6%W`sPIzxjO2ow&7pC~Oj6sH>YWo!nG0ZKQKTUWuvC!!MMK0uN zS&~|L#+?>#qZ$LXD*cJW+*CgFO;%CMxVQ>lbNz>49?w8u_Z^RDO^FEU(8>2Rpac|~+u znkHMSwT=iU6L4x{V*GDU8n~|{kWpaU_{m3Y{B2?*u?=*7{mGX9*uJ2LOD*L03XH8} z6c$?}aDoJ!GWxItc4YIv3Bs)}m6iFlF99Qtbg*A$#%3aOMoXZd^P^Zcnf*|^Lvz3V z*FC3&wUK@`X`d;>VrPhhl}Oz~PTs!#%Px+eDd(CGus!@@B|ex*BM?yr8&wX8QE0Ci zQo|X7OaC=Rz{PQb>#WFLsq`-r6sjeJ z(oR=X*lFk}8zIb>+iBvrZ>jtoG!|OR4!QgjjpRKPE8LHDijUQ4S~LsRS9e5_DuMM6 zDa_D0s;IZ*b@^JA&J(H5-8B}sq{Fc26wes{Q;Ik<1OMXakfM;uGB}!lj-zO5Gn(n`7 z*nyI2zLJ<^vdzi7Cg_x8!goC5F`JEJS#F=J{U}QeFIz+&v0b+q{S~+B5e`y%gO9~M z7QC3m(Q}ZodDM=!Er<;m3m&9|aALv?x58V`Og-LbJ`n`9f17&U9diC!i-`{de?6{A zM$l~gcDOX~X}9~L(H4|i!GImD3BYu7y{!_TC9ZIEo`n;G!`#gnI?+GTC{wYJJGA%; zs;%U047w0rwAeVvriWoXQf(g!7uG)$OPsi}jaYp?zDwEE;k`o`40{)ZBzI4AG$ z)5+?Ky3#>?d?tEi725$?Jw00VIRWMs$$`vYtpdm)xf8O`zdr$Z(72=K5wZbfZwWKrtE^GY-ZmM}{wjXSbW9nS$e%=az*21#c` zP*6gW5_|jgVjiRz_ggj}JAWC@0v2{qO?(!@4{`E9s$wX%IZn8{L{$*D4(J z3Gfo8_7?C*hw2}brCL?jCcZ~XT>*u<>d{wDUks_2xTxvOY^H!Pjs2>mu0IjK|EOVD zPi(@7)Eh~f2tIuF_M_3X0-{JHgg5y+x*{bln&$pBo2L&r*Wmj(Y*?JyUpbDpXr?MQ zP1W+$&<)IVrI89trO0XrOH2mZqP(i#C%y3$dF-^ zjFllI1jr%%`MV&dzS938=wLcu-24R}*=6i z%6q9Fu%FCHAACbKLHg)nyga@QuW`1n5wSU}b>&<1?$sT%L%U(|q#yw2ePe~!3l8*C zc?}+tGF1e@95l@yV3UYQx6RdV6a{lH?>@N-NI}WEy5%BIPp1ab8wa?zo0&1sR*0zo zNd#njl=G%m&R+E&5t!_(cHR+dJ4@B_4b|FB*BlvaI57>7a(;pj<#+> z82S|wD3cTkKfCjNa~G5cB1?yw-g(s*yH{NE&in{|E&o9_dD6#THq@eh_qLw018xW+ zRg$_QPuMMsP+-p-PgOg!j&B3>3fn^=H%_;2)vf9AJX06^5Pb`@hYdk*{{kBl+wnc2 z#_+4|BbOxUN$_h)zA4vCO-9p2pIA=v2G)*P%O1o=IA~Lr=Sp){@vUWO)QccZ-Ej`a zWcQvpj?E`^bITo~;I$55u8V65fjX0WJ~AJf;%}JB#DVKyQh}sIf!A$({v|J*qy
j#(n6`cTK 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 String { + format!("Hello, {}! You've been greeted from Rust!", name) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![greet]) + .plugin(tauri_plugin_{{ plugin_name_snake_case }}::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/src/main.rs b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/src/main.rs new file mode 100644 index 0000000..455963e --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + tauri_app_lib::run(); +} diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/tauri.conf.json b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/tauri.conf.json new file mode 100644 index 0000000..72ebf40 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/tauri.conf.json @@ -0,0 +1,37 @@ +{ + "productName": "tauri-app", + "version": "0.1.0", + "identifier": "com.tauri.dev", + "build": { + "beforeDevCommand": "pnpm dev", + "beforeBuildCommand": "pnpm build", + "devUrl": "http://localhost:1420", + "frontendDist": "../dist" + }, + "app": { + "withGlobalTauri": false, + "security": { + "csp": null + }, + "windows": [ + { + "fullscreen": false, + "height": 600, + "resizable": true, + "title": "tauri-app", + "width": 800 + } + ] + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/App.svelte b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/App.svelte new file mode 100644 index 0000000..decfc98 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/App.svelte @@ -0,0 +1,54 @@ + + + + + diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/lib/Greet.svelte b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/lib/Greet.svelte new file mode 100644 index 0000000..1da5e30 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/lib/Greet.svelte @@ -0,0 +1,22 @@ + + +
+
+ + +
+

{greetMsg}

+
+ diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/main.js b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/main.js new file mode 100644 index 0000000..d3398c8 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/main.js @@ -0,0 +1,9 @@ +import "./style.css"; +import App from "./App.svelte"; +import { mount } from 'svelte'; + +const app = mount(App, { + target: document.getElementById("app"), +}); + +export default app; diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/style.css b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/style.css new file mode 100644 index 0000000..c0f9e3b --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/style.css @@ -0,0 +1,102 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color: #0f0f0f; + background-color: #f6f6f6; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +.container { + margin: 0; + padding-top: 10vh; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: 0.75s; +} + +.logo.tauri:hover { + filter: drop-shadow(0 0 2em #24c8db); +} + +.row { + display: flex; + justify-content: center; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +h1 { + text-align: center; +} + +input, +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + color: #0f0f0f; + background-color: #ffffff; + transition: border-color 0.25s; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); +} + +button { + cursor: pointer; +} + +button:hover { + border-color: #396cd8; +} + +input, +button { + outline: none; +} + +#greet-input { + margin-right: 5px; +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f6f6f6; + background-color: #2f2f2f; + } + + a:hover { + color: #24c8db; + } + + input, + button { + color: #ffffff; + background-color: #0f0f0f98; + } +} diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/vite-env.d.ts b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/vite.config.js b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/vite.config.js new file mode 100644 index 0000000..82e1f23 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/vite.config.js @@ -0,0 +1,24 @@ +import { defineConfig } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; + +const host = process.env.TAURI_DEV_HOST; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [svelte()], + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // prevent Vite from obscuring rust errors + clearScreen: false, + // tauri expects a fixed port, fail if that port is not available + server: { + host: host || false, + port: 1420, + strictPort: true, + hmr: host ? { + protocol: 'ws', + host, + port: 1421 + } : undefined, + }, +}) diff --git a/crates/tauri-cli/templates/plugin/__example-basic/vanilla/.gitignore b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/crates/tauri-cli/templates/plugin/__example-basic/vanilla/package.json b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/package.json new file mode 100644 index 0000000..ee8a002 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/package.json @@ -0,0 +1,14 @@ +{ + "name": "app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "dependencies": { + "@tauri-apps/cli": "^2.0.0-alpha.17" + } +} diff --git a/crates/tauri-cli/templates/plugin/__example-basic/vanilla/public/index.html b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/public/index.html new file mode 100644 index 0000000..9c95f40 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/public/index.html @@ -0,0 +1,5 @@ + + +
Plugin example
+ + diff --git a/crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/.gitignore b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/.gitignore new file mode 100644 index 0000000..aba21e2 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/.gitignore @@ -0,0 +1,3 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ diff --git a/crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/Cargo.crate-manifest b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/Cargo.crate-manifest new file mode 100644 index 0000000..c751841 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/Cargo.crate-manifest @@ -0,0 +1,22 @@ +[package] +name = "tauri-app" +version = "0.1.0" +description = "A Tauri App" +authors = ["you"] +license = "" +repository = "" +edition = "2024" +rust-version = "1.90" + +[lib] +name = "tauri_app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +tauri-build = {{ tauri_build_dep }} + +[dependencies] +tauri = {{ tauri_example_dep }} +tauri-plugin-{{ plugin_name }} = { path = "../../../" } diff --git a/crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/build.rs b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/build.rs new file mode 100644 index 0000000..795b9b7 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/capabilities/default.json b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/capabilities/default.json new file mode 100644 index 0000000..86275c4 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/capabilities/default.json @@ -0,0 +1,12 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "enables the default permissions", + "windows": [ + "main" + ], + "permissions": [ + "core:default", + "{{ plugin_name }}:default" + ] +} \ No newline at end of file diff --git a/crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/icons/128x128.png b/crates/tauri-cli/templates/plugin/__example-basic/vanilla/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..77e7d2338e9d8ccffc731198dc584906627c903f GIT binary patch literal 11059 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
+

Welcome to Tauri!

+ +
+ +

+ Click on the Tauri, Vite, and Svelte logos to learn more. +

+ +
+ +
+ +
+ +
{@html response}
+
+ +
j#(n6`cTK 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 + + diff --git a/crates/tauri-cli/templates/plugin/android/src/main/java/Example.kt b/crates/tauri-cli/templates/plugin/android/src/main/java/Example.kt new file mode 100644 index 0000000..4133f05 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/android/src/main/java/Example.kt @@ -0,0 +1,10 @@ +package {{android_package_id}} + +import android.util.Log + +class Example { + fun pong(value: String): String { + Log.i("Pong", value) + return value + } +} diff --git a/crates/tauri-cli/templates/plugin/android/src/main/java/ExamplePlugin.kt b/crates/tauri-cli/templates/plugin/android/src/main/java/ExamplePlugin.kt new file mode 100644 index 0000000..68a36ca --- /dev/null +++ b/crates/tauri-cli/templates/plugin/android/src/main/java/ExamplePlugin.kt @@ -0,0 +1,28 @@ +package {{android_package_id}} + +import android.app.Activity +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.JSObject +import app.tauri.plugin.Plugin +import app.tauri.plugin.Invoke + +@InvokeArg +class PingArgs { + var value: String? = null +} + +@TauriPlugin +class ExamplePlugin(private val activity: Activity): Plugin(activity) { + private val implementation = Example() + + @Command + fun ping(invoke: Invoke) { + val args = invoke.parseArgs(PingArgs::class.java) + + val ret = JSObject() + ret.put("value", implementation.pong(args.value ?: "default value :(")) + invoke.resolve(ret) + } +} diff --git a/crates/tauri-cli/templates/plugin/android/src/test/java/ExampleUnitTest.kt b/crates/tauri-cli/templates/plugin/android/src/test/java/ExampleUnitTest.kt new file mode 100644 index 0000000..50792fe --- /dev/null +++ b/crates/tauri-cli/templates/plugin/android/src/test/java/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package {{android_package_id}} + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/crates/tauri-cli/templates/plugin/build.rs b/crates/tauri-cli/templates/plugin/build.rs new file mode 100644 index 0000000..e0ff2cc --- /dev/null +++ b/crates/tauri-cli/templates/plugin/build.rs @@ -0,0 +1,8 @@ +const COMMANDS: &[&str] = &["ping"]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS) + .android_path("android") + .ios_path("ios") + .build(); +} diff --git a/crates/tauri-cli/templates/plugin/guest-js/index.ts b/crates/tauri-cli/templates/plugin/guest-js/index.ts new file mode 100644 index 0000000..cbbafe2 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/guest-js/index.ts @@ -0,0 +1,12 @@ +{{#if license_header}} +{{ license_header }} +{{/if}} +import { invoke } from '@tauri-apps/api/core' + +export async function ping(value: string): Promise { + return await invoke<{value?: string}>('plugin:{{ plugin_name }}|ping', { + payload: { + value, + }, + }).then((r) => (r.value ? r.value : null)); +} diff --git a/crates/tauri-cli/templates/plugin/ios-spm/.gitignore b/crates/tauri-cli/templates/plugin/ios-spm/.gitignore new file mode 100644 index 0000000..5922fda --- /dev/null +++ b/crates/tauri-cli/templates/plugin/ios-spm/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/crates/tauri-cli/templates/plugin/ios-spm/Package.swift b/crates/tauri-cli/templates/plugin/ios-spm/Package.swift new file mode 100644 index 0000000..ad2726b --- /dev/null +++ b/crates/tauri-cli/templates/plugin/ios-spm/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "tauri-plugin-{{ plugin_name }}", + platforms: [ + .macOS(.v10_13), + .iOS(.v15), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-{{ plugin_name }}", + type: .static, + targets: ["tauri-plugin-{{ plugin_name }}"]), + ], + dependencies: [ + .package(name: "Tauri", path: "../.tauri/tauri-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-{{ plugin_name }}", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/crates/tauri-cli/templates/plugin/ios-spm/README.md b/crates/tauri-cli/templates/plugin/ios-spm/README.md new file mode 100644 index 0000000..f4900bd --- /dev/null +++ b/crates/tauri-cli/templates/plugin/ios-spm/README.md @@ -0,0 +1,3 @@ +# Tauri Plugin {{ plugin_name_original }} + +A description of this package. diff --git a/crates/tauri-cli/templates/plugin/ios-spm/Sources/ExamplePlugin.swift b/crates/tauri-cli/templates/plugin/ios-spm/Sources/ExamplePlugin.swift new file mode 100644 index 0000000..526d730 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/ios-spm/Sources/ExamplePlugin.swift @@ -0,0 +1,20 @@ +import SwiftRs +import Tauri +import UIKit +import WebKit + +class PingArgs: Decodable { + let value: String? +} + +class ExamplePlugin: Plugin { + @objc public func ping(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(PingArgs.self) + invoke.resolve(["value": args.value ?? ""]) + } +} + +@_cdecl("init_plugin_{{ plugin_name_snake_case }}") +func initPlugin() -> Plugin { + return ExamplePlugin() +} diff --git a/crates/tauri-cli/templates/plugin/ios-spm/Tests/PluginTests/PluginTests.swift b/crates/tauri-cli/templates/plugin/ios-spm/Tests/PluginTests/PluginTests.swift new file mode 100644 index 0000000..4f8e9ac --- /dev/null +++ b/crates/tauri-cli/templates/plugin/ios-spm/Tests/PluginTests/PluginTests.swift @@ -0,0 +1,8 @@ +import XCTest +@testable import ExamplePlugin + +final class ExamplePluginTests: XCTestCase { + func testExample() throws { + let plugin = ExamplePlugin() + } +} diff --git a/crates/tauri-cli/templates/plugin/package.json b/crates/tauri-cli/templates/plugin/package.json new file mode 100644 index 0000000..b22988d --- /dev/null +++ b/crates/tauri-cli/templates/plugin/package.json @@ -0,0 +1,33 @@ +{ + "name": "tauri-plugin-{{ plugin_name }}-api", + "version": "0.1.0", + "author": "{{ author }}", + "description": "", + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "files": [ + "dist-js", + "README.md" + ], + "scripts": { + "build": "rollup -c", + "prepublishOnly": "pnpm build", + "pretest": "pnpm build" + }, + "dependencies": { + "@tauri-apps/api": "^2.0.0" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^12.0.0", + "rollup": "^4.9.6", + "typescript": "^7.0.2", + "tslib": "^2.6.2" + } +} diff --git a/crates/tauri-cli/templates/plugin/rollup.config.js b/crates/tauri-cli/templates/plugin/rollup.config.js new file mode 100644 index 0000000..8b4768f --- /dev/null +++ b/crates/tauri-cli/templates/plugin/rollup.config.js @@ -0,0 +1,31 @@ +import { readFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { cwd } from 'node:process' +import typescript from '@rollup/plugin-typescript' + +const pkg = JSON.parse(readFileSync(join(cwd(), 'package.json'), 'utf8')) + +export default { + input: 'guest-js/index.ts', + output: [ + { + file: pkg.exports.import, + format: 'esm' + }, + { + file: pkg.exports.require, + format: 'cjs' + } + ], + plugins: [ + typescript({ + declaration: true, + declarationDir: dirname(pkg.exports.import) + }) + ], + external: [ + /^@tauri-apps\/api/, + ...Object.keys(pkg.dependencies || {}), + ...Object.keys(pkg.peerDependencies || {}) + ] +} diff --git a/crates/tauri-cli/templates/plugin/src/commands.rs b/crates/tauri-cli/templates/plugin/src/commands.rs new file mode 100644 index 0000000..80c5446 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/src/commands.rs @@ -0,0 +1,16 @@ +{{#if license_header}} +{{ license_header }} +{{/if}} +use tauri::{AppHandle, command, Runtime}; + +use crate::models::*; +use crate::Result; +use crate::{{ plugin_name_pascal_case }}Ext; + +#[command] +pub(crate) async fn ping( + app: AppHandle, + payload: PingRequest, +) -> Result { + app.{{ plugin_name_snake_case }}().ping(payload) +} diff --git a/crates/tauri-cli/templates/plugin/src/desktop.rs b/crates/tauri-cli/templates/plugin/src/desktop.rs new file mode 100644 index 0000000..0fe5b85 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/src/desktop.rs @@ -0,0 +1,25 @@ +{{#if license_header}} +{{ license_header }} +{{/if}} +use serde::de::DeserializeOwned; +use tauri::{plugin::PluginApi, AppHandle, Runtime}; + +use crate::models::*; + +pub fn init( + app: &AppHandle, + _api: PluginApi, +) -> crate::Result<{{ plugin_name_pascal_case }}> { + Ok({{ plugin_name_pascal_case }}(app.clone())) +} + +/// Access to the {{ plugin_name }} APIs. +pub struct {{ plugin_name_pascal_case }}(AppHandle); + +impl {{ plugin_name_pascal_case }} { + pub fn ping(&self, payload: PingRequest) -> crate::Result { + Ok(PingResponse { + value: payload.value, + }) + } +} diff --git a/crates/tauri-cli/templates/plugin/src/error.rs b/crates/tauri-cli/templates/plugin/src/error.rs new file mode 100644 index 0000000..cb29d46 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/src/error.rs @@ -0,0 +1,24 @@ +{{#if license_header}} +{{ license_header }} +{{/if}} +use serde::{ser::Serializer, Serialize}; + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[cfg(mobile)] + #[error(transparent)] + PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/crates/tauri-cli/templates/plugin/src/lib.rs b/crates/tauri-cli/templates/plugin/src/lib.rs new file mode 100644 index 0000000..874b99f --- /dev/null +++ b/crates/tauri-cli/templates/plugin/src/lib.rs @@ -0,0 +1,51 @@ +{{#if license_header}} +{{ license_header }} +{{/if}} +use tauri::{ + plugin::{Builder, TauriPlugin}, + Manager, Runtime, +}; + +pub use models::*; + +#[cfg(desktop)] +mod desktop; +#[cfg(mobile)] +mod mobile; + +mod commands; +mod error; +mod models; + +pub use error::{Error, Result}; + +#[cfg(desktop)] +use desktop::{{ plugin_name_pascal_case }}; +#[cfg(mobile)] +use mobile::{{ plugin_name_pascal_case }}; + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the {{ plugin_name }} APIs. +pub trait {{ plugin_name_pascal_case }}Ext { + fn {{ plugin_name_snake_case }}(&self) -> &{{ plugin_name_pascal_case }}; +} + +impl> crate::{{ plugin_name_pascal_case }}Ext for T { + fn {{ plugin_name_snake_case }}(&self) -> &{{ plugin_name_pascal_case }} { + self.state::<{{ plugin_name_pascal_case }}>().inner() + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + Builder::new("{{ plugin_name }}") + .invoke_handler(tauri::generate_handler![commands::ping]) + .setup(|app, api| { + #[cfg(mobile)] + let {{ plugin_name_snake_case }} = mobile::init(app, api)?; + #[cfg(desktop)] + let {{ plugin_name_snake_case }} = desktop::init(app, api)?; + app.manage({{ plugin_name_snake_case }}); + Ok(()) + }) + .build() +} diff --git a/crates/tauri-cli/templates/plugin/src/mobile.rs b/crates/tauri-cli/templates/plugin/src/mobile.rs new file mode 100644 index 0000000..9277ec3 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/src/mobile.rs @@ -0,0 +1,37 @@ +{{#if license_header}} +{{ license_header }} +{{/if}} +use serde::de::DeserializeOwned; +use tauri::{ + plugin::{PluginApi, PluginHandle}, + AppHandle, Runtime, +}; + +use crate::models::*; + +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_{{ plugin_name_snake_case }}); + +// initializes the Kotlin or Swift plugin classes +pub fn init( + _app: &AppHandle, + api: PluginApi, +) -> crate::Result<{{ plugin_name_pascal_case }}> { + #[cfg(target_os = "android")] + let handle = api.register_android_plugin("{{ android_package_id }}", "ExamplePlugin")?; + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_{{ plugin_name_snake_case }})?; + Ok({{ plugin_name_pascal_case }}(handle)) +} + +/// Access to the {{ plugin_name }} APIs. +pub struct {{ plugin_name_pascal_case }}(PluginHandle); + +impl {{ plugin_name_pascal_case }} { + pub fn ping(&self, payload: PingRequest) -> crate::Result { + self + .0 + .run_mobile_plugin("ping", payload) + .map_err(Into::into) + } +} diff --git a/crates/tauri-cli/templates/plugin/src/models.rs b/crates/tauri-cli/templates/plugin/src/models.rs new file mode 100644 index 0000000..914fb23 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/src/models.rs @@ -0,0 +1,16 @@ +{{#if license_header}} +{{ license_header }} +{{/if}} +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PingRequest { + pub value: Option, +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PingResponse { + pub value: Option, +} diff --git a/crates/tauri-cli/templates/plugin/tsconfig.json b/crates/tauri-cli/templates/plugin/tsconfig.json new file mode 100644 index 0000000..5e003de --- /dev/null +++ b/crates/tauri-cli/templates/plugin/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "rootDir": "guest-js", + "target": "es2021", + "module": "esnext", + "moduleResolution": "bundler", + "skipLibCheck": true, + "strict": true, + "noUnusedLocals": true, + "noImplicitAny": true, + "noEmit": true + }, + "include": ["guest-js/*.ts"], + "exclude": ["dist-js", "node_modules"] +} diff --git a/crates/tauri-cli/templates/tauri.conf.json b/crates/tauri-cli/templates/tauri.conf.json new file mode 100644 index 0000000..048fc0f --- /dev/null +++ b/crates/tauri-cli/templates/tauri.conf.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "{{ app_name }}", + "version": "0.1.0", + "identifier": "com.tauri.dev", + "build": { + "frontendDist": "{{ frontend_dist }}"{{#if dev_url}}, + "devUrl": "{{ dev_url }}"{{/if}}{{#if before_dev_command}}, + "beforeDevCommand": "{{ before_dev_command }}"{{/if}}{{#if before_build_command}}, + "beforeBuildCommand": "{{ before_build_command }}"{{/if}} + }, + "app": { + "windows": [ + { + "title": "{{ window_title }}", + "width": 800, + "height": 600, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "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 new file mode 100644 index 0000000..2604ffd --- /dev/null +++ b/crates/tauri-cli/tests/fixtures/pbxproj/project.pbxproj @@ -0,0 +1,467 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 2B78BA327A38C76093D36092 /* libapi_lib.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A00E5F95D64FD14E47F85BD /* libapi_lib.a */; }; + 3043432501C9BC2DB6B4CB95 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71EB788DE4662CFC0D97F567 /* CoreGraphics.framework */; }; + 328B4ADB3700C1873BEB7B10 /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = 90D3B673AFAB8D8AB561F616 /* main.mm */; }; + 6F379F15DA085785BA2624D4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6B7E79E23E646BA7968B457C /* Assets.xcassets */; }; + 9AADB041D25772D04E543F15 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62601E25FA39E62BE119B74D /* Metal.framework */; }; + 9DDA3BE70DD0E4013973FE38 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6082E363D51372A7658C351 /* UIKit.framework */; }; + AC8BDC2C7A63FA3FDC5967F4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B2D1B108AE002010BDEC6D2 /* LaunchScreen.storyboard */; }; + AFA0CA286325FD7A34968CA2 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384966E551417F94A02D2706 /* Security.framework */; }; + B60763BD194DFACA215EC7DA /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC377692DC31A070A0188C9D /* QuartzCore.framework */; }; + C6D80743F168BDF017B7769E /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59CFE20DCF760BE67D9CE3D6 /* WebKit.framework */; }; + DFFF888045C8D9D9FB69E8FD /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 338E66700FD330B99D434DD7 /* MetalKit.framework */; }; + F86717F05E27C72C9FA1FB27 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 74A8FDFB350B966F5AAD4A24 /* assets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0E96CE07CD20273DD46BF325 /* main.rs */ = {isa = PBXFileReference; path = main.rs; sourceTree = ""; }; + 1C1AB1B414CA2795AFBEDDB9 /* tray.rs */ = {isa = PBXFileReference; path = tray.rs; sourceTree = ""; }; + 2F63E2AA460089BB58D40C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 338E66700FD330B99D434DD7 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; }; + 384966E551417F94A02D2706 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 3CA88F22095BE63D88585625 /* menu_plugin.rs */ = {isa = PBXFileReference; path = menu_plugin.rs; sourceTree = ""; }; + 4A00E5F95D64FD14E47F85BD /* libapi_lib.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libapi_lib.a; sourceTree = ""; }; + 4B2D1B108AE002010BDEC6D2 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 59CFE20DCF760BE67D9CE3D6 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + 5AC703CEBA41A121596066F3 /* api_iOS.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = api_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 62601E25FA39E62BE119B74D /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; }; + 6B7E79E23E646BA7968B457C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 71EB788DE4662CFC0D97F567 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 74A8FDFB350B966F5AAD4A24 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = SOURCE_ROOT; }; + 785D025E9542F7E098BF22B5 /* lib.rs */ = {isa = PBXFileReference; path = lib.rs; sourceTree = ""; }; + 879941AE3DAA14534BBC6391 /* api_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = api_iOS.entitlements; sourceTree = ""; }; + 90D3B673AFAB8D8AB561F616 /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; + B6082E363D51372A7658C351 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + DC377692DC31A070A0188C9D /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + EC8C7948C50C3C9B5D96CB61 /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = ""; }; + F835F52713CE8F029D5D252C /* cmd.rs */ = {isa = PBXFileReference; path = cmd.rs; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 11E18DCDB3ADFE87C18915EF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2B78BA327A38C76093D36092 /* libapi_lib.a in Frameworks */, + 3043432501C9BC2DB6B4CB95 /* CoreGraphics.framework in Frameworks */, + 9AADB041D25772D04E543F15 /* Metal.framework in Frameworks */, + DFFF888045C8D9D9FB69E8FD /* MetalKit.framework in Frameworks */, + B60763BD194DFACA215EC7DA /* QuartzCore.framework in Frameworks */, + AFA0CA286325FD7A34968CA2 /* Security.framework in Frameworks */, + 9DDA3BE70DD0E4013973FE38 /* UIKit.framework in Frameworks */, + C6D80743F168BDF017B7769E /* WebKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0677CEAF1F282F38CBA0F140 = { + isa = PBXGroup; + children = ( + 74A8FDFB350B966F5AAD4A24 /* assets */, + 6B7E79E23E646BA7968B457C /* Assets.xcassets */, + 4B2D1B108AE002010BDEC6D2 /* LaunchScreen.storyboard */, + F2116A6428EED18BE2A07E2B /* api_iOS */, + 86D903732E10FAC4D300E8DF /* Externals */, + 7A9A7AC155D9E22E54D6D847 /* Sources */, + CF9AA87D2F6E9C389B7AB70B /* src */, + 10C9FC3FA3E12D6A4A67999D /* Frameworks */, + 4AC51E67B71E27F15B02C5CD /* Products */, + ); + sourceTree = ""; + }; + 07051859D6E2D8109C8FB128 /* bindings */ = { + isa = PBXGroup; + children = ( + EC8C7948C50C3C9B5D96CB61 /* bindings.h */, + ); + path = bindings; + sourceTree = ""; + }; + 10C9FC3FA3E12D6A4A67999D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 71EB788DE4662CFC0D97F567 /* CoreGraphics.framework */, + 4A00E5F95D64FD14E47F85BD /* libapi_lib.a */, + 62601E25FA39E62BE119B74D /* Metal.framework */, + 338E66700FD330B99D434DD7 /* MetalKit.framework */, + DC377692DC31A070A0188C9D /* QuartzCore.framework */, + 384966E551417F94A02D2706 /* Security.framework */, + B6082E363D51372A7658C351 /* UIKit.framework */, + 59CFE20DCF760BE67D9CE3D6 /* WebKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 4AC51E67B71E27F15B02C5CD /* Products */ = { + isa = PBXGroup; + children = ( + 5AC703CEBA41A121596066F3 /* api_iOS.app */, + ); + name = Products; + sourceTree = ""; + }; + 7A9A7AC155D9E22E54D6D847 /* Sources */ = { + isa = PBXGroup; + children = ( + A3574F52DBC5463B9C3D043D /* api */, + ); + path = Sources; + sourceTree = ""; + }; + 86D903732E10FAC4D300E8DF /* Externals */ = { + isa = PBXGroup; + children = ( + ); + path = Externals; + sourceTree = ""; + }; + A3574F52DBC5463B9C3D043D /* api */ = { + isa = PBXGroup; + children = ( + 90D3B673AFAB8D8AB561F616 /* main.mm */, + 07051859D6E2D8109C8FB128 /* bindings */, + ); + path = api; + sourceTree = ""; + }; + CF9AA87D2F6E9C389B7AB70B /* src */ = { + isa = PBXGroup; + children = ( + F835F52713CE8F029D5D252C /* cmd.rs */, + 785D025E9542F7E098BF22B5 /* lib.rs */, + 0E96CE07CD20273DD46BF325 /* main.rs */, + 3CA88F22095BE63D88585625 /* menu_plugin.rs */, + 1C1AB1B414CA2795AFBEDDB9 /* tray.rs */, + ); + name = src; + path = ../../src; + sourceTree = ""; + }; + F2116A6428EED18BE2A07E2B /* api_iOS */ = { + isa = PBXGroup; + children = ( + 879941AE3DAA14534BBC6391 /* api_iOS.entitlements */, + 2F63E2AA460089BB58D40C79 /* Info.plist */, + ); + path = api_iOS; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 54DC6E273C78071F3BA12EF3 /* api_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 01CBC40275452376830D79B1 /* Build configuration list for PBXNativeTarget "api_iOS" */; + buildPhases = ( + FF948951157DE71465B5BD5F /* Build Rust Code */, + 71E73CC9AB5F1323EC1F6365 /* Sources */, + CA2BEC44B6EDA1F21B6155CD /* Resources */, + 11E18DCDB3ADFE87C18915EF /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = api_iOS; + productName = api_iOS; + productReference = 5AC703CEBA41A121596066F3 /* api_iOS.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 9BC88C3717DA5D4B78A51C15 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + TargetAttributes = { + 54DC6E273C78071F3BA12EF3 = { + DevelopmentTeam = Q93MBH6S2F; + }; + }; + }; + buildConfigurationList = 8FA67D0F928A09CD639137D1 /* Build configuration list for PBXProject "api" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = 0677CEAF1F282F38CBA0F140; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 54DC6E273C78071F3BA12EF3 /* api_iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + CA2BEC44B6EDA1F21B6155CD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6F379F15DA085785BA2624D4 /* Assets.xcassets in Resources */, + AC8BDC2C7A63FA3FDC5967F4 /* LaunchScreen.storyboard in Resources */, + F86717F05E27C72C9FA1FB27 /* assets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + FF948951157DE71465B5BD5F /* Build Rust Code */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build Rust Code"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapi_lib.a", + "$(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapi_lib.a", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cargo tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths \"${FRAMEWORK_SEARCH_PATHS:?}\" --header-search-paths \"${HEADER_SEARCH_PATHS:?}\" --gcc-preprocessor-definitions \"${GCC_PREPROCESSOR_DEFINITIONS:-}\" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 71E73CC9AB5F1323EC1F6365 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 328B4ADB3700C1873BEB7B10 /* main.mm in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A83F70B4C02DD0222038C7F1 /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = release; + }; + B6AD77E490F315562F75D3D7 /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = debug; + }; + BF284FE6E7AE0C8DDCCE398B /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = ( + arm64, + ); + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = api_iOS/api_iOS.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = Q93MBH6S2F; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = "x86_64"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\".\"", + ); + INFOPLIST_FILE = api_iOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "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; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = "arm64"; + }; + name = debug; + }; + DB_0E254D0FD84970B57F6410 /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = ( + arm64, + ); + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = api_iOS/api_iOS.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = Q93MBH6S2F; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = "x86_64"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\".\"", + ); + INFOPLIST_FILE = api_iOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "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; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = "arm64"; + }; + name = release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 01CBC40275452376830D79B1 /* Build configuration list for PBXNativeTarget "api_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BF284FE6E7AE0C8DDCCE398B /* debug */, + DB_0E254D0FD84970B57F6410 /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; + 8FA67D0F928A09CD639137D1 /* Build configuration list for PBXProject "api" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B6AD77E490F315562F75D3D7 /* debug */, + A83F70B4C02DD0222038C7F1 /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 9BC88C3717DA5D4B78A51C15 /* Project object */; +} 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 new file mode 100644 index 0000000..97ba4c6 --- /dev/null +++ b/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project-modified.pbxproj.snap @@ -0,0 +1,472 @@ +--- +source: crates/tauri-cli/src/helpers/pbxproj.rs +expression: pbxproj.serialize() +--- +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 2B78BA327A38C76093D36092 /* libapi_lib.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A00E5F95D64FD14E47F85BD /* libapi_lib.a */; }; + 3043432501C9BC2DB6B4CB95 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71EB788DE4662CFC0D97F567 /* CoreGraphics.framework */; }; + 328B4ADB3700C1873BEB7B10 /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = 90D3B673AFAB8D8AB561F616 /* main.mm */; }; + 6F379F15DA085785BA2624D4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6B7E79E23E646BA7968B457C /* Assets.xcassets */; }; + 9AADB041D25772D04E543F15 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62601E25FA39E62BE119B74D /* Metal.framework */; }; + 9DDA3BE70DD0E4013973FE38 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6082E363D51372A7658C351 /* UIKit.framework */; }; + AC8BDC2C7A63FA3FDC5967F4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B2D1B108AE002010BDEC6D2 /* LaunchScreen.storyboard */; }; + AFA0CA286325FD7A34968CA2 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384966E551417F94A02D2706 /* Security.framework */; }; + B60763BD194DFACA215EC7DA /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC377692DC31A070A0188C9D /* QuartzCore.framework */; }; + C6D80743F168BDF017B7769E /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59CFE20DCF760BE67D9CE3D6 /* WebKit.framework */; }; + DFFF888045C8D9D9FB69E8FD /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 338E66700FD330B99D434DD7 /* MetalKit.framework */; }; + F86717F05E27C72C9FA1FB27 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 74A8FDFB350B966F5AAD4A24 /* assets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0E96CE07CD20273DD46BF325 /* main.rs */ = {isa = PBXFileReference; path = main.rs; sourceTree = ""; }; + 1C1AB1B414CA2795AFBEDDB9 /* tray.rs */ = {isa = PBXFileReference; path = tray.rs; sourceTree = ""; }; + 2F63E2AA460089BB58D40C79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 338E66700FD330B99D434DD7 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; }; + 384966E551417F94A02D2706 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 3CA88F22095BE63D88585625 /* menu_plugin.rs */ = {isa = PBXFileReference; path = menu_plugin.rs; sourceTree = ""; }; + 4A00E5F95D64FD14E47F85BD /* libapi_lib.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libapi_lib.a; sourceTree = ""; }; + 4B2D1B108AE002010BDEC6D2 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 59CFE20DCF760BE67D9CE3D6 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + 5AC703CEBA41A121596066F3 /* api_iOS.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = api_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 62601E25FA39E62BE119B74D /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; }; + 6B7E79E23E646BA7968B457C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 71EB788DE4662CFC0D97F567 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 74A8FDFB350B966F5AAD4A24 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = assets; sourceTree = SOURCE_ROOT; }; + 785D025E9542F7E098BF22B5 /* lib.rs */ = {isa = PBXFileReference; path = lib.rs; sourceTree = ""; }; + 879941AE3DAA14534BBC6391 /* api_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = api_iOS.entitlements; sourceTree = ""; }; + 90D3B673AFAB8D8AB561F616 /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; + B6082E363D51372A7658C351 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + DC377692DC31A070A0188C9D /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + EC8C7948C50C3C9B5D96CB61 /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = ""; }; + F835F52713CE8F029D5D252C /* cmd.rs */ = {isa = PBXFileReference; path = cmd.rs; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 11E18DCDB3ADFE87C18915EF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2B78BA327A38C76093D36092 /* libapi_lib.a in Frameworks */, + 3043432501C9BC2DB6B4CB95 /* CoreGraphics.framework in Frameworks */, + 9AADB041D25772D04E543F15 /* Metal.framework in Frameworks */, + DFFF888045C8D9D9FB69E8FD /* MetalKit.framework in Frameworks */, + B60763BD194DFACA215EC7DA /* QuartzCore.framework in Frameworks */, + AFA0CA286325FD7A34968CA2 /* Security.framework in Frameworks */, + 9DDA3BE70DD0E4013973FE38 /* UIKit.framework in Frameworks */, + C6D80743F168BDF017B7769E /* WebKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0677CEAF1F282F38CBA0F140 = { + isa = PBXGroup; + children = ( + 74A8FDFB350B966F5AAD4A24 /* assets */, + 6B7E79E23E646BA7968B457C /* Assets.xcassets */, + 4B2D1B108AE002010BDEC6D2 /* LaunchScreen.storyboard */, + F2116A6428EED18BE2A07E2B /* api_iOS */, + 86D903732E10FAC4D300E8DF /* Externals */, + 7A9A7AC155D9E22E54D6D847 /* Sources */, + CF9AA87D2F6E9C389B7AB70B /* src */, + 10C9FC3FA3E12D6A4A67999D /* Frameworks */, + 4AC51E67B71E27F15B02C5CD /* Products */, + ); + sourceTree = ""; + }; + 07051859D6E2D8109C8FB128 /* bindings */ = { + isa = PBXGroup; + children = ( + EC8C7948C50C3C9B5D96CB61 /* bindings.h */, + ); + path = bindings; + sourceTree = ""; + }; + 10C9FC3FA3E12D6A4A67999D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 71EB788DE4662CFC0D97F567 /* CoreGraphics.framework */, + 4A00E5F95D64FD14E47F85BD /* libapi_lib.a */, + 62601E25FA39E62BE119B74D /* Metal.framework */, + 338E66700FD330B99D434DD7 /* MetalKit.framework */, + DC377692DC31A070A0188C9D /* QuartzCore.framework */, + 384966E551417F94A02D2706 /* Security.framework */, + B6082E363D51372A7658C351 /* UIKit.framework */, + 59CFE20DCF760BE67D9CE3D6 /* WebKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 4AC51E67B71E27F15B02C5CD /* Products */ = { + isa = PBXGroup; + children = ( + 5AC703CEBA41A121596066F3 /* api_iOS.app */, + ); + name = Products; + sourceTree = ""; + }; + 7A9A7AC155D9E22E54D6D847 /* Sources */ = { + isa = PBXGroup; + children = ( + A3574F52DBC5463B9C3D043D /* api */, + ); + path = Sources; + sourceTree = ""; + }; + 86D903732E10FAC4D300E8DF /* Externals */ = { + isa = PBXGroup; + children = ( + ); + path = Externals; + sourceTree = ""; + }; + A3574F52DBC5463B9C3D043D /* api */ = { + isa = PBXGroup; + children = ( + 90D3B673AFAB8D8AB561F616 /* main.mm */, + 07051859D6E2D8109C8FB128 /* bindings */, + ); + path = api; + sourceTree = ""; + }; + CF9AA87D2F6E9C389B7AB70B /* src */ = { + isa = PBXGroup; + children = ( + F835F52713CE8F029D5D252C /* cmd.rs */, + 785D025E9542F7E098BF22B5 /* lib.rs */, + 0E96CE07CD20273DD46BF325 /* main.rs */, + 3CA88F22095BE63D88585625 /* menu_plugin.rs */, + 1C1AB1B414CA2795AFBEDDB9 /* tray.rs */, + ); + name = src; + path = ../../src; + sourceTree = ""; + }; + F2116A6428EED18BE2A07E2B /* api_iOS */ = { + isa = PBXGroup; + children = ( + 879941AE3DAA14534BBC6391 /* api_iOS.entitlements */, + 2F63E2AA460089BB58D40C79 /* Info.plist */, + ); + path = api_iOS; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 54DC6E273C78071F3BA12EF3 /* api_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 01CBC40275452376830D79B1 /* Build configuration list for PBXNativeTarget "api_iOS" */; + buildPhases = ( + FF948951157DE71465B5BD5F /* Build Rust Code */, + 71E73CC9AB5F1323EC1F6365 /* Sources */, + CA2BEC44B6EDA1F21B6155CD /* Resources */, + 11E18DCDB3ADFE87C18915EF /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = api_iOS; + productName = api_iOS; + productReference = 5AC703CEBA41A121596066F3 /* api_iOS.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 9BC88C3717DA5D4B78A51C15 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + TargetAttributes = { + 54DC6E273C78071F3BA12EF3 = { + DevelopmentTeam = Q93MBH6S2F; + }; + }; + }; + buildConfigurationList = 8FA67D0F928A09CD639137D1 /* Build configuration list for PBXProject "api" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = 0677CEAF1F282F38CBA0F140; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 54DC6E273C78071F3BA12EF3 /* api_iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + CA2BEC44B6EDA1F21B6155CD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6F379F15DA085785BA2624D4 /* Assets.xcassets in Resources */, + AC8BDC2C7A63FA3FDC5967F4 /* LaunchScreen.storyboard in Resources */, + F86717F05E27C72C9FA1FB27 /* assets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + FF948951157DE71465B5BD5F /* Build Rust Code */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build Rust Code"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapi_lib.a", + "$(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapi_lib.a", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cargo tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths \"${FRAMEWORK_SEARCH_PATHS:?}\" --header-search-paths \"${HEADER_SEARCH_PATHS:?}\" --gcc-preprocessor-definitions \"${GCC_PREPROCESSOR_DEFINITIONS:-}\" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 71E73CC9AB5F1323EC1F6365 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 328B4ADB3700C1873BEB7B10 /* main.mm in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A83F70B4C02DD0222038C7F1 /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = release; + }; + B6AD77E490F315562F75D3D7 /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = debug; + }; + BF284FE6E7AE0C8DDCCE398B /* debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = ( + arm64, + ); + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = api_iOS/api_iOS.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = Q93MBH6S2F; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = "x86_64"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\".\"", + ); + INFOPLIST_FILE = api_iOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "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; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = "arm64"; + }; + name = debug; + }; + DB_0E254D0FD84970B57F6410 /* release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = ( + arm64, + ); + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = api_iOS/api_iOS.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = Q93MBH6S2F; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = "x86_64"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\".\"", + ); + INFOPLIST_FILE = api_iOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "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; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = "arm64"; + UNKNOWN = 9283j49238h; + }; + name = release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 01CBC40275452376830D79B1 /* Build configuration list for PBXNativeTarget "api_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BF284FE6E7AE0C8DDCCE398B /* debug */, + DB_0E254D0FD84970B57F6410 /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; + 8FA67D0F928A09CD639137D1 /* Build configuration list for PBXProject "api" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B6AD77E490F315562F75D3D7 /* debug */, + A83F70B4C02DD0222038C7F1 /* release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 9BC88C3717DA5D4B78A51C15 /* Project object */; +} 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 new file mode 100644 index 0000000..24c4db8 --- /dev/null +++ b/crates/tauri-cli/tests/fixtures/pbxproj/snapshots/tauri_cli__helpers__pbxproj__tests__project.pbxproj.snap @@ -0,0 +1,892 @@ +--- +source: crates/tauri-cli/src/helpers/pbxproj.rs +expression: "super::parse(fixtures_path.join(\"project.pbxproj\")).expect(\"failed to parse pbxproj\")" +--- +Pbxproj { + xc_build_configuration: { + "A83F70B4C02DD0222038C7F1": XCBuildConfiguration { + build_settings: [ + BuildSettings { + identation: "\t\t\t\t", + line_number: 262, + key: "ALWAYS_SEARCH_USER_PATHS", + value: "NO", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 263, + key: "CLANG_ANALYZER_NONNULL", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 264, + key: "CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION", + value: "YES_AGGRESSIVE", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 265, + key: "CLANG_CXX_LANGUAGE_STANDARD", + value: "\"gnu++14\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 266, + key: "CLANG_CXX_LIBRARY", + value: "\"libc++\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 267, + key: "CLANG_ENABLE_MODULES", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 268, + key: "CLANG_ENABLE_OBJC_ARC", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 269, + key: "CLANG_ENABLE_OBJC_WEAK", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 270, + key: "CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 271, + key: "CLANG_WARN_BOOL_CONVERSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 272, + key: "CLANG_WARN_COMMA", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 273, + key: "CLANG_WARN_CONSTANT_CONVERSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 274, + key: "CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 275, + key: "CLANG_WARN_DIRECT_OBJC_ISA_USAGE", + value: "YES_ERROR", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 276, + key: "CLANG_WARN_DOCUMENTATION_COMMENTS", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 277, + key: "CLANG_WARN_EMPTY_BODY", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 278, + key: "CLANG_WARN_ENUM_CONVERSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 279, + key: "CLANG_WARN_INFINITE_RECURSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 280, + key: "CLANG_WARN_INT_CONVERSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 281, + key: "CLANG_WARN_NON_LITERAL_NULL_CONVERSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 282, + key: "CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 283, + key: "CLANG_WARN_OBJC_LITERAL_CONVERSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 284, + key: "CLANG_WARN_OBJC_ROOT_CLASS", + value: "YES_ERROR", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 285, + key: "CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 286, + key: "CLANG_WARN_RANGE_LOOP_ANALYSIS", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 287, + key: "CLANG_WARN_STRICT_PROTOTYPES", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 288, + key: "CLANG_WARN_SUSPICIOUS_MOVE", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 289, + key: "CLANG_WARN_UNGUARDED_AVAILABILITY", + value: "YES_AGGRESSIVE", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 290, + key: "CLANG_WARN_UNREACHABLE_CODE", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 291, + key: "CLANG_WARN__DUPLICATE_METHOD_MATCH", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 292, + key: "COPY_PHASE_STRIP", + value: "NO", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 293, + key: "DEBUG_INFORMATION_FORMAT", + value: "\"dwarf-with-dsym\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 294, + key: "ENABLE_NS_ASSERTIONS", + value: "NO", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 295, + key: "ENABLE_STRICT_OBJC_MSGSEND", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 296, + key: "GCC_C_LANGUAGE_STANDARD", + value: "gnu11", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 297, + key: "GCC_NO_COMMON_BLOCKS", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 298, + key: "GCC_WARN_64_TO_32_BIT_CONVERSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 299, + key: "GCC_WARN_ABOUT_RETURN_TYPE", + value: "YES_ERROR", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 300, + key: "GCC_WARN_UNDECLARED_SELECTOR", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 301, + key: "GCC_WARN_UNINITIALIZED_AUTOS", + value: "YES_AGGRESSIVE", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 302, + key: "GCC_WARN_UNUSED_FUNCTION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 303, + key: "GCC_WARN_UNUSED_VARIABLE", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 304, + key: "IPHONEOS_DEPLOYMENT_TARGET", + value: "14.0", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 305, + key: "MTL_ENABLE_DEBUG_INFO", + value: "NO", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 306, + key: "MTL_FAST_MATH", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 307, + key: "PRODUCT_NAME", + value: "\"$(TARGET_NAME)\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 308, + key: "SDKROOT", + value: "iphoneos", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 309, + key: "SWIFT_COMPILATION_MODE", + value: "wholemodule", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 310, + key: "SWIFT_OPTIMIZATION_LEVEL", + value: "\"-O\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 311, + key: "SWIFT_VERSION", + value: "5.0", + }, + ], + }, + "B6AD77E490F315562F75D3D7": XCBuildConfiguration { + build_settings: [ + BuildSettings { + identation: "\t\t\t\t", + line_number: 318, + key: "ALWAYS_SEARCH_USER_PATHS", + value: "NO", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 319, + key: "CLANG_ANALYZER_NONNULL", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 320, + key: "CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION", + value: "YES_AGGRESSIVE", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 321, + key: "CLANG_CXX_LANGUAGE_STANDARD", + value: "\"gnu++14\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 322, + key: "CLANG_CXX_LIBRARY", + value: "\"libc++\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 323, + key: "CLANG_ENABLE_MODULES", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 324, + key: "CLANG_ENABLE_OBJC_ARC", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 325, + key: "CLANG_ENABLE_OBJC_WEAK", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 326, + key: "CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 327, + key: "CLANG_WARN_BOOL_CONVERSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 328, + key: "CLANG_WARN_COMMA", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 329, + key: "CLANG_WARN_CONSTANT_CONVERSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 330, + key: "CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 331, + key: "CLANG_WARN_DIRECT_OBJC_ISA_USAGE", + value: "YES_ERROR", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 332, + key: "CLANG_WARN_DOCUMENTATION_COMMENTS", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 333, + key: "CLANG_WARN_EMPTY_BODY", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 334, + key: "CLANG_WARN_ENUM_CONVERSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 335, + key: "CLANG_WARN_INFINITE_RECURSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 336, + key: "CLANG_WARN_INT_CONVERSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 337, + key: "CLANG_WARN_NON_LITERAL_NULL_CONVERSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 338, + key: "CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 339, + key: "CLANG_WARN_OBJC_LITERAL_CONVERSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 340, + key: "CLANG_WARN_OBJC_ROOT_CLASS", + value: "YES_ERROR", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 341, + key: "CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 342, + key: "CLANG_WARN_RANGE_LOOP_ANALYSIS", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 343, + key: "CLANG_WARN_STRICT_PROTOTYPES", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 344, + key: "CLANG_WARN_SUSPICIOUS_MOVE", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 345, + key: "CLANG_WARN_UNGUARDED_AVAILABILITY", + value: "YES_AGGRESSIVE", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 346, + key: "CLANG_WARN_UNREACHABLE_CODE", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 347, + key: "CLANG_WARN__DUPLICATE_METHOD_MATCH", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 348, + key: "COPY_PHASE_STRIP", + value: "NO", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 349, + key: "DEBUG_INFORMATION_FORMAT", + value: "dwarf", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 350, + key: "ENABLE_STRICT_OBJC_MSGSEND", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 351, + key: "ENABLE_TESTABILITY", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 352, + key: "GCC_C_LANGUAGE_STANDARD", + value: "gnu11", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 353, + key: "GCC_DYNAMIC_NO_PIC", + value: "NO", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 354, + key: "GCC_NO_COMMON_BLOCKS", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 355, + key: "GCC_OPTIMIZATION_LEVEL", + value: "0", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 356, + key: "GCC_PREPROCESSOR_DEFINITIONS", + value: "(\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t);\n", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 360, + key: "GCC_WARN_64_TO_32_BIT_CONVERSION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 361, + key: "GCC_WARN_ABOUT_RETURN_TYPE", + value: "YES_ERROR", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 362, + key: "GCC_WARN_UNDECLARED_SELECTOR", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 363, + key: "GCC_WARN_UNINITIALIZED_AUTOS", + value: "YES_AGGRESSIVE", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 364, + key: "GCC_WARN_UNUSED_FUNCTION", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 365, + key: "GCC_WARN_UNUSED_VARIABLE", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 366, + key: "IPHONEOS_DEPLOYMENT_TARGET", + value: "14.0", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 367, + key: "MTL_ENABLE_DEBUG_INFO", + value: "INCLUDE_SOURCE", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 368, + key: "MTL_FAST_MATH", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 369, + key: "ONLY_ACTIVE_ARCH", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 370, + key: "PRODUCT_NAME", + value: "\"$(TARGET_NAME)\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 371, + key: "SDKROOT", + value: "iphoneos", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 372, + key: "SWIFT_ACTIVE_COMPILATION_CONDITIONS", + value: "DEBUG", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 373, + key: "SWIFT_OPTIMIZATION_LEVEL", + value: "\"-Onone\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 374, + key: "SWIFT_VERSION", + value: "5.0", + }, + ], + }, + "BF284FE6E7AE0C8DDCCE398B": XCBuildConfiguration { + build_settings: [ + BuildSettings { + identation: "\t\t\t\t", + line_number: 381, + key: "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 382, + key: "ARCHS", + value: "(\t\t\t\t\tarm64,\n\t\t\t\t);\n", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 385, + key: "ASSETCATALOG_COMPILER_APPICON_NAME", + value: "AppIcon", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 386, + key: "CODE_SIGN_ENTITLEMENTS", + value: "api_iOS/api_iOS.entitlements", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 387, + key: "CODE_SIGN_IDENTITY", + value: "\"iPhone Developer\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 388, + key: "DEVELOPMENT_TEAM", + value: "Q93MBH6S2F", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 389, + key: "ENABLE_BITCODE", + value: "NO", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 390, + key: "\"EXCLUDED_ARCHS[sdk=iphoneos*]\"", + value: "\"x86_64\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 391, + key: "FRAMEWORK_SEARCH_PATHS", + value: "(\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"\\\".\\\"\",\n\t\t\t\t);\n", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 395, + key: "INFOPLIST_FILE", + value: "api_iOS/Info.plist", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 396, + key: "LD_RUNPATH_SEARCH_PATHS", + value: "(\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n", + }, + BuildSettings { + 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 $(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 $(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: 402, + key: "PRODUCT_BUNDLE_IDENTIFIER", + value: "com.tauri.api", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 403, + key: "PRODUCT_NAME", + value: "\"Tauri API\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 404, + key: "SDKROOT", + value: "iphoneos", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 405, + key: "TARGETED_DEVICE_FAMILY", + value: "\"1,2\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 406, + key: "VALID_ARCHS", + value: "\"arm64\"", + }, + ], + }, + "DB_0E254D0FD84970B57F6410": XCBuildConfiguration { + build_settings: [ + BuildSettings { + identation: "\t\t\t\t", + line_number: 413, + key: "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", + value: "YES", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 414, + key: "ARCHS", + value: "(\t\t\t\t\tarm64,\n\t\t\t\t);\n", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 417, + key: "ASSETCATALOG_COMPILER_APPICON_NAME", + value: "AppIcon", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 418, + key: "CODE_SIGN_ENTITLEMENTS", + value: "api_iOS/api_iOS.entitlements", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 419, + key: "CODE_SIGN_IDENTITY", + value: "\"iPhone Developer\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 420, + key: "DEVELOPMENT_TEAM", + value: "Q93MBH6S2F", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 421, + key: "ENABLE_BITCODE", + value: "NO", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 422, + key: "\"EXCLUDED_ARCHS[sdk=iphoneos*]\"", + value: "\"x86_64\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 423, + key: "FRAMEWORK_SEARCH_PATHS", + value: "(\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"\\\".\\\"\",\n\t\t\t\t);\n", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 427, + key: "INFOPLIST_FILE", + value: "api_iOS/Info.plist", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 428, + key: "LD_RUNPATH_SEARCH_PATHS", + value: "(\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n", + }, + BuildSettings { + 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 $(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 $(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: 434, + key: "PRODUCT_BUNDLE_IDENTIFIER", + value: "com.tauri.api", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 435, + key: "PRODUCT_NAME", + value: "\"Tauri API\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 436, + key: "SDKROOT", + value: "iphoneos", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 437, + key: "TARGETED_DEVICE_FAMILY", + value: "\"1,2\"", + }, + BuildSettings { + identation: "\t\t\t\t", + line_number: 438, + key: "VALID_ARCHS", + value: "\"arm64\"", + }, + ], + }, + }, + xc_configuration_list: { + "01CBC40275452376830D79B1": XCConfigurationList { + comment: "/* Build configuration list for PBXNativeTarget \"api_iOS\" */", + build_configurations: [ + BuildConfigurationRef { + id: "BF284FE6E7AE0C8DDCCE398B", + comments: "/* debug */", + }, + BuildConfigurationRef { + id: "DB_0E254D0FD84970B57F6410", + comments: "/* release */", + }, + ], + }, + "8FA67D0F928A09CD639137D1": XCConfigurationList { + comment: "/* Build configuration list for PBXProject \"api\" */", + build_configurations: [ + BuildConfigurationRef { + id: "B6AD77E490F315562F75D3D7", + comments: "/* debug */", + }, + BuildConfigurationRef { + id: "A83F70B4C02DD0222038C7F1", + comments: "/* release */", + }, + ], + }, + }, +} diff --git a/crates/tauri-cli/tests/init.rs b/crates/tauri-cli/tests/init.rs new file mode 100644 index 0000000..6022d1a --- /dev/null +++ b/crates/tauri-cli/tests/init.rs @@ -0,0 +1,54 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::fs; +use std::process::Command; + +#[test] +fn init_non_interactive_works() { + let temp_dir = tempfile::tempdir().unwrap(); + let dir = temp_dir.path(); + + // Determine the path to the cargo-tauri binary. + // The test binary is in target/debug/deps/, so we navigate up to target/debug/ + let current_exe = std::env::current_exe().unwrap(); + let target_debug = current_exe.parent().unwrap().parent().unwrap(); + let cargo_tauri = if cfg!(windows) { + target_debug.join("cargo-tauri.exe") + } else { + target_debug.join("cargo-tauri") + }; + + assert!( + cargo_tauri.exists(), + "cargo-tauri binary not found at {:?}", + cargo_tauri + ); + + // Run `init` without --force or --ci; since stdin is not a terminal in tests, + // it should automatically skip prompts and use defaults. + let output = Command::new(&cargo_tauri) + .arg("init") + .arg("--app-name") + .arg("testapp") + .arg("--frontend-dist") + .arg("../dist") + .arg("--dev-url") + .arg("http://localhost:3000") + .current_dir(dir) + .output() + .expect("failed to run cargo-tauri init"); + + assert!(output.status.success(), "init failed: {:?}", output); + + // Verify that src-tauri was created. + let src_tauri = dir.join("src-tauri"); + assert!(src_tauri.exists(), "src-tauri directory was not created"); + + // Verify that the tauri.conf.json contains the app name we set. + let conf_path = src_tauri.join("tauri.conf.json"); + assert!(conf_path.exists(), "tauri.conf.json not found"); + let content = fs::read_to_string(&conf_path).unwrap(); + assert!(content.contains("testapp"), "app name not found in config"); +} diff --git a/crates/tauri-codegen/CHANGELOG.md b/crates/tauri-codegen/CHANGELOG.md new file mode 100644 index 0000000..34118aa --- /dev/null +++ b/crates/tauri-codegen/CHANGELOG.md @@ -0,0 +1,799 @@ +# Changelog + +## \[2.6.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.3` + +## \[2.6.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.2` + +## \[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 + +- Upgraded to `tauri-utils@2.8.3` + +## \[2.5.4] + +### Bug Fixes + +- [`eb5d88427`](https://www.github.com/tauri-apps/tauri/commit/eb5d88427a7dcb347fb0feae9e816db05b101844) ([#14883](https://www.github.com/tauri-apps/tauri/pull/14883) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix `tauri::Context` code generation failing with `can't capture dynamic environment in a fn item` when custom assets are provided. + +## \[2.5.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.8.2` + +## \[2.5.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.8.1` + +## \[2.5.1] + +### Performance Improvements + +- [`8e3bd63db`](https://www.github.com/tauri-apps/tauri/commit/8e3bd63db919a4cf72bb3d28028033d8654deb34) ([#14457](https://www.github.com/tauri-apps/tauri/pull/14457) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Wrap the generated context code in a function to make rust analyzer faster + +## \[2.5.0] + +### Bug Fixes + +- [`c5008b829`](https://www.github.com/tauri-apps/tauri/commit/c5008b829dc779f0768089bff9b891fc76b11355) ([#14274](https://www.github.com/tauri-apps/tauri/pull/14274)) Do not hash empty scripts when generating the Content-Security-Policy SHA-256 hashes. +- [`7b0d4e732`](https://www.github.com/tauri-apps/tauri/commit/7b0d4e73227e42d88732b6d9fe643499dd78ec4e) ([#14265](https://www.github.com/tauri-apps/tauri/pull/14265)) Fix JavaScript SHA256 hash generation on Windows not ignoring carriage return characters. + +### Dependencies + +- Upgraded to `tauri-utils@2.8.0` +- [`6aa7f2d85`](https://www.github.com/tauri-apps/tauri/commit/6aa7f2d852870aeba1d4dd0e07f8be2bc9b66286) Upgraded to `tauri-utils@2.8.0` + +## \[2.4.0] + +### Dependencies + +- Upgraded to `tauri-utils@2.7.0`. + +## \[2.3.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.6.0` + +## \[2.3.0] + +### New Features + +- [`414619c36`](https://www.github.com/tauri-apps/tauri/commit/414619c36e94e21939534dd72c0438b93da75546) ([#13536](https://www.github.com/tauri-apps/tauri/pull/13536) by [@Tunglies](https://www.github.com/tauri-apps/tauri/../../Tunglies)) Added support for the `bundleName` property in the macOS bundler configuration. This allows specifying the `CFBundleName` value for generated macOS bundles. + +### What's Changed + +- [`168629646`](https://www.github.com/tauri-apps/tauri/commit/168629646335f24cc7f1c4a61df22688b2198f98) ([#13418](https://www.github.com/tauri-apps/tauri/pull/13418) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Put dynamic ACL into a feature `dynamic-acl`, this is currently enabled by default to align with the previous behaviors, you can disable it through `default-features = false` to reduce the final binary size by not including the ACL references + +### Dependencies + +- Upgraded to `tauri-utils@2.5.0` + +## \[2.2.0] + +### Dependencies + +- Upgraded to `tauri-utils@2.4.0` +- [`48b12b440`](https://www.github.com/tauri-apps/tauri/commit/48b12b440478937c46fdfef9f9d95194be117020) Update to `tauri-utils@2.4.0` + +## \[2.1.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.3.1` + +## \[2.1.0] + +### New Features + +- [`013f8f652`](https://www.github.com/tauri-apps/tauri/commit/013f8f652302f2d49c5ec0a075582033d8b074fb) ([#12890](https://www.github.com/tauri-apps/tauri/pull/12890) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Added `build > removeUnusedCommands` to trigger the build scripts and macros to remove unused commands based on the capabilities you defined. Note this won't be accounting for dynamically added ACLs so make sure to check it when using this. + +### Dependencies + +- Upgraded to `tauri-utils@2.3.0` + +## \[2.0.5] + +### Dependencies + +- Upgraded to `tauri-utils@2.2.0` + +## \[2.0.4] + +### Dependencies + +- Upgraded to `tauri-utils@2.1.1` + +## \[2.0.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.1.0` + +## \[2.0.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.2` + +## \[2.0.1] + +### What's Changed + +- [`0ab2b3306`](https://www.github.com/tauri-apps/tauri/commit/0ab2b330644b6419f6cee1d5377bfb5cdda2ccf9) ([#11205](https://www.github.com/tauri-apps/tauri/pull/11205) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.1` + +## \[2.0.0] + +### What's Changed + +- [`382ed482b`](https://www.github.com/tauri-apps/tauri/commit/382ed482bd08157c39e62f9a0aaad8802f1092cb) Bump MSRV to 1.78. +- [`637285790`](https://www.github.com/tauri-apps/tauri/commit/6372857905ae9c0aedb7f482ddf6cf9f9836c9f2) Promote to v2 stable! + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0` + +## \[2.0.0-rc.13] + +### Bug Fixes + +- [`1efa5e718`](https://www.github.com/tauri-apps/tauri/commit/1efa5e7184009537b688a395596c696173ae0231) ([#11099](https://www.github.com/tauri-apps/tauri/pull/11099) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Rerun build script if the platform-specific configuration file changes. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.13` + +## \[2.0.0-rc.12] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.12` + +## \[2.0.0-rc.11] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.11` + +## \[2.0.0-rc.10] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.10` + +## \[2.0.0-rc.9] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.9` + +## \[2.0.0-rc.8] + +### What's Changed + +- [`27d018343`](https://www.github.com/tauri-apps/tauri/commit/27d01834312ee7953b6ccd5b0a368e7a69b225e9) ([#10844](https://www.github.com/tauri-apps/tauri/pull/10844) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Changes how the Info.plist is embedded on macOS development to avoid a clippy warning. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.8` + +## \[2.0.0-rc.7] + +### Bug Fixes + +- [`88bc35732`](https://www.github.com/tauri-apps/tauri/commit/88bc357325ba278527d8cba956e828f5744c8a34) ([#10734](https://www.github.com/tauri-apps/tauri/pull/10734) by [@chippers](https://www.github.com/tauri-apps/tauri/../../chippers)) Generate context in a separate thread to prevent a stack overflow. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.7` + +## \[2.0.0-rc.6] + +### What's Changed + +- [`f4d5241b3`](https://www.github.com/tauri-apps/tauri/commit/f4d5241b377d0f7a1b58100ee19f7843384634ac) ([#10731](https://www.github.com/tauri-apps/tauri/pull/10731) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Update documentation icon path. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.6` + +## \[2.0.0-rc.5] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.5` + +## \[2.0.0-rc.4] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.4` + +## \[2.0.0-rc.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.3` +- [`0afee5ed8`](https://www.github.com/tauri-apps/tauri/commit/0afee5ed80265c95c4581e173c4886677cfed593) ([#10436](https://www.github.com/tauri-apps/tauri/pull/10436) by [@nyurik](https://www.github.com/tauri-apps/tauri/../../nyurik)) Updated brotli to v6. + +## \[2.0.0-rc.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.2` + +## \[2.0.0-rc.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.1` + +## \[2.0.0-rc.0] + +### Enhancements + +- [`1e0793b68`](https://www.github.com/tauri-apps/tauri/commit/1e0793b6821799829e380c88066b3415cc9006df) ([#10357](https://www.github.com/tauri-apps/tauri/pull/10357)) Enhance `AssetResolver::get` in development mode by reading distDir directly as a fallback to the embedded assets. + +### Bug Fixes + +- [`24445d71d`](https://www.github.com/tauri-apps/tauri/commit/24445d71de92d526d0ccaecb54f13003ddc6f6b4)([#10432](https://www.github.com/tauri-apps/tauri/pull/10432)) Fixes asset resolving when not using the `compression` feature. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.0` + +## \[2.0.0-beta.19] + +### Bug Fixes + +- [`5d2922985`](https://www.github.com/tauri-apps/tauri/commit/5d2922985801908e4b929a7a0e387806ff02ab89) ([#10268](https://www.github.com/tauri-apps/tauri/pull/10268) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix icon rewriting always triggering build to rerun due to conflicts between file names. + +### What's Changed + +- [`4c239729c`](https://www.github.com/tauri-apps/tauri/commit/4c239729c3e1b899ecbc6793c3682848e8de1729) ([#10167](https://www.github.com/tauri-apps/tauri/pull/10167) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add support for `test = true` in `generate_context!` macro to skip some code generation that could affect some tests, for now it only skips empedding a plist on macOS. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.19` + +## \[2.0.0-beta.18] + +### New Features + +- [`5b769948a`](https://www.github.com/tauri-apps/tauri/commit/5b769948a81cac333f64c870a470ba6525bd5cd3) ([#9959](https://www.github.com/tauri-apps/tauri/pull/9959)) Add `include_image_codegen` function to help embedding instances of `Image` struct at compile-time in rust to be used with window, menu or tray icons. + +### Bug Fixes + +- [`1f6e478c8`](https://www.github.com/tauri-apps/tauri/commit/1f6e478c842a16219798b9962718e9ddb969c041) ([#9878](https://www.github.com/tauri-apps/tauri/pull/9878)) Fixes Info.plist rewriting always triggering build to rerun. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.18` + +## \[2.0.0-beta.17] + +### What's Changed + +- [`ccc3ea729`](https://www.github.com/tauri-apps/tauri/commit/ccc3ea729de205ef467f737f1feeb5bf02d9cd72)([#9646](https://www.github.com/tauri-apps/tauri/pull/9646)) Use `TAURI_ENV_TARGET_TRIPLE` (which is set by `tauri-build`) to determine the target when reading the config file. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.17` + +## \[2.0.0-beta.16] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.16` + +## \[2.0.0-beta.15] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.15` + +## \[2.0.0-beta.14] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.14` + +## \[2.0.0-beta.13] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.13` + +## \[2.0.0-beta.12] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.12` + +## \[2.0.0-beta.11] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.11` + +## \[2.0.0-beta.10] + +### New Features + +- [`e227fe02f`](https://www.github.com/tauri-apps/tauri/commit/e227fe02f986e145c0731a64693e1c830a9eb5b0)([#9156](https://www.github.com/tauri-apps/tauri/pull/9156)) Allow plugins to define (at compile time) JavaScript that are initialized when `withGlobalTauri` is true. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.10` + +## \[2.0.0-beta.9] + +### New Features + +- [`ba0206d8a`](https://www.github.com/tauri-apps/tauri/commit/ba0206d8a30a9b43ec5090dcaabd1a23baa1420c)([#9141](https://www.github.com/tauri-apps/tauri/pull/9141)) The `Context` codegen now accepts a `assets` input to define a custom `tauri::Assets` implementation. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.9` + +## \[2.0.0-beta.8] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.8` + +### Breaking Changes + +- [`ed48e2b3c`](https://www.github.com/tauri-apps/tauri/commit/ed48e2b3c7fa914e4c9af432c02b8154f872c68a)([#9122](https://www.github.com/tauri-apps/tauri/pull/9122)) Expose `tauri::image` module to export the `JsImage` type and removed the `Image` root re-export. + +## \[2.0.0-beta.7] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.7` + +### Breaking Changes + +- [`d1e77acd8`](https://www.github.com/tauri-apps/tauri/commit/d1e77acd8dfdf554b90b542513a58a2de1ef2360)([#9011](https://www.github.com/tauri-apps/tauri/pull/9011)) Change the generated context code to use the new `Image` type in tauri. + +## \[2.0.0-beta.6] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.6` + +### Breaking Changes + +- [`3657ad82`](https://www.github.com/tauri-apps/tauri/commit/3657ad82f88ce528551d032d521c52eed3f396b4)([#9008](https://www.github.com/tauri-apps/tauri/pull/9008)) Allow defining permissions for the application commands via `tauri_build::Attributes::app_manifest`. + +## \[2.0.0-beta.5] + +### Enhancements + +- [`bc5b5e67`](https://www.github.com/tauri-apps/tauri/commit/bc5b5e671a546512f823f1c157421b4c3311dfc0)([#8984](https://www.github.com/tauri-apps/tauri/pull/8984)) Do not include a CSP tag in the application HTML and rely on the custom protocol response header instead. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.5` + +## \[2.0.0-beta.4] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.4` + +## \[2.0.0-beta.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.3` + +## \[2.0.0-beta.2] + +### Enhancements + +- [`83a68deb`](https://www.github.com/tauri-apps/tauri/commit/83a68deb5676d39cd4728d2e140f6b46d5f787ed)([#8797](https://www.github.com/tauri-apps/tauri/pull/8797)) Added a new configuration option `tauri.conf.json > app > security > capabilities` to reference existing capabilities and inline new ones. If it is empty, all capabilities are still included preserving the current behavior. +- [`8d16a80d`](https://www.github.com/tauri-apps/tauri/commit/8d16a80d2fb2468667e7987d0cc99dbc7e3b9d0a)([#8802](https://www.github.com/tauri-apps/tauri/pull/8802)) The `generate_context` proc macro now accepts a `capabilities` attribute where the value is an array of file paths that can be [conditionally compiled](https://doc.rust-lang.org/reference/conditional-compilation.html). These capabilities are added to the application along the capabilities defined in the Tauri configuration file. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.2` + +## \[2.0.0-beta.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.1` + +## \[2.0.0-beta.0] + +### New Features + +- [`74a2a603`](https://www.github.com/tauri-apps/tauri/commit/74a2a6036a5e57462f161d728cbd8a6f121028ca)([#8661](https://www.github.com/tauri-apps/tauri/pull/8661)) Implement access control list for IPC usage. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.0` + +### Breaking Changes + +- [`8de308d1`](https://www.github.com/tauri-apps/tauri/commit/8de308d1bf6a855d7a26af58bd0e744938ba47d8)([#8723](https://www.github.com/tauri-apps/tauri/pull/8723)) Restructured Tauri config per [RFC#5](https://github.com/tauri-apps/rfcs/blob/f3e82a6b0c5390401e855850d47dc7b7d9afd684/texts/0005-tauri-config-restructure.md): + + - Moved `package.productName`, `package.version` and `tauri.bundle.identifier` fields to the top-level. + - Removed `package` object. + - Renamed `tauri` object to `app`. + - Moved `tauri.bundle` object to the top-level. + - Renamed `build.distDir` field to `frontendDist`. + - Renamed `build.devPath` field to `devUrl` and will no longer accepts paths, it will only accept URLs. + - Moved `tauri.pattern` to `app.security.pattern`. + - Removed `tauri.bundle.updater` object, and its fields have been moved to the updater plugin under `plugins.updater` object. + - Moved `build.withGlobalTauri` to `app.withGlobalTauri`. + - Moved `tauri.bundle.dmg` object to `bundle.macOS.dmg`. + - Moved `tauri.bundle.deb` object to `bundle.linux.deb`. + - Moved `tauri.bundle.appimage` object to `bundle.linux.appimage`. + - Removed all license fields from each bundle configuration object and instead added `bundle.license` and `bundle.licenseFile`. + - Renamed `AppUrl` to `FrontendDist` and refactored its variants to be more explicit. + +## \[2.0.0-alpha.13] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.13` + +## \[2.0.0-alpha.12] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.12` + +## \[2.0.0-alpha.11] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.11` + +## \[2.0.0-alpha.10] + +### Enhancements + +- [`c6c59cf2`](https://www.github.com/tauri-apps/tauri/commit/c6c59cf2373258b626b00a26f4de4331765dd487) Pull changes from Tauri 1.5 release. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.10` + +## \[2.0.0-alpha.9] + +### New Features + +- [`880266a7`](https://www.github.com/tauri-apps/tauri/commit/880266a7f697e1fe58d685de3bb6836ce5251e92)([#8031](https://www.github.com/tauri-apps/tauri/pull/8031)) Bump the MSRV to 1.70. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.9` + +### Breaking Changes + +- [`ebcc21e4`](https://www.github.com/tauri-apps/tauri/commit/ebcc21e4b95f4e8c27639fb1bca545b432f52d5e)([#8057](https://www.github.com/tauri-apps/tauri/pull/8057)) Renamed the beforeDevCommand, beforeBuildCommand and beforeBundleCommand hooks environment variables from `TAURI_PLATFORM, TAURI_ARCH, TAURI_FAMILY, TAURI_PLATFORM_VERSION, TAURI_PLATFORM_TYPE and TAURI_DEBUG` to `TAURI_ENV_PLATFORM, TAURI_ENV_ARCH, TAURI_ENV_FAMILY, TAURI_ENV_PLATFORM_VERSION, TAURI_ENV_PLATFORM_TYPE and TAURI_ENV_DEBUG` to differentiate the prefix with other CLI environment variables. + +## \[2.0.0-alpha.8] + +### Enhancements + +- [`100d9ede`](https://www.github.com/tauri-apps/tauri/commit/100d9ede35995d9db21d2087dd5606adfafb89a5)([#7802](https://www.github.com/tauri-apps/tauri/pull/7802)) Use `Target` enum from `tauri_utils::platform`. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.8` + +## \[2.0.0-alpha.7] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.7` + +## \[2.0.0-alpha.6] + +### Dependencies + +- Updated to latest `tauri-utils` + +## \[2.0.0-alpha.5] + +- [`96639ca2`](https://www.github.com/tauri-apps/tauri/commit/96639ca239c9e4f75142fc07868ac46822111cff)([#6749](https://www.github.com/tauri-apps/tauri/pull/6749)) Moved the `shell` functionality to its own plugin in the plugins-workspace repository. +- [`3188f376`](https://www.github.com/tauri-apps/tauri/commit/3188f3764978c6d1452ee31d5a91469691e95094)([#6883](https://www.github.com/tauri-apps/tauri/pull/6883)) Bump the MSRV to 1.65. +- [`ae102980`](https://www.github.com/tauri-apps/tauri/commit/ae102980fcdde3f55effdc0623ea425b48d07dd1)([#6719](https://www.github.com/tauri-apps/tauri/pull/6719)) Refactor the `Context` conditional fields and only parse the tray icon on desktop. + +## \[2.0.0-alpha.4] + +- Added `android` configuration object under `tauri > bundle`. + - Bumped due to a bump in tauri-utils. + - [db4c9dc6](https://www.github.com/tauri-apps/tauri/commit/db4c9dc655e07ee2184fe04571f500f7910890cd) feat(core): add option to configure Android's minimum SDK version ([#6651](https://www.github.com/tauri-apps/tauri/pull/6651)) on 2023-04-07 + +## \[2.0.0-alpha.3] + +- Pull changes from Tauri 1.3 release. + - [](https://www.github.com/tauri-apps/tauri/commit/undefined) on undefined + +## \[2.0.0-alpha.2] + +- Return `bool` in the invoke handler. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 + +## \[2.0.0-alpha.1] + +- Bump the MSRV to 1.64. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Added `crate_name` field on `PackageInfo`. + - [630a7f4b](https://www.github.com/tauri-apps/tauri/commit/630a7f4b18cef169bfd48673609306fec434e397) refactor: remove mobile log initialization, ref [#6049](https://www.github.com/tauri-apps/tauri/pull/6049) ([#6081](https://www.github.com/tauri-apps/tauri/pull/6081)) on 2023-01-17 + +## \[2.0.0-alpha.0] + +- Change `devPath` URL to use the local IP address on iOS and Android. + - [6f061504](https://www.github.com/tauri-apps/tauri/commit/6f0615044d09ec58393a7ebca5e45bb175e20db3) feat(cli): add `android dev` and `ios dev` commands ([#4982](https://www.github.com/tauri-apps/tauri/pull/4982)) on 2022-08-20 +- First mobile alpha release! + - [fa3a1098](https://www.github.com/tauri-apps/tauri/commit/fa3a10988a03aed1b66fb17d893b1a9adb90f7cd) feat(ci): prepare 2.0.0-alpha.0 ([#5786](https://www.github.com/tauri-apps/tauri/pull/5786)) on 2022-12-08 + +## \[1.4.2] + +### Dependencies + +- Upgraded to `tauri-utils@1.5.2` + +## \[1.4.1] + +### Dependencies + +- Upgraded to `tauri-utils@1.5.0` + +## \[1.4.0] + +### Enhancements + +- [`17d5a4f5`](https://www.github.com/tauri-apps/tauri/commit/17d5a4f51f244d3ff42014b5d1b075fad7c636a5)([#6706](https://www.github.com/tauri-apps/tauri/pull/6706)) Early panic if the PNG icon is not RGBA. +- [`d2710e9d`](https://www.github.com/tauri-apps/tauri/commit/d2710e9d2e8fd93975ef6494512370faa8cb3b7e)([#6944](https://www.github.com/tauri-apps/tauri/pull/6944)) Unpin `time`, `ignore`, and `winnow` crate versions. Developers now have to pin crates if needed themselves. A list of crates that need pinning to adhere to Tauri's MSRV will be visible in Tauri's GitHub workflow: https://github.com/tauri-apps/tauri/blob/dev/.github/workflows/test-core.yml#L85. + +## \[1.3.0] + +- Bump minimum supported Rust version to 1.60. + - [5fdc616d](https://www.github.com/tauri-apps/tauri/commit/5fdc616df9bea633810dcb814ac615911d77222c) feat: Use the zbus-backed of notify-rust ([#6332](https://www.github.com/tauri-apps/tauri/pull/6332)) on 2023-03-31 +- Pin `time` to `0.3.15`. + - [3d16461b](https://www.github.com/tauri-apps/tauri/commit/3d16461b68583ba7db037fbc217786e79b46ddf2) fix(core): pin time to 0.3.15 ([#6312](https://www.github.com/tauri-apps/tauri/pull/6312)) on 2023-02-19 + +## \[1.2.1] + +- Fix `allowlist > app > show/hide` always disabled when `allowlist > app > all: false`. + - Bumped due to a bump in tauri-utils. + - [bb251087](https://www.github.com/tauri-apps/tauri/commit/bb2510876d0bdff736d36bf3a465cdbe4ad2b90c) fix(core): extend allowlist with `app`'s allowlist, closes [#5650](https://www.github.com/tauri-apps/tauri/pull/5650) ([#5652](https://www.github.com/tauri-apps/tauri/pull/5652)) on 2022-11-18 + +## \[1.2.0] + +- Properly serialize HTML template tags. + - [aec5537d](https://www.github.com/tauri-apps/tauri/commit/aec5537de0205f62b2ae5c89da04d21930a6fc2e) fix(codegen): serialize template tags, closes [#4410](https://www.github.com/tauri-apps/tauri/pull/4410) ([#5247](https://www.github.com/tauri-apps/tauri/pull/5247)) on 2022-09-28 +- - [7d9aa398](https://www.github.com/tauri-apps/tauri/commit/7d9aa3987efce2d697179ffc33646d086c68030c) feat: bump MSRV to 1.59 ([#5296](https://www.github.com/tauri-apps/tauri/pull/5296)) on 2022-09-28 + +## \[1.1.1] + +- Add missing allowlist config for `set_cursor_grab`, `set_cursor_visible`, `set_cursor_icon` and `set_cursor_position` APIs. + - Bumped due to a bump in tauri-utils. + - [c764408d](https://www.github.com/tauri-apps/tauri/commit/c764408da7fae123edd41115bda42fa75a4731d2) fix: Add missing allowlist config for cursor apis, closes [#5207](https://www.github.com/tauri-apps/tauri/pull/5207) ([#5211](https://www.github.com/tauri-apps/tauri/pull/5211)) on 2022-09-16 + +## \[1.1.0] + +- Use `TARGET` environment variable for code generation inside build scripts. + - [6ba99689](https://www.github.com/tauri-apps/tauri/commit/6ba99689aa7ed0ffa9072a1c8ab5db12c9bf95af) feat(codegen): use TARGET environment variable if set ([#4921](https://www.github.com/tauri-apps/tauri/pull/4921)) on 2022-08-12 +- Added support to configuration files in TOML format (Tauri.toml file). + - [ae83d008](https://www.github.com/tauri-apps/tauri/commit/ae83d008f9e1b89bfc8dddaca42aa5c1fbc36f6d) feat: add support to TOML config file `Tauri.toml`, closes [#4806](https://www.github.com/tauri-apps/tauri/pull/4806) ([#4813](https://www.github.com/tauri-apps/tauri/pull/4813)) on 2022-08-02 +- Improve tray icon read error message. + - [52f0c8bb](https://www.github.com/tauri-apps/tauri/commit/52f0c8bb836c6d50b7ce2393161394f4ce78f5ae) feat(core): improve tray icon read error messages ([#4850](https://www.github.com/tauri-apps/tauri/pull/4850)) on 2022-08-03 +- Fix relative paths in `version` field of `tauri.config.json` not being correctly parsed by `generate_context!()`. + - [accbc5e8](https://www.github.com/tauri-apps/tauri/commit/accbc5e8806a32efc555d019634fc2fa14d17f0a) fix(codegen): fix relative paths in `version` field of `tauri.config.json`, closes [#4723](https://www.github.com/tauri-apps/tauri/pull/4723) ([#4725](https://www.github.com/tauri-apps/tauri/pull/4725)) on 2022-07-24 +- Only rewrite temporary icon files when the content change, avoid needless rebuilds. + - [f957cbb5](https://www.github.com/tauri-apps/tauri/commit/f957cbb56ccbd8d1c047a978b4579946252a6fd2) fix(codegen): write output file when contents change ([#4889](https://www.github.com/tauri-apps/tauri/pull/4889)) on 2022-08-09 + +## \[1.0.4] + +- Validate `__TAURI_ISOLATION_HOOK__` being set by a file in the isolation application. + - [3b4ed970](https://www.github.com/tauri-apps/tauri/commit/3b4ed970e663f5bffbfe0358610f9c3f157c513f) feat(codegen): validate `__TAURI_ISOLATION_HOOK__` is referenced ([#4631](https://www.github.com/tauri-apps/tauri/pull/4631)) on 2022-07-11 + +## \[1.0.3] + +- The `TAURI_CONFIG` environment variable now represents the configuration to be merged instead of the entire JSON. + - [fa028ebf](https://www.github.com/tauri-apps/tauri/commit/fa028ebf3c8ca7b43a70d283a01dbea86217594f) refactor: do not pass entire config from CLI to core, send patch instead ([#4598](https://www.github.com/tauri-apps/tauri/pull/4598)) on 2022-07-06 + +## \[1.0.2] + +- Expose `platform::windows_version` function. + - Bumped due to a bump in tauri-utils. + - [bf764e83](https://www.github.com/tauri-apps/tauri/commit/bf764e83e01e7443e6cc54572001e1c98c357465) feat(utils): expose `windows_version` function ([#4534](https://www.github.com/tauri-apps/tauri/pull/4534)) on 2022-06-30 + +## \[1.0.1] + +- Set the bundle name and app metadata in the Info.plist file in development mode. + - [38f5db6e](https://www.github.com/tauri-apps/tauri/commit/38f5db6e6a8b496b50d486db6fd8204266de3a69) feat(codegen): fill app metadata in development Info.plist on 2022-06-21 +- Set the application icon in development mode on macOS. + - [307c2ebf](https://www.github.com/tauri-apps/tauri/commit/307c2ebfb68238dacab6088f9c6ba310c727c68c) feat(core): set macOS app icon in development ([#4385](https://www.github.com/tauri-apps/tauri/pull/4385)) on 2022-06-19 + +## \[1.0.0] + +- Upgrade to `stable`! + - [f4bb30cc](https://www.github.com/tauri-apps/tauri/commit/f4bb30cc73d6ba9b9ef19ef004dc5e8e6bb901d3) feat(covector): prepare for v1 ([#4351](https://www.github.com/tauri-apps/tauri/pull/4351)) on 2022-06-15 + +## \[1.0.0-rc.11] + +- Read the tray icon path relatively to the config directory. + - [562e8ca2](https://www.github.com/tauri-apps/tauri/commit/562e8ca23facf1a8e5fa6c8cdf872357d3523a78) fix(codegen): tray icon path is relative to the config directory on 2022-06-15 + +## \[1.0.0-rc.10] + +- **Breaking change:** The `TrayIcon` enum has been removed and now `Icon` is used instead. + This allows you to use more image formats and use embedded icons on Linux. + - [4ce8e228](https://www.github.com/tauri-apps/tauri/commit/4ce8e228134cd3f22973b74ef26ca0d165fbbbd9) refactor(core): use `Icon` for tray icons ([#4342](https://www.github.com/tauri-apps/tauri/pull/4342)) on 2022-06-14 + +## \[1.0.0-rc.9] + +- Added a config flag to bundle the media framework used by webkit2gtk `tauri.conf.json > tauri > bundle > appimage > bundleMediaFramework`. + - Bumped due to a bump in tauri-utils. + - [d335fae9](https://www.github.com/tauri-apps/tauri/commit/d335fae92cdcbb0ee18aad4e54558914afa3e778) feat(bundler): bundle additional gstreamer files, closes [#4092](https://www.github.com/tauri-apps/tauri/pull/4092) ([#4271](https://www.github.com/tauri-apps/tauri/pull/4271)) on 2022-06-10 + +## \[1.0.0-rc.8] + +- **Breaking change:** `PackageInfo::version` is now a `semver::Version` instead of a `String`. + - [2badbd2d](https://www.github.com/tauri-apps/tauri/commit/2badbd2d7ed51bf33c1b547b4c837b600574bd4a) refactor: force semver versions, change updater `should_install` sig ([#4215](https://www.github.com/tauri-apps/tauri/pull/4215)) on 2022-05-25 + - [a7388e23](https://www.github.com/tauri-apps/tauri/commit/a7388e23c3b9019d48b078cae00a75c74d74d11b) fix(ci): adjust change file to include tauri-utils and tauri-codegen on 2022-05-27 + +## \[1.0.0-rc.7] + +- Allow configuring the display options for the MSI execution allowing quieter updates. + - Bumped due to a bump in tauri-utils. + - [9f2c3413](https://www.github.com/tauri-apps/tauri/commit/9f2c34131952ea83c3f8e383bc3cec7e1450429f) feat(core): configure msiexec display options, closes [#3951](https://www.github.com/tauri-apps/tauri/pull/3951) ([#4061](https://www.github.com/tauri-apps/tauri/pull/4061)) on 2022-05-15 + +## \[1.0.0-rc.6] + +- The `dangerous_allow_asset_csp_modification` configuration value has been changed to allow a list of CSP directives to disable. + - [164078c0](https://www.github.com/tauri-apps/tauri/commit/164078c0b719ccbc12e956fecf8a7d4a3c5044e1) feat: allow limiting dangerousDisableAssetCspModification, closes [#3831](https://www.github.com/tauri-apps/tauri/pull/3831) ([#4021](https://www.github.com/tauri-apps/tauri/pull/4021)) on 2022-05-02 + +## \[1.0.0-rc.5] + +- Read platform-specific configuration files when generating code without the `TAURI_CONFIG` env var. + - [edf85bc1](https://www.github.com/tauri-apps/tauri/commit/edf85bc1d18450c92aee17f7f99c163abe432ebd) fix(codegen): read platform-specific config file ([#3966](https://www.github.com/tauri-apps/tauri/pull/3966)) on 2022-04-25 + +## \[1.0.0-rc.4] + +- Added an option to disable the CSP injection of distributable assets nonces and hashes. + - [f6e32ee1](https://www.github.com/tauri-apps/tauri/commit/f6e32ee1880eb364ed76beb937c9d12e14d54910) feat(core): add dangerous option to disable compile time CSP injection ([#3775](https://www.github.com/tauri-apps/tauri/pull/3775)) on 2022-03-28 + +- Replace multiple dependencies who's C code compiled concurrently and caused + the other ones to bloat compile time significantly. + +- `zstd` -> `brotli` + +- `blake3` -> a vendored version of the blake3 reference + +- `ring` -> `getrandom` + +See https://github.com/tauri-apps/tauri/pull/3773 for more information about +these specific choices. + +- [8661e3e2](https://www.github.com/tauri-apps/tauri/commit/8661e3e24d96c399bfbcdee5d8e9d6beba2265a7) replace dependencies with long build times when used together (closes [#3571](https://www.github.com/tauri-apps/tauri/pull/3571)) ([#3773](https://www.github.com/tauri-apps/tauri/pull/3773)) on 2022-03-27 + +## \[1.0.0-rc.3] + +- Parse window icons at compile time. + - [8c935872](https://www.github.com/tauri-apps/tauri/commit/8c9358725a17dcc2acaf4d10c3f654afdff586b0) refactor(core): move `png` and `ico` behind Cargo features ([#3588](https://www.github.com/tauri-apps/tauri/pull/3588)) on 2022-03-05 + +## \[1.0.0-rc.2] + +- Changed the default value for `tauri > bundle > macOS > minimumSystemVersion` to `10.13`. + - Bumped due to a bump in tauri-utils. + - [fce344b9](https://www.github.com/tauri-apps/tauri/commit/fce344b90b7227f8f5514853c2f885fb24d3648e) feat(core): set default value for `minimum_system_version` to 10.13 ([#3497](https://www.github.com/tauri-apps/tauri/pull/3497)) on 2022-02-17 + +## \[1.0.0-rc.1] + +- Change default value for the `freezePrototype` configuration to `false`. + - Bumped due to a bump in tauri-utils. + - [3a4c0160](https://www.github.com/tauri-apps/tauri/commit/3a4c01606184be762adee055ddac803de0d28527) fix(core): change default `freezePrototype` to false, closes [#3416](https://www.github.com/tauri-apps/tauri/pull/3416) [#3406](https://www.github.com/tauri-apps/tauri/pull/3406) ([#3423](https://www.github.com/tauri-apps/tauri/pull/3423)) on 2022-02-12 + +## \[1.0.0-rc.0] + +- Apply `nonce` to `script` and `style` tags and set them on the `CSP` (`script-src` and `style-src` fetch directives). + - [cf54dcf9](https://www.github.com/tauri-apps/tauri/commit/cf54dcf9c81730e42c9171daa9c8aa474c95b522) feat: improve `CSP` security with nonces and hashes, add `devCsp` \[TRI-004] ([#8](https://www.github.com/tauri-apps/tauri/pull/8)) on 2022-01-09 +- Added the `isolation` pattern. + - [d5d6d2ab](https://www.github.com/tauri-apps/tauri/commit/d5d6d2abc17cd89c3a079d2ce01581193469dbc0) Isolation Pattern ([#43](https://www.github.com/tauri-apps/tauri/pull/43)) Co-authored-by: Ngo Iok Ui (Wu Yu Wei) Co-authored-by: Lucas Fernandes Nogueira on 2022-01-17 +- Adds support for using JSON5 format for the `tauri.conf.json` file, along with also supporting the `.json5` extension. + +Here is the logic flow that determines if JSON or JSON5 will be used to parse the config: + +1. Check if `tauri.conf.json` exists + a. Parse it with `serde_json` + b. Parse it with `json5` if `serde_json` fails + c. Return original `serde_json` error if all above steps failed +2. Check if `tauri.conf.json5` exists + a. Parse it with `json5` + b. Return error if all above steps failed +3. Return error if all above steps failed + +- [995de57a](https://www.github.com/tauri-apps/tauri/commit/995de57a76cf51215277673e526d7ec32b86b564) Add seamless support for using JSON5 in the config file ([#47](https://www.github.com/tauri-apps/tauri/pull/47)) on 2022-02-03 +- The minimum Rust version is now `1.56`. + - [a9dfc015](https://www.github.com/tauri-apps/tauri/commit/a9dfc015505afe91281c2027954ffcc588b1a59c) feat: update to edition 2021 and set minimum rust to 1.56 ([#2789](https://www.github.com/tauri-apps/tauri/pull/2789)) on 2021-10-22 + +## \[1.0.0-beta.4] + +- Embed Info.plist file contents on binary on dev. + - [537ab1b6](https://www.github.com/tauri-apps/tauri/commit/537ab1b6d5a792c550a535619965c9e4126292e6) feat(core): inject src-tauri/Info.plist file on dev and merge on bundle, closes [#1570](https://www.github.com/tauri-apps/tauri/pull/1570) [#2338](https://www.github.com/tauri-apps/tauri/pull/2338) ([#2444](https://www.github.com/tauri-apps/tauri/pull/2444)) on 2021-08-15 +- Fix ES Module detection for default imports with relative paths or scoped packages and exporting of async functions. + - [b2b36cfe](https://www.github.com/tauri-apps/tauri/commit/b2b36cfe8dfcccb341638a4cb6dc23a514c54148) fix(core): fixes ES Module detection for default imports with relative paths or scoped packages ([#2380](https://www.github.com/tauri-apps/tauri/pull/2380)) on 2021-08-10 + - [fbf8caf5](https://www.github.com/tauri-apps/tauri/commit/fbf8caf5c419cb4fc3d123be910e094a8e8c4bef) fix(core): ESM detection when using `export async function` ([#2425](https://www.github.com/tauri-apps/tauri/pull/2425)) on 2021-08-14 + +## \[1.0.0-beta.3] + +- Improve ESM detection with regexes. + - [4b0ec018](https://www.github.com/tauri-apps/tauri/commit/4b0ec0188078a8fefd4119fe5e19ebc30191f802) fix(core): improve JS ESM detection ([#2139](https://www.github.com/tauri-apps/tauri/pull/2139)) on 2021-07-02 +- Inject invoke key on `script` tags with `type="module"`. + - [f03eea9c](https://www.github.com/tauri-apps/tauri/commit/f03eea9c9b964709532afbc4d1dd343b3fd96081) feat(core): inject invoke key on ``). Maps a HTML path to a list of hashes. + pub(crate) inline_scripts: BTreeMap>, + /// A list of hashes of the contents of all `style` elements. + pub(crate) styles: Vec, +} + +impl CspHashes { + /// Only add a CSP hash to the appropriate category if we think the file matches + /// + /// Note: this only checks the file extension, much like how a browser will assume a .js file is + /// a JavaScript file unless HTTP headers tell it otherwise. + pub fn add_if_applicable( + &mut self, + entry: &DirEntry, + dangerous_disable_asset_csp_modification: &DisabledCspModificationKind, + ) -> Result<(), EmbeddedAssetsError> { + let path = entry.path(); + + // we only hash JavaScript files for now, may expand to other CSP hashable types in the future + if let Some("js") | Some("mjs") = path.extension().and_then(|os| os.to_str()) { + if dangerous_disable_asset_csp_modification.can_modify("script-src") { + let mut hasher = Sha256::new(); + hasher.update( + &std::fs::read(path) + .map(|b| tauri_utils::html2::normalize_script_for_csp(&b)) + .map_err(|error| EmbeddedAssetsError::AssetRead { + path: path.to_path_buf(), + error, + })?, + ); + let hash = hasher.finalize(); + self.scripts.push(format!( + "'sha256-{}'", + base64::engine::general_purpose::STANDARD.encode(hash) + )); + } + } + + Ok(()) + } +} + +/// Options used to embed assets. +#[derive(Default)] +pub struct AssetOptions { + pub(crate) csp: bool, + pub(crate) pattern: PatternKind, + pub(crate) freeze_prototype: bool, + pub(crate) dangerous_disable_asset_csp_modification: DisabledCspModificationKind, + #[cfg(feature = "isolation")] + pub(crate) isolation_schema: String, +} + +impl AssetOptions { + /// Creates the default asset options. + pub fn new(pattern: PatternKind) -> Self { + Self { + csp: false, + pattern, + freeze_prototype: false, + dangerous_disable_asset_csp_modification: DisabledCspModificationKind::Flag(false), + #[cfg(feature = "isolation")] + isolation_schema: format!("isolation-{}", uuid::Uuid::new_v4()), + } + } + + /// Instruct the asset handler to inject the CSP token to HTML files (Linux only) and add asset nonces and hashes to the policy. + #[must_use] + pub fn with_csp(mut self) -> Self { + self.csp = true; + self + } + + /// Instruct the asset handler to include a script to freeze the `Object.prototype` on all HTML files. + #[must_use] + pub fn freeze_prototype(mut self, freeze: bool) -> Self { + self.freeze_prototype = freeze; + self + } + + /// Instruct the asset handler to **NOT** modify the CSP. This is **NOT** recommended. + pub fn dangerous_disable_asset_csp_modification( + mut self, + dangerous_disable_asset_csp_modification: DisabledCspModificationKind, + ) -> Self { + self.dangerous_disable_asset_csp_modification = dangerous_disable_asset_csp_modification; + self + } +} + +impl EmbeddedAssets { + /// Compress a collection of files and directories, ready to be generated into [`Assets`]. + /// + /// [`Assets`]: tauri_utils::assets::Assets + pub fn new( + input: impl Into, + options: &AssetOptions, + mut map: impl FnMut( + &AssetKey, + &Path, + &mut Vec, + &mut CspHashes, + ) -> Result<(), EmbeddedAssetsError>, + ) -> Result { + // we need to pre-compute all files now, so that we can inject data from all files into a few + let RawEmbeddedAssets { paths, csp_hashes } = RawEmbeddedAssets::new(input.into(), options)?; + + struct CompressState { + csp_hashes: CspHashes, + assets: BTreeMap, + } + + let CompressState { assets, csp_hashes } = paths.into_iter().try_fold( + CompressState { + csp_hashes, + assets: BTreeMap::new(), + }, + move |mut state, (prefix, entry)| { + let (key, asset) = + Self::compress_file(&prefix, entry.path(), &mut map, &mut state.csp_hashes)?; + state.assets.insert(key, asset); + Result::<_, EmbeddedAssetsError>::Ok(state) + }, + )?; + + Ok(Self { assets, csp_hashes }) + } + + /// Use highest compression level for release, the fastest one for everything else + #[cfg(feature = "compression")] + fn compression_settings() -> BrotliEncoderParams { + let mut settings = BrotliEncoderParams::default(); + + // the following compression levels are hand-picked and are not min-maxed. + // they have a good balance of runtime vs size for the respective profile goals. + // see the "brotli" section of this comment https://github.com/tauri-apps/tauri/issues/3571#issuecomment-1054847558 + if cfg!(debug_assertions) { + settings.quality = 2 + } else { + settings.quality = 9 + } + + settings + } + + /// Compress a file and spit out the information in a [`BTreeMap`] friendly form. + fn compress_file( + prefix: &Path, + path: &Path, + map: &mut impl FnMut( + &AssetKey, + &Path, + &mut Vec, + &mut CspHashes, + ) -> Result<(), EmbeddedAssetsError>, + csp_hashes: &mut CspHashes, + ) -> Result { + let mut input = std::fs::read(path).map_err(|error| EmbeddedAssetsError::AssetRead { + path: path.to_owned(), + error, + })?; + + // get a key to the asset path without the asset directory prefix + let key = path + .strip_prefix(prefix) + .map(AssetKey::from) // format the path for use in assets + .map_err(|_| EmbeddedAssetsError::PrefixInvalid { + prefix: prefix.to_owned(), + path: path.to_owned(), + })?; + + // perform any caller-requested input manipulation + map(&key, path, &mut input, csp_hashes)?; + + // we must canonicalize the base of our paths to allow long paths on windows + let out_dir = std::env::var("OUT_DIR") + .map_err(|_| EmbeddedAssetsError::OutDir) + .map(PathBuf::from) + .and_then(|p| p.canonicalize().map_err(|_| EmbeddedAssetsError::OutDir)) + .map(|p| p.join(TARGET_PATH))?; + + // make sure that our output directory is created + std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?; + + // get a hash of the input - allows for caching existing files + let hash = crate::checksum(&input).map_err(EmbeddedAssetsError::Hex)?; + + // use the content hash to determine filename, keep extensions that exist + let out_path = if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + out_dir.join(format!("{hash}.{ext}")) + } else { + out_dir.join(hash) + }; + + // only compress and write to the file if it doesn't already exist. + if !out_path.exists() { + #[allow(unused_mut)] + let mut out_file = + File::create(&out_path).map_err(|error| EmbeddedAssetsError::AssetWrite { + path: out_path.clone(), + error, + })?; + + #[cfg(not(feature = "compression"))] + { + use std::io::Write; + out_file + .write_all(&input) + .map_err(|error| EmbeddedAssetsError::AssetWrite { + path: path.to_owned(), + error, + })?; + } + + #[cfg(feature = "compression")] + { + let mut input = std::io::Cursor::new(input); + // entirely write input to the output file path with compression + brotli::BrotliCompress(&mut input, &mut out_file, &Self::compression_settings()).map_err( + |error| EmbeddedAssetsError::AssetWrite { + path: path.to_owned(), + error, + }, + )?; + } + } + + Ok((key, (path.into(), out_path))) + } +} + +impl ToTokens for EmbeddedAssets { + fn to_tokens(&self, tokens: &mut TokenStream) { + let mut assets = TokenStream::new(); + for (key, (input, output)) in &self.assets { + let key: &str = key.as_ref(); + let input = input.display().to_string(); + let output = output.display().to_string(); + + // add original asset as a compiler dependency, rely on dead code elimination to clean it up + assets.append_all(quote!(#key => { + const _: &[u8] = include_bytes!(#input); + include_bytes!(#output) + },)); + } + + let mut global_hashes = TokenStream::new(); + // Sort the hashes so the generated code does not depend on the filesystem + // walk order the assets were collected in, which varies across machines + let mut script_hashes: Vec<_> = self.csp_hashes.scripts.iter().collect(); + script_hashes.sort(); + for script_hash in script_hashes { + let hash = script_hash.as_str(); + global_hashes.append_all(quote!(CspHash::Script(#hash),)); + } + + let mut style_hashes: Vec<_> = self.csp_hashes.styles.iter().collect(); + style_hashes.sort(); + for style_hash in style_hashes { + let hash = style_hash.as_str(); + global_hashes.append_all(quote!(CspHash::Style(#hash),)); + } + + let mut html_hashes = TokenStream::new(); + for (path, hashes) in &self.csp_hashes.inline_scripts { + let key = path.as_str(); + let mut value = TokenStream::new(); + for script_hash in hashes { + let hash = script_hash.as_str(); + value.append_all(quote!(CspHash::Script(#hash),)); + } + html_hashes.append_all(quote!(#key => &[#value],)); + } + + // we expect phf related items to be in path when generating the path code + tokens.append_all(quote! {{ + #[allow(unused_imports)] + use ::tauri::utils::assets::{CspHash, EmbeddedAssets, phf, phf::phf_map}; + EmbeddedAssets::new(phf_map! { #assets }, &[#global_hashes], phf_map! { #html_hashes }) + }}); + } +} + +pub(crate) fn ensure_out_dir() -> EmbeddedAssetsResult { + let out_dir = std::env::var("OUT_DIR") + .map_err(|_| EmbeddedAssetsError::OutDir) + .map(PathBuf::from) + .and_then(|p| p.canonicalize().map_err(|_| EmbeddedAssetsError::OutDir))?; + + // make sure that our output directory is created + std::fs::create_dir_all(&out_dir).map_err(|_| EmbeddedAssetsError::OutDir)?; + Ok(out_dir) +} diff --git a/crates/tauri-codegen/src/image.rs b/crates/tauri-codegen/src/image.rs new file mode 100644 index 0000000..92a6e60 --- /dev/null +++ b/crates/tauri-codegen/src/image.rs @@ -0,0 +1,208 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + Cached, + embedded_assets::{EmbeddedAssetsError, EmbeddedAssetsResult}, +}; +use proc_macro2::TokenStream; +use quote::{ToTokens, TokenStreamExt, quote}; +use std::{ffi::OsStr, io::Cursor, path::Path}; + +/// The format the Icon is consumed as. +pub(crate) enum IconFormat { + /// The image, completely unmodified. + Raw, + + /// RGBA raw data, meant to be consumed by [`tauri::image::Image`]. + Image { width: u32, height: u32 }, +} + +pub struct CachedIcon { + cache: Cached, + format: IconFormat, + root: TokenStream, +} + +impl CachedIcon { + pub fn new(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult { + match icon.extension().map(OsStr::to_string_lossy).as_deref() { + Some("png") => Self::new_png(root, icon), + Some("ico") => Self::new_ico(root, icon), + unknown => Err(EmbeddedAssetsError::InvalidImageExtension { + extension: unknown.unwrap_or_default().into(), + path: icon.to_path_buf(), + }), + } + } + + /// Cache the icon without any manipulation. + pub fn new_raw(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult { + let buf = Self::open(icon); + Cached::try_from(buf).map(|cache| Self { + cache, + root: root.clone(), + format: IconFormat::Raw, + }) + } + + /// Cache an ICO icon as RGBA data, see [`ImageFormat::Image`]. + pub fn new_ico(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult { + let buf = Self::open(icon); + + let icon_dir = ico::IconDir::read(Cursor::new(&buf)) + .unwrap_or_else(|e| panic!("failed to parse icon {}: {}", icon.display(), e)); + + let entry = largest_ico_entry(&icon_dir) + .unwrap_or_else(|| panic!("icon {} has no entries", icon.display())); + let rgba = entry + .decode() + .unwrap_or_else(|e| panic!("failed to decode icon {}: {}", icon.display(), e)) + .rgba_data() + .to_vec(); + + Cached::try_from(rgba).map(|cache| Self { + cache, + root: root.clone(), + format: IconFormat::Image { + width: entry.width(), + height: entry.height(), + }, + }) + } + + /// Cache a PNG icon as RGBA data, see [`ImageFormat::Image`]. + pub fn new_png(root: &TokenStream, icon: &Path) -> EmbeddedAssetsResult { + let buf = Self::open(icon); + let decoder = png::Decoder::new(Cursor::new(&buf)); + let mut reader = decoder + .read_info() + .unwrap_or_else(|e| panic!("failed to read icon {}: {}", icon.display(), e)); + + if reader.output_color_type().0 != png::ColorType::Rgba { + panic!("icon {} is not RGBA", icon.display()); + } + + let mut rgba = Vec::with_capacity(reader.output_buffer_size().unwrap()); + while let Ok(Some(row)) = reader.next_row() { + rgba.extend(row.data()); + } + + Cached::try_from(rgba).map(|cache| Self { + cache, + root: root.clone(), + format: IconFormat::Image { + width: reader.info().width, + height: reader.info().height, + }, + }) + } + + fn open(path: &Path) -> Vec { + std::fs::read(path).unwrap_or_else(|e| panic!("failed to open icon {}: {}", path.display(), e)) + } +} + +/// Picks the entry of an ICO file to embed: the largest one (and, for equal sizes, the deepest). +/// +/// ICO files conventionally store entries smallest-first, so taking the first entry would give +/// the OS a 16x16 image to upscale; the largest entry lets it downscale a high-resolution source instead. +fn largest_ico_entry(icon_dir: &ico::IconDir) -> Option<&ico::IconDirEntry> { + icon_dir + .entries() + .iter() + .max_by_key(|e| (e.width() * e.height(), e.bits_per_pixel())) +} + +impl ToTokens for CachedIcon { + fn to_tokens(&self, tokens: &mut TokenStream) { + let root = &self.root; + let cache = &self.cache; + let raw = quote!(::std::include_bytes!(#cache)); + tokens.append_all(match self.format { + IconFormat::Raw => raw, + IconFormat::Image { width, height } => { + quote!(#root::image::Image::new(#raw, #width, #height)) + } + }) + } +} + +#[cfg(test)] +mod tests { + use super::largest_ico_entry; + use ico::{IconDir, IconDirEntry, IconImage, ResourceType}; + use std::io::Cursor; + + fn image(size: u32, alpha: u8) -> IconImage { + let rgba = [255, 0, 0, alpha].repeat((size * size) as usize); + IconImage::from_rgba_data(size, size, rgba) + } + + /// Builds an ICO with an opaque entry for each size, in the given order, and reads it back + /// through the same parser `CachedIcon::new_ico` uses. + fn icon_dir(sizes: &[u32]) -> IconDir { + let mut dir = IconDir::new(ResourceType::Icon); + for &size in sizes { + let entry = if size == 256 { + IconDirEntry::encode_as_png(&image(size, 255)).unwrap() + } else { + IconDirEntry::encode_as_bmp(&image(size, 255)).unwrap() + }; + dir.add_entry(entry); + } + let mut buf = Vec::new(); + dir.write(&mut buf).unwrap(); + IconDir::read(Cursor::new(buf)).unwrap() + } + + #[test] + fn picks_largest_entry_regardless_of_order() { + // the entry order `tauri icon` used to generate, with the PNG-compressed 256 entry last + let dir = icon_dir(&[32, 16, 24, 48, 64, 256]); + let entry = largest_ico_entry(&dir).unwrap(); + assert_eq!((entry.width(), entry.height()), (256, 256)); + assert!(entry.is_png()); + + // largest first, in the middle and alone + for sizes in [&[64, 48, 16][..], &[16, 64, 48], &[64]] { + let dir = icon_dir(sizes); + let entry = largest_ico_entry(&dir).unwrap(); + assert_eq!((entry.width(), entry.height()), (64, 64), "{sizes:?}"); + } + } + + #[test] + fn picks_deepest_entry_for_equal_sizes() { + // a single opaque color encodes as a low-depth BMP, a translucent one needs 32bpp + let shallow = IconDirEntry::encode_as_bmp(&image(32, 255)).unwrap(); + let deep = IconDirEntry::encode_as_bmp(&image(32, 128)).unwrap(); + assert!(shallow.bits_per_pixel() < deep.bits_per_pixel()); + let deep_bpp = deep.bits_per_pixel(); + + for entries in [[shallow.clone(), deep.clone()], [deep, shallow]] { + let mut dir = IconDir::new(ResourceType::Icon); + for entry in entries { + dir.add_entry(entry); + } + let entry = largest_ico_entry(&dir).unwrap(); + assert_eq!((entry.width(), entry.height()), (32, 32)); + assert_eq!(entry.bits_per_pixel(), deep_bpp); + } + } + + #[test] + fn selected_entry_decodes_at_its_own_size() { + let dir = icon_dir(&[16, 256]); + let entry = largest_ico_entry(&dir).unwrap(); + let decoded = entry.decode().unwrap(); + assert_eq!((decoded.width(), decoded.height()), (256, 256)); + assert_eq!(decoded.rgba_data().len(), 256 * 256 * 4); + } + + #[test] + fn no_entries() { + assert!(largest_ico_entry(&IconDir::new(ResourceType::Icon)).is_none()); + } +} diff --git a/crates/tauri-codegen/src/lib.rs b/crates/tauri-codegen/src/lib.rs new file mode 100644 index 0000000..3204f68 --- /dev/null +++ b/crates/tauri-codegen/src/lib.rs @@ -0,0 +1,152 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! - Embed, hash, and compress assets, including icons for the app as well as the tray icon. +//! - Parse `tauri.conf.json` at compile time and generate the Config struct. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" +)] + +pub use self::context::{ContextData, context_codegen}; +use crate::embedded_assets::{EmbeddedAssetsError, ensure_out_dir}; +use proc_macro2::TokenStream; +use quote::{ToTokens, TokenStreamExt, quote}; +use std::{ + borrow::Cow, + fmt::{self, Write}, + path::{Path, PathBuf}, +}; +pub use tauri_utils::config::{Config, parse::ConfigError}; +use tauri_utils::platform::Target; +use tauri_utils::write_if_changed; + +mod context; +pub mod embedded_assets; +pub mod image; +#[doc(hidden)] +pub mod vendor; + +/// Represents all the errors that can happen while reading the config during codegen. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum CodegenConfigError { + #[error("unable to access current working directory: {0}")] + CurrentDir(std::io::Error), + + // this error should be "impossible" because we use std::env::current_dir() - cover it anyways + #[error( + "Tauri config file has no parent, this shouldn't be possible. file an issue on https://github.com/tauri-apps/tauri - target {0}" + )] + Parent(PathBuf), + + #[error("unable to parse inline JSON TAURI_CONFIG env var: {0}")] + FormatInline(serde_json::Error), + + #[error(transparent)] + Json(#[from] serde_json::Error), + + #[error("{0}")] + ConfigError(#[from] ConfigError), +} + +/// Get the [`Config`] from the `TAURI_CONFIG` environmental variable, or read from the passed path. +/// +/// If the passed path is relative, it should be relative to the current working directory of the +/// compiling crate. +pub fn get_config(path: &Path) -> Result<(Config, PathBuf), CodegenConfigError> { + let path = if path.is_relative() { + let cwd = std::env::current_dir().map_err(CodegenConfigError::CurrentDir)?; + Cow::Owned(cwd.join(path)) + } else { + Cow::Borrowed(path) + }; + + // this should be impossible because of the use of `current_dir()` above, but handle it anyways + let parent = path + .parent() + .map(ToOwned::to_owned) + .ok_or_else(|| CodegenConfigError::Parent(path.into_owned()))?; + + let target = std::env::var("TAURI_ENV_TARGET_TRIPLE") + .as_deref() + .map(Target::from_triple) + .unwrap_or_else(|_| Target::current()); + + // in the future we may want to find a way to not need the TAURI_CONFIG env var so that + // it is impossible for the content of two separate configs to get mixed up. The chances are + // already unlikely unless the developer goes out of their way to run the cli on a different + // project than the target crate. + let mut config = + serde_json::from_value(tauri_utils::config::parse::read_from(target, &parent)?.0)?; + + if let Ok(env) = std::env::var("TAURI_CONFIG") { + let merge_config: serde_json::Value = + serde_json::from_str(&env).map_err(CodegenConfigError::FormatInline)?; + json_patch::merge(&mut config, &merge_config); + } + + // Set working directory to where `tauri.config.json` is, so that relative paths in it are parsed correctly. + let old_cwd = std::env::current_dir().map_err(CodegenConfigError::CurrentDir)?; + std::env::set_current_dir(parent.clone()).map_err(CodegenConfigError::CurrentDir)?; + + let config = serde_json::from_value(config)?; + + // Reset working directory. + std::env::set_current_dir(old_cwd).map_err(CodegenConfigError::CurrentDir)?; + + Ok((config, parent)) +} + +/// Create a blake3 checksum of the passed bytes. +fn checksum(bytes: &[u8]) -> Result { + let mut hasher = vendor::blake3_reference::Hasher::default(); + hasher.update(bytes); + + let mut bytes = [0u8; 32]; + hasher.finalize(&mut bytes); + + let mut hex = String::with_capacity(2 * bytes.len()); + for b in bytes { + write!(hex, "{b:02x}")?; + } + Ok(hex) +} + +/// Cache the data to `$OUT_DIR`, only if it does not already exist. +/// +/// Due to using a checksum as the filename, an existing file should be the exact same content +/// as the data being checked. +struct Cached { + checksum: String, +} + +impl TryFrom for Cached { + type Error = EmbeddedAssetsError; + + fn try_from(value: String) -> Result { + Self::try_from(Vec::from(value)) + } +} + +impl TryFrom> for Cached { + type Error = EmbeddedAssetsError; + + fn try_from(content: Vec) -> Result { + let checksum = checksum(content.as_ref()).map_err(EmbeddedAssetsError::Hex)?; + let path = ensure_out_dir()?.join(&checksum); + + write_if_changed(&path, &content) + .map(|_| Self { checksum }) + .map_err(|error| EmbeddedAssetsError::AssetWrite { path, error }) + } +} + +impl ToTokens for Cached { + fn to_tokens(&self, tokens: &mut TokenStream) { + let path = &self.checksum; + tokens.append_all(quote!(::std::concat!(::std::env!("OUT_DIR"), "/", #path))) + } +} diff --git a/crates/tauri-codegen/src/vendor/blake3_reference.rs b/crates/tauri-codegen/src/vendor/blake3_reference.rs new file mode 100644 index 0000000..db19218 --- /dev/null +++ b/crates/tauri-codegen/src/vendor/blake3_reference.rs @@ -0,0 +1,378 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! This is a lightly modified version of the BLAKE3 reference implementation. +//! +//! The changes applied are to remove unused item warnings due to using it +//! vendored along with some minor clippy suggestions. No logic changes. I +//! suggest diffing against the original to find all the changes. +//! +//! ## Original Header +//! This is the reference implementation of BLAKE3. It is used for testing and +//! as a readable example of the algorithms involved. Section 5.1 of [the BLAKE3 +//! spec](https://github.com/BLAKE3-team/BLAKE3-specs/blob/master/blake3.pdf) +//! discusses this implementation. You can render docs for this implementation +//! by running `cargo doc --open` in this directory. +//! +//! # Example +//! +//! ``` +//! let mut hasher = tauri_codegen::vendor::blake3_reference::Hasher::new(); +//! hasher.update(b"abc"); +//! hasher.update(b"def"); +//! let mut hash = [0; 32]; +//! hasher.finalize(&mut hash); +//! let mut extended_hash = [0; 500]; +//! hasher.finalize(&mut extended_hash); +//! assert_eq!(hash, extended_hash[..32]); +//! ``` +//! +//! CC0-1.0 OR Apache-2.0 + +use core::cmp::min; +use core::convert::TryInto; + +const OUT_LEN: usize = 32; +const BLOCK_LEN: usize = 64; +const CHUNK_LEN: usize = 1024; + +const CHUNK_START: u32 = 1 << 0; +const CHUNK_END: u32 = 1 << 1; +const PARENT: u32 = 1 << 2; +const ROOT: u32 = 1 << 3; + +const IV: [u32; 8] = [ + 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19, +]; + +const MSG_PERMUTATION: [usize; 16] = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8]; + +// The mixing function, G, which mixes either a column or a diagonal. +fn g(state: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize, mx: u32, my: u32) { + state[a] = state[a].wrapping_add(state[b]).wrapping_add(mx); + state[d] = (state[d] ^ state[a]).rotate_right(16); + state[c] = state[c].wrapping_add(state[d]); + state[b] = (state[b] ^ state[c]).rotate_right(12); + state[a] = state[a].wrapping_add(state[b]).wrapping_add(my); + state[d] = (state[d] ^ state[a]).rotate_right(8); + state[c] = state[c].wrapping_add(state[d]); + state[b] = (state[b] ^ state[c]).rotate_right(7); +} + +fn round(state: &mut [u32; 16], m: &[u32; 16]) { + // Mix the columns. + g(state, 0, 4, 8, 12, m[0], m[1]); + g(state, 1, 5, 9, 13, m[2], m[3]); + g(state, 2, 6, 10, 14, m[4], m[5]); + g(state, 3, 7, 11, 15, m[6], m[7]); + // Mix the diagonals. + g(state, 0, 5, 10, 15, m[8], m[9]); + g(state, 1, 6, 11, 12, m[10], m[11]); + g(state, 2, 7, 8, 13, m[12], m[13]); + g(state, 3, 4, 9, 14, m[14], m[15]); +} + +fn permute(m: &mut [u32; 16]) { + let mut permuted = [0; 16]; + for i in 0..16 { + permuted[i] = m[MSG_PERMUTATION[i]]; + } + *m = permuted; +} + +fn compress( + chaining_value: &[u32; 8], + block_words: &[u32; 16], + counter: u64, + block_len: u32, + flags: u32, +) -> [u32; 16] { + let mut state = [ + chaining_value[0], + chaining_value[1], + chaining_value[2], + chaining_value[3], + chaining_value[4], + chaining_value[5], + chaining_value[6], + chaining_value[7], + IV[0], + IV[1], + IV[2], + IV[3], + counter as u32, + (counter >> 32) as u32, + block_len, + flags, + ]; + let mut block = *block_words; + + round(&mut state, &block); // round 1 + permute(&mut block); + round(&mut state, &block); // round 2 + permute(&mut block); + round(&mut state, &block); // round 3 + permute(&mut block); + round(&mut state, &block); // round 4 + permute(&mut block); + round(&mut state, &block); // round 5 + permute(&mut block); + round(&mut state, &block); // round 6 + permute(&mut block); + round(&mut state, &block); // round 7 + + for i in 0..8 { + state[i] ^= state[i + 8]; + state[i + 8] ^= chaining_value[i]; + } + state +} + +fn first_8_words(compression_output: [u32; 16]) -> [u32; 8] { + compression_output[0..8].try_into().unwrap() +} + +fn words_from_little_endian_bytes(bytes: &[u8], words: &mut [u32]) { + debug_assert_eq!(bytes.len(), 4 * words.len()); + for (four_bytes, word) in bytes.as_chunks::<4>().0.iter().zip(words) { + *word = u32::from_le_bytes(*four_bytes); + } +} + +// Each chunk or parent node can produce either an 8-word chaining value or, by +// setting the ROOT flag, any number of final output bytes. The Output struct +// captures the state just prior to choosing between those two possibilities. +struct Output { + input_chaining_value: [u32; 8], + block_words: [u32; 16], + counter: u64, + block_len: u32, + flags: u32, +} + +impl Output { + fn chaining_value(&self) -> [u32; 8] { + first_8_words(compress( + &self.input_chaining_value, + &self.block_words, + self.counter, + self.block_len, + self.flags, + )) + } + + fn root_output_bytes(&self, out_slice: &mut [u8]) { + for (output_block_counter, out_block) in (0u64..).zip(out_slice.chunks_mut(2 * OUT_LEN)) { + let words = compress( + &self.input_chaining_value, + &self.block_words, + output_block_counter, + self.block_len, + self.flags | ROOT, + ); + // The output length might not be a multiple of 4. + for (word, out_word) in words.iter().zip(out_block.chunks_mut(4)) { + out_word.copy_from_slice(&word.to_le_bytes()[..out_word.len()]); + } + } + } +} + +struct ChunkState { + chaining_value: [u32; 8], + chunk_counter: u64, + block: [u8; BLOCK_LEN], + block_len: u8, + blocks_compressed: u8, + flags: u32, +} + +impl ChunkState { + fn new(key_words: [u32; 8], chunk_counter: u64, flags: u32) -> Self { + Self { + chaining_value: key_words, + chunk_counter, + block: [0; BLOCK_LEN], + block_len: 0, + blocks_compressed: 0, + flags, + } + } + + fn len(&self) -> usize { + BLOCK_LEN * self.blocks_compressed as usize + self.block_len as usize + } + + fn start_flag(&self) -> u32 { + if self.blocks_compressed == 0 { + CHUNK_START + } else { + 0 + } + } + + fn update(&mut self, mut input: &[u8]) { + while !input.is_empty() { + // If the block buffer is full, compress it and clear it. More + // input is coming, so this compression is not CHUNK_END. + if self.block_len as usize == BLOCK_LEN { + let mut block_words = [0; 16]; + words_from_little_endian_bytes(&self.block, &mut block_words); + self.chaining_value = first_8_words(compress( + &self.chaining_value, + &block_words, + self.chunk_counter, + BLOCK_LEN as u32, + self.flags | self.start_flag(), + )); + self.blocks_compressed += 1; + self.block = [0; BLOCK_LEN]; + self.block_len = 0; + } + + // Copy input bytes into the block buffer. + let want = BLOCK_LEN - self.block_len as usize; + let take = min(want, input.len()); + self.block[self.block_len as usize..][..take].copy_from_slice(&input[..take]); + self.block_len += take as u8; + input = &input[take..]; + } + } + + fn output(&self) -> Output { + let mut block_words = [0; 16]; + words_from_little_endian_bytes(&self.block, &mut block_words); + Output { + input_chaining_value: self.chaining_value, + block_words, + counter: self.chunk_counter, + block_len: self.block_len as u32, + flags: self.flags | self.start_flag() | CHUNK_END, + } + } +} + +fn parent_output( + left_child_cv: [u32; 8], + right_child_cv: [u32; 8], + key_words: [u32; 8], + flags: u32, +) -> Output { + let mut block_words = [0; 16]; + block_words[..8].copy_from_slice(&left_child_cv); + block_words[8..].copy_from_slice(&right_child_cv); + Output { + input_chaining_value: key_words, + block_words, + counter: 0, // Always 0 for parent nodes. + block_len: BLOCK_LEN as u32, // Always BLOCK_LEN (64) for parent nodes. + flags: PARENT | flags, + } +} + +fn parent_cv( + left_child_cv: [u32; 8], + right_child_cv: [u32; 8], + key_words: [u32; 8], + flags: u32, +) -> [u32; 8] { + parent_output(left_child_cv, right_child_cv, key_words, flags).chaining_value() +} + +/// An incremental hasher that can accept any number of writes. +pub struct Hasher { + chunk_state: ChunkState, + key_words: [u32; 8], + cv_stack: [[u32; 8]; 54], // Space for 54 subtree chaining values: + cv_stack_len: u8, // 2^54 * CHUNK_LEN = 2^64 + flags: u32, +} + +impl Hasher { + fn new_internal(key_words: [u32; 8], flags: u32) -> Self { + Self { + chunk_state: ChunkState::new(key_words, 0, flags), + key_words, + cv_stack: [[0; 8]; 54], + cv_stack_len: 0, + flags, + } + } + + /// Construct a new `Hasher` for the regular hash function. + pub fn new() -> Self { + Self::new_internal(IV, 0) + } + + fn push_stack(&mut self, cv: [u32; 8]) { + self.cv_stack[self.cv_stack_len as usize] = cv; + self.cv_stack_len += 1; + } + + fn pop_stack(&mut self) -> [u32; 8] { + self.cv_stack_len -= 1; + self.cv_stack[self.cv_stack_len as usize] + } + + // Section 5.1.2 of the BLAKE3 spec explains this algorithm in more detail. + fn add_chunk_chaining_value(&mut self, mut new_cv: [u32; 8], mut total_chunks: u64) { + // This chunk might complete some subtrees. For each completed subtree, + // its left child will be the current top entry in the CV stack, and + // its right child will be the current value of `new_cv`. Pop each left + // child off the stack, merge it with `new_cv`, and overwrite `new_cv` + // with the result. After all these merges, push the final value of + // `new_cv` onto the stack. The number of completed subtrees is given + // by the number of trailing 0-bits in the new total number of chunks. + while total_chunks & 1 == 0 { + new_cv = parent_cv(self.pop_stack(), new_cv, self.key_words, self.flags); + total_chunks >>= 1; + } + self.push_stack(new_cv); + } + + /// Add input to the hash state. This can be called any number of times. + pub fn update(&mut self, mut input: &[u8]) { + while !input.is_empty() { + // If the current chunk is complete, finalize it and reset the + // chunk state. More input is coming, so this chunk is not ROOT. + if self.chunk_state.len() == CHUNK_LEN { + let chunk_cv = self.chunk_state.output().chaining_value(); + let total_chunks = self.chunk_state.chunk_counter + 1; + self.add_chunk_chaining_value(chunk_cv, total_chunks); + self.chunk_state = ChunkState::new(self.key_words, total_chunks, self.flags); + } + + // Compress input bytes into the current chunk state. + let want = CHUNK_LEN - self.chunk_state.len(); + let take = min(want, input.len()); + self.chunk_state.update(&input[..take]); + input = &input[take..]; + } + } + + /// Finalize the hash and write any number of output bytes. + pub fn finalize(&self, out_slice: &mut [u8]) { + // Starting with the Output from the current chunk, compute all the + // parent chaining values along the right edge of the tree, until we + // have the root Output. + let mut output = self.chunk_state.output(); + let mut parent_nodes_remaining = self.cv_stack_len as usize; + while parent_nodes_remaining > 0 { + parent_nodes_remaining -= 1; + output = parent_output( + self.cv_stack[parent_nodes_remaining], + output.chaining_value(), + self.key_words, + self.flags, + ); + } + output.root_output_bytes(out_slice); + } +} + +impl Default for Hasher { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/tauri-codegen/src/vendor/mod.rs b/crates/tauri-codegen/src/vendor/mod.rs new file mode 100644 index 0000000..323fa58 --- /dev/null +++ b/crates/tauri-codegen/src/vendor/mod.rs @@ -0,0 +1,11 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Manual vendored dependencies - NOT STABLE. +//! +//! This module and all submodules are not considered part of the public +//! api. They can and will change at any time for any reason in any +//! version. + +pub mod blake3_reference; diff --git a/crates/tauri-driver/CHANGELOG.md b/crates/tauri-driver/CHANGELOG.md new file mode 100644 index 0000000..fd67ede --- /dev/null +++ b/crates/tauri-driver/CHANGELOG.md @@ -0,0 +1,91 @@ +# 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 + +- [`06f911aaf`](https://www.github.com/tauri-apps/tauri/commit/06f911aaff495121f08ebc77d9d1b41382298a1f) ([#14871](https://www.github.com/tauri-apps/tauri/pull/14871) by [@goosewobbler](https://www.github.com/tauri-apps/tauri/../../goosewobbler)) Prevent native WebDriver stdout from polluting tauri-driver's stdout used by test frameworks. + +## \[2.0.4] + +### Enhancements + +- [`0cf2d9933`](https://www.github.com/tauri-apps/tauri/commit/0cf2d9933f20375349f9f307b4dd7049690030d9) ([#13238](https://www.github.com/tauri-apps/tauri/pull/13238)) Automatically append the `.exe` extension on the application path in `tauri:options`. + +### Bug Fixes + +- [`577c7ffc4`](https://www.github.com/tauri-apps/tauri/commit/577c7ffc45ef005403cdf698e595614038c7d1e1) ([#10108](https://www.github.com/tauri-apps/tauri/pull/10108)) Ensure the webdriver process is closed when the tauri-driver process finishes. + +## \[2.0.3] + +### Bug Fixes + +- [`fb294af8e`](https://www.github.com/tauri-apps/tauri/commit/fb294af8e3717d547029f3bbf9323318e0d9861a) ([#12383](https://www.github.com/tauri-apps/tauri/pull/12383) by [@bicarlsen](https://www.github.com/tauri-apps/tauri/../../bicarlsen)) Parse ms:edgeOptions separately to prevent `invalid argument` errors. + +## \[2.0.2] + +### Enhancements + +- [`70f96e322`](https://www.github.com/tauri-apps/tauri/commit/70f96e3222871a1931a18d6093d7efcbc59e7fee) ([#12240](https://www.github.com/tauri-apps/tauri/pull/12240) by [@getong](https://www.github.com/tauri-apps/tauri/../../getong)) Updated `hyper` to version 1. This won't affect the user facing API. + +## \[2.0.1] + +### What's Changed + +- [`0ab2b3306`](https://www.github.com/tauri-apps/tauri/commit/0ab2b330644b6419f6cee1d5377bfb5cdda2ccf9) ([#11205](https://www.github.com/tauri-apps/tauri/pull/11205) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +### What's Changed + +- [`382ed482b`](https://www.github.com/tauri-apps/tauri/commit/382ed482bd08157c39e62f9a0aaad8802f1092cb) Bump MSRV to 1.78. +- [`637285790`](https://www.github.com/tauri-apps/tauri/commit/6372857905ae9c0aedb7f482ddf6cf9f9836c9f2) Promote to v2 stable! + +## \[0.1.5] + +### Bug Fixes + +- [`5f64ed2b7`](https://www.github.com/tauri-apps/tauri/commit/5f64ed2b78201b7e379b6234f7a799d9695b11d7) ([#10738](https://www.github.com/tauri-apps/tauri/pull/10738) by [@chippers](https://www.github.com/tauri-apps/tauri/../../chippers)) support both 1.x and 2.x automation env vars in `tauri-driver` + +### What's Changed + +- [`f4d5241b3`](https://www.github.com/tauri-apps/tauri/commit/f4d5241b377d0f7a1b58100ee19f7843384634ac) ([#10731](https://www.github.com/tauri-apps/tauri/pull/10731) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Update documentation icon path. + +## \[0.1.4] + +### New Features + +- [`435d7513`](https://www.github.com/tauri-apps/tauri/commit/435d7513e45eab8b512e9a7e695a1adef8a98a46)([#8609](https://www.github.com/tauri-apps/tauri/pull/8609)) Added `webviewOptions` object to the `tauri:options` capability to configure the [Edge webview options](https://learn.microsoft.com/en-us/microsoft-edge/webdriver-chromium/capabilities-edge-options#webviewoptions-object) on Windows. + +## \[0.1.3] + +### What's Changed + +- [`9edebbba`](https://www.github.com/tauri-apps/tauri/commit/9edebbba4ec472772b2f6307232e8d256f62c8ba)([#7475](https://www.github.com/tauri-apps/tauri/pull/7475)) Update locked dependencies to fix a Windows build issue when using them with a recent Rust compiler. +- [`9edebbba`](https://www.github.com/tauri-apps/tauri/commit/9edebbba4ec472772b2f6307232e8d256f62c8ba)([#7475](https://www.github.com/tauri-apps/tauri/pull/7475)) Bump minimum Rust version to `1.60` to be in line with the rest of the Tauri project. + +## \[0.1.2] + +- Expose `native-host` option in tauri-driver and set default to `127.0.0.1`. + - [cd9dfc7b](https://www.github.com/tauri-apps/tauri/commit/cd9dfc7b9a3fe0e04e40d9b0f9be674aefd0d725) fix(driver): expose native-host option and set default to 127.0.0.1 ([#3816](https://www.github.com/tauri-apps/tauri/pull/3816)) on 2022-03-31 + +## \[0.1.1] + +- The minimum Rust version is now `1.56`. + - [a9dfc015](https://www.github.com/tauri-apps/tauri/commit/a9dfc015505afe91281c2027954ffcc588b1a59c) feat: update to edition 2021 and set minimum rust to 1.56 ([#2789](https://www.github.com/tauri-apps/tauri/pull/2789)) on 2021-10-22 +- Add `args` field (array of application CLI arguments) to the `tauri:options` capabilities. + - [d0970e34](https://www.github.com/tauri-apps/tauri/commit/d0970e3499297a6c102a36f2dc479d3d657bfaf3) feat(driver): add `args` to `tauri:options` ([#3154](https://www.github.com/tauri-apps/tauri/pull/3154)) on 2022-01-03 + +## \[0.1.0] + +- Initial release including Linux and Windows support. + - [be76fb1d](https://www.github.com/tauri-apps/tauri/commit/be76fb1dfe73a1605cc2ad246418579f4c2e1999) WebDriver support ([#1972](https://www.github.com/tauri-apps/tauri/pull/1972)) on 2021-06-23 + - [c22e5a7c](https://www.github.com/tauri-apps/tauri/commit/c22e5a7c2ebede41657973b80eff6b68106817fc) fix(covector): keep `tauri-driver` version as alpha on 2021-06-23 + - [b4426eda](https://www.github.com/tauri-apps/tauri/commit/b4426eda9e64fcdd25a2d72e548b8b0fbfa09619) Revert "WebDriver support ([#1972](https://www.github.com/tauri-apps/tauri/pull/1972))" on 2021-06-23 + - [4b2aa356](https://www.github.com/tauri-apps/tauri/commit/4b2aa35684632ed2afd7dec4ad848df5704868e4) Add back WebDriver support ([#2324](https://www.github.com/tauri-apps/tauri/pull/2324)) on 2021-08-01 diff --git a/crates/tauri-driver/Cargo.toml b/crates/tauri-driver/Cargo.toml new file mode 100644 index 0000000..afcafc9 --- /dev/null +++ b/crates/tauri-driver/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "tauri-driver" +version = "2.0.6" +authors = ["Tauri Programme within The Commons Conservancy"] +categories = ["gui", "web-programming"] +license = "Apache-2.0 OR MIT" +homepage = "https://tauri.app" +repository = "https://github.com/tauri-apps/tauri" +description = "Webdriver server for Tauri applications" +edition = "2024" +rust-version = "1.90" + +[dependencies] +anyhow = "1" +futures = "0.3" +futures-util = "0.3" +http-body-util = "0.1" +hyper = { version = "1", features = ["client", "http1", "server"] } +hyper-util = { version = "0.1", features = [ + "client", + "client-legacy", + "http1", + "server", + "tokio", +] } +pico-args = { version = "0.5", features = ["eq-separator"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["macros"] } +which = "8" + +[target."cfg(unix)".dependencies] +signal-hook = "0.4" +signal-hook-tokio = { version = "0.4", features = ["futures-v0_3"] } + +[target."cfg(windows)".dependencies] +win32job = "2" + +[lints] +workspace = true diff --git a/crates/tauri-driver/LICENSE-APACHE-2.0 b/crates/tauri-driver/LICENSE-APACHE-2.0 new file mode 100644 index 0000000..bffaaba --- /dev/null +++ b/crates/tauri-driver/LICENSE-APACHE-2.0 @@ -0,0 +1 @@ +../../LICENSE-APACHE-2.0 \ No newline at end of file diff --git a/crates/tauri-driver/LICENSE-MIT b/crates/tauri-driver/LICENSE-MIT new file mode 100644 index 0000000..b2cfbdc --- /dev/null +++ b/crates/tauri-driver/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/tauri-driver/LICENSE.spdx b/crates/tauri-driver/LICENSE.spdx new file mode 100644 index 0000000..12947c5 --- /dev/null +++ b/crates/tauri-driver/LICENSE.spdx @@ -0,0 +1 @@ +../../LICENSE.spdx \ No newline at end of file diff --git a/crates/tauri-driver/README.md b/crates/tauri-driver/README.md new file mode 100644 index 0000000..bbc8794 --- /dev/null +++ b/crates/tauri-driver/README.md @@ -0,0 +1,44 @@ +# `tauri-driver` _(pre-alpha)_ + +Cross-platform WebDriver server for Tauri applications. + +This is a [WebDriver Intermediary Node] that wraps the native WebDriver server +for platforms that [Tauri] supports. Your WebDriver client will connect to the +running `tauri-driver` server, and `tauri-driver` will handle starting the +native WebDriver server for you behind the scenes. It requires two separate +ports to be used since two distinct [WebDriver Remote Ends] run. + +You can configure the ports used with arguments when starting the binary: + +- `--port` (default: `4444`) +- `--native-port` (default: `4445`) + +Supported platforms: + +- Linux via `WebKitWebDriver` +- Windows via [Microsoft Edge Driver] +- **[Todo]** macOS via [Appium Mac2 Driver] (probably) + +_note: the (probably) items haven't been proof-of-concept'd yet, and if it is +not possible to use the listed native webdriver, then a custom implementation +will be used that wraps around [wry]._ + +## Installation + +You can install tauri-driver using Cargo: + +```sh +cargo install tauri-driver --locked +``` + +## Trying it out + +Check out the documentation at https://tauri.app/develop/tests/webdriver/, +including a small example application with WebDriver tests. + +[WebDriver Intermediary Node]: https://www.w3.org/TR/webdriver/#dfn-intermediary-nodes +[WebDriver Remote Ends]: https://www.w3.org/TR/webdriver/#dfn-remote-ends +[Microsoft Edge Driver]: https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ +[Appium Mac2 Driver]: https://github.com/appium/appium-mac2-driver +[wry]: https://github.com/tauri-apps/wry +[Tauri]: https://github.com/tauri-apps/tauri diff --git a/crates/tauri-driver/src/cli.rs b/crates/tauri-driver/src/cli.rs new file mode 100644 index 0000000..4e38ebf --- /dev/null +++ b/crates/tauri-driver/src/cli.rs @@ -0,0 +1,63 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +const HELP: &str = "\ +USAGE: tauri-driver [FLAGS] [OPTIONS] + +FLAGS: + -h, --help Prints help information + +OPTIONS: + --port NUMBER Sets the tauri-driver intermediary port + --native-port NUMBER Sets the port of the underlying WebDriver + --native-host HOST Sets the host of the underlying WebDriver (Linux only) + --native-driver PATH Sets the path to the native WebDriver binary +"; + +#[derive(Debug, Clone)] +pub struct Args { + pub port: u16, + pub native_port: u16, + pub native_host: String, + pub native_driver: Option, +} + +impl From for Args { + fn from(mut args: pico_args::Arguments) -> Self { + // if the user wanted help, we don't care about parsing the rest of the args + if args.contains(["-h", "--help"]) { + println!("{HELP}"); + std::process::exit(0); + } + + let native_driver = match args.opt_value_from_str("--native-driver") { + Ok(native_driver) => native_driver, + Err(e) => { + eprintln!("Error while parsing option --native-driver: {e}"); + std::process::exit(1); + } + }; + + let parsed = Args { + port: args.value_from_str("--port").unwrap_or(4444), + native_port: args.value_from_str("--native-port").unwrap_or(4445), + native_host: args + .value_from_str("--native-host") + .unwrap_or(String::from("127.0.0.1")), + native_driver, + }; + + // be strict about accepting args, error for anything extraneous + let rest = args.finish(); + if !rest.is_empty() { + eprintln!("Error: unused arguments left: {rest:?}"); + eprintln!("{HELP}"); + std::process::exit(1); + } + + parsed + } +} diff --git a/crates/tauri-driver/src/main.rs b/crates/tauri-driver/src/main.rs new file mode 100644 index 0000000..75795f9 --- /dev/null +++ b/crates/tauri-driver/src/main.rs @@ -0,0 +1,52 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Cross-platform WebDriver server for Tauri applications. +//! +//! This is a [WebDriver Intermediary Node](https://www.w3.org/TR/webdriver/#dfn-intermediary-nodes) that wraps the native WebDriver server for platforms that [Tauri](https://github.com/tauri-apps/tauri) supports. Your WebDriver client will connect to the running `tauri-driver` server, and `tauri-driver` will handle starting the native WebDriver server for you behind the scenes. It requires two separate ports to be used since two distinct [WebDriver Remote Ends](https://www.w3.org/TR/webdriver/#dfn-remote-ends) run. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" +)] + +#[cfg(any(target_os = "linux", windows))] +mod cli; +#[cfg(any(target_os = "linux", windows))] +mod server; +#[cfg(any(target_os = "linux", windows))] +mod webdriver; + +#[cfg(not(any(target_os = "linux", windows)))] +fn main() { + println!("tauri-driver is not supported on this platform"); + std::process::exit(1); +} + +#[cfg(any(target_os = "linux", windows))] +fn main() { + let args = pico_args::Arguments::from_env().into(); + + #[cfg(windows)] + let _job_handle = { + let job = win32job::Job::create().unwrap(); + let mut info = job.query_extended_limit_info().unwrap(); + info.limit_kill_on_job_close(); + job.set_extended_limit_info(&info).unwrap(); + job.assign_current_process().unwrap(); + job + }; + + // start the native webdriver on the port specified in args + let mut driver = webdriver::native(&args); + let driver = driver + .spawn() + .expect("error while running native webdriver"); + + // start our webdriver intermediary node + if let Err(e) = server::run(args, driver) { + eprintln!("error while running server: {e}"); + std::process::exit(1); + } +} diff --git a/crates/tauri-driver/src/server.rs b/crates/tauri-driver/src/server.rs new file mode 100644 index 0000000..c113bcc --- /dev/null +++ b/crates/tauri-driver/src/server.rs @@ -0,0 +1,245 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::cli::Args; +use anyhow::Error; +use futures_util::TryFutureExt; +use http_body_util::{BodyExt, Full}; +use hyper::{ + Method, Request, Response, + body::{Bytes, Incoming}, + header::CONTENT_LENGTH, + http::uri::Authority, + service::service_fn, +}; +use hyper_util::{ + client::legacy::{Client, connect::HttpConnector}, + rt::{TokioExecutor, TokioIo}, + server::conn::auto, +}; +use serde::Deserialize; +use serde_json::{Map, Value, json}; +use std::path::PathBuf; +use std::process::Child; +use tokio::net::TcpListener; + +const TAURI_OPTIONS: &str = "tauri:options"; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TauriOptions { + application: PathBuf, + #[serde(default)] + args: Vec, + #[cfg(target_os = "windows")] + #[serde(default)] + webview_options: Option, +} + +impl TauriOptions { + #[cfg(target_os = "linux")] + fn into_native_object(self) -> Map { + let mut map = Map::new(); + map.insert( + "webkitgtk:browserOptions".into(), + json!({"binary": self.application, "args": self.args}), + ); + map + } + + #[cfg(target_os = "windows")] + fn into_native_object(self) -> Map { + let mut ms_edge_options = Map::new(); + ms_edge_options.insert( + "binary".into(), + json!(self.application.with_extension("exe")), + ); + ms_edge_options.insert("args".into(), self.args.into()); + + if let Some(webview_options) = self.webview_options { + ms_edge_options.insert("webviewOptions".into(), webview_options); + } + + let mut map = Map::new(); + map.insert("ms:edgeChromium".into(), json!(true)); + map.insert("browserName".into(), json!("webview2")); + map.insert("ms:edgeOptions".into(), ms_edge_options.into()); + map + } +} + +async fn handle( + client: Client>, + req: Request, + args: Args, +) -> Result, Error> { + // manipulate a new session to convert options to the native driver format + let new_req: Request> = + if let (&Method::POST, "/session") = (req.method(), req.uri().path()) { + let (mut parts, body) = req.into_parts(); + + // get the body from the future stream and parse it as json + let body = body.collect().await?.to_bytes().to_vec(); + let json: Value = serde_json::from_slice(&body)?; + + // manipulate the json to convert from tauri option to native driver options + let json = map_capabilities(json); + + // serialize json and update the content-length header to be accurate + let bytes = serde_json::to_vec(&json)?; + parts.headers.insert(CONTENT_LENGTH, bytes.len().into()); + + Request::from_parts(parts, Full::new(bytes.into())) + } else { + let (parts, body) = req.into_parts(); + + let body = body.collect().await?.to_bytes().to_vec(); + + Request::from_parts(parts, Full::new(body.into())) + }; + + client + .request(forward_to_native_driver(new_req, args)?) + .err_into() + .await +} + +/// Transform the request to a request for the native webdriver server. +fn forward_to_native_driver( + mut req: Request>, + args: Args, +) -> Result>, Error> { + let host: Authority = { + let headers = req.headers_mut(); + headers.remove("host").expect("hyper request has host") + } + .to_str()? + .parse()?; + + let path = req + .uri() + .path_and_query() + .expect("hyper request has uri") + .clone(); + + let uri = format!( + "http://{}:{}{}", + host.host(), + args.native_port, + path.as_str() + ); + + let (mut parts, body) = req.into_parts(); + parts.uri = uri.parse()?; + Ok(Request::from_parts(parts, body)) +} + +/// only happy path for now, no errors +fn map_capabilities(mut json: Value) -> Value { + let mut native = None; + if let Some(capabilities) = json.get_mut("capabilities") { + if let Some(always_match) = capabilities.get_mut("alwaysMatch") { + if let Some(always_match) = always_match.as_object_mut() { + if let Some(tauri_options) = always_match.remove(TAURI_OPTIONS) { + if let Ok(options) = serde_json::from_value::(tauri_options) { + native = Some(options.into_native_object()); + } + } + + if let Some(native) = native.clone() { + always_match.extend(native); + } + } + } + } + + if let Some(native) = native { + if let Some(desired) = json.get_mut("desiredCapabilities") { + if let Some(desired) = desired.as_object_mut() { + desired.remove(TAURI_OPTIONS); + desired.extend(native); + } + } + } + + json +} + +#[tokio::main(flavor = "current_thread")] +pub async fn run(args: Args, mut _driver: Child) -> Result<(), Error> { + #[cfg(unix)] + let (signals_handle, signals_task) = { + use futures_util::StreamExt; + use signal_hook::consts::signal::*; + + let signals = signal_hook_tokio::Signals::new([SIGTERM, SIGINT, SIGQUIT])?; + let signals_handle = signals.handle(); + let signals_task = tokio::spawn(async move { + let mut signals = signals.fuse(); + #[allow(clippy::never_loop)] + while let Some(signal) = signals.next().await { + match signal { + SIGTERM | SIGINT | SIGQUIT => { + _driver + .kill() + .expect("unable to kill native webdriver server"); + std::process::exit(0); + } + _ => unreachable!(), + } + } + }); + (signals_handle, signals_task) + }; + + let address = std::net::SocketAddr::from(([127, 0, 0, 1], args.port)); + + // the client we use to proxy requests to the native webdriver + let client = Client::builder(TokioExecutor::new()) + .http1_preserve_header_case(true) + .http1_title_case_headers(true) + .retry_canceled_requests(false) + .build_http(); + + // set up a http1 server that uses the service we just created + let srv = async move { + if let Ok(listener) = TcpListener::bind(address).await { + loop { + let client = client.clone(); + let args = args.clone(); + if let Ok((stream, _)) = listener.accept().await { + let io = TokioIo::new(stream); + + tokio::task::spawn(async move { + if let Err(err) = auto::Builder::new(TokioExecutor::new()) + .http1() + .title_case_headers(true) + .preserve_header_case(true) + .serve_connection( + io, + service_fn(|request| handle(client.clone(), request, args.clone())), + ) + .await + { + println!("Error serving connection: {err:?}"); + } + }); + } else { + println!("accept new stream fail, ignore here"); + } + } + } else { + println!("can not listen to address: {address:?}"); + } + }; + srv.await; + + #[cfg(unix)] + { + signals_handle.close(); + signals_task.await?; + } + + Ok(()) +} diff --git a/crates/tauri-driver/src/webdriver.rs b/crates/tauri-driver/src/webdriver.rs new file mode 100644 index 0000000..73ce2b8 --- /dev/null +++ b/crates/tauri-driver/src/webdriver.rs @@ -0,0 +1,61 @@ +// Copyright 2019-2026 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::cli::Args; +use std::{ + env::current_dir, + process::{Command, Stdio}, +}; + +// the name of the binary to find in $PATH +#[cfg(target_os = "linux")] +const DRIVER_BINARY: &str = "WebKitWebDriver"; + +#[cfg(target_os = "windows")] +const DRIVER_BINARY: &str = "msedgedriver.exe"; + +/// Find the native driver binary in the PATH, or exits the process with an error. +pub fn native(args: &Args) -> Command { + let native_binary = match args.native_driver.as_deref() { + Some(custom) => { + if custom.exists() { + custom.to_owned() + } else { + eprintln!( + "can not find the supplied binary path {}. This is currently required.", + custom.display() + ); + match current_dir() { + Ok(cwd) => eprintln!("current working directory: {}", cwd.display()), + Err(error) => eprintln!("can not find current working directory: {error}"), + } + std::process::exit(1); + } + } + None => match which::which(DRIVER_BINARY) { + Ok(binary) => binary, + Err(error) => { + eprintln!( + "can not find binary {DRIVER_BINARY} in the PATH. This is currently required.\ + You can also pass a custom path with --native-driver" + ); + eprintln!("{error:?}"); + std::process::exit(1); + } + }, + }; + + let mut cmd = Command::new(native_binary); + cmd.env("TAURI_AUTOMATION", "true"); // 1.x + cmd.env("TAURI_WEBVIEW_AUTOMATION", "true"); // 2.x + cmd.arg(format!("--port={}", args.native_port)); + cmd.arg(format!("--host={}", args.native_host)); + + // Don't inherit stdout from parent to prevent native WebDriver binary/HTTP protocol data + // from corrupting tauri-driver's stdout (which gets captured by the test framework). + // Keep stderr inherited so WebDriver logs/errors are still visible. + cmd.stdout(Stdio::null()); + + cmd +} diff --git a/crates/tauri-macos-sign/CHANGELOG.md b/crates/tauri-macos-sign/CHANGELOG.md new file mode 100644 index 0000000..9502ef7 --- /dev/null +++ b/crates/tauri-macos-sign/CHANGELOG.md @@ -0,0 +1,74 @@ +# 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 + +- [`268bb339f`](https://www.github.com/tauri-apps/tauri/commit/268bb339f0c512f021cc94e102573432cf2696d0) ([#14766](https://www.github.com/tauri-apps/tauri/pull/14766) by [@sftse](https://www.github.com/tauri-apps/tauri/../../sftse)) Remove once-cell-regex from direct dependencies. + +## \[2.3.2] + +### Dependencies + +- [`514cf21e1`](https://www.github.com/tauri-apps/tauri/commit/514cf21e1417c7a78a0db494f891ba79d948b73d) ([#14591](https://www.github.com/tauri-apps/tauri/pull/14591) by [@Tunglies](https://www.github.com/tauri-apps/tauri/../../Tunglies)) Update num-bigint-dig from 0.8.4 to 0.8.6 + +## \[2.3.1] + +### Performance Improvements + +- [`ee3cc4a91`](https://www.github.com/tauri-apps/tauri/commit/ee3cc4a91bf1315ecaefe90f423ffd55ef6c40db) ([#14475](https://www.github.com/tauri-apps/tauri/pull/14475) by [@Tunglies](https://www.github.com/tauri-apps/tauri/../../Tunglies)) perf: remove needless clones in various files for improved performance. No user facing changes. + +## \[2.3.0] + +### Enhancements + +- [`f59bf9d53`](https://www.github.com/tauri-apps/tauri/commit/f59bf9d5392ffd209e26ce5259c26d1acc31c4ba) ([#14337](https://www.github.com/tauri-apps/tauri/pull/14337) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) **Potentially breaking change:** Export custom Error enum instead of using anyhow. The changes happened in https://github.com/tauri-apps/tauri/pull/14126. + +## \[2.2.0] + +### New Features + +- [`a9ec12843`](https://www.github.com/tauri-apps/tauri/commit/a9ec12843aa7d0eb774bd3a53e2e63da12cfa77b) ([#13521](https://www.github.com/tauri-apps/tauri/pull/13521) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Added a `--skip-stapling` option to make `tauri build|bundle` *not* wait for notarization to finish on macOS. + +## \[2.1.0] + +### Enhancements + +- [`d6520a21c`](https://www.github.com/tauri-apps/tauri/commit/d6520a21ce02c3e2be2955999946c2cb7bdb07aa) ([#12541](https://www.github.com/tauri-apps/tauri/pull/12541) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Updated `wry` to 0.50, `windows` to 0.60, `webview2-com` to 0.36, and `objc2` to 0.6. This can be a **breaking change** if you use the `with_webview` API! + +## \[2.0.1] + +### What's Changed + +- [`0ab2b3306`](https://www.github.com/tauri-apps/tauri/commit/0ab2b330644b6419f6cee1d5377bfb5cdda2ccf9) ([#11205](https://www.github.com/tauri-apps/tauri/pull/11205) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +### What's Changed + +- [`382ed482b`](https://www.github.com/tauri-apps/tauri/commit/382ed482bd08157c39e62f9a0aaad8802f1092cb) Bump MSRV to 1.78. +- [`637285790`](https://www.github.com/tauri-apps/tauri/commit/6372857905ae9c0aedb7f482ddf6cf9f9836c9f2) Promote to v2 stable! + +## \[0.1.1-rc.1] + +### Enhancements + +- [`f5d61822b`](https://www.github.com/tauri-apps/tauri/commit/f5d61822bf5988827776dd58bed75c19364e86bd) ([#11184](https://www.github.com/tauri-apps/tauri/pull/11184) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Added `Keychain::with_certificate_file` and `certificate::generate_self_signed`. + +## \[0.1.1-rc.0] + +### Bug Fixes + +- [`1b0c447fc`](https://www.github.com/tauri-apps/tauri/commit/1b0c447fcbc424e08e4260277ec178df86f45d1d) ([#10654](https://www.github.com/tauri-apps/tauri/pull/10654) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fixes output not visible when running on Node.js via NAPI. + +## \[0.1.0-beta.0] + +### New Features + +- [`7c7fa0964`](https://www.github.com/tauri-apps/tauri/commit/7c7fa0964db3403037fdb9a34de2b877ddb8df1c) ([#9963](https://www.github.com/tauri-apps/tauri/pull/9963) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Initial release. diff --git a/crates/tauri-macos-sign/Cargo.toml b/crates/tauri-macos-sign/Cargo.toml new file mode 100644 index 0000000..258e2bf --- /dev/null +++ b/crates/tauri-macos-sign/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "tauri-macos-sign" +version = "2.3.4" +authors = ["Tauri Programme within The Commons Conservancy"] +license = "Apache-2.0 OR MIT" +keywords = ["codesign", "signing", "macos", "ios", "tauri"] +repository = "https://github.com/tauri-apps/tauri" +description = "Code signing utilities for macOS and iOS apps" +edition = "2024" +rust-version = "1.90" + +[dependencies] +thiserror = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tempfile = "3" +# This must match the version apple-codesign uses +x509-certificate = "0.24" +once_cell = "1" +regex = "1" +os_pipe = "1" +plist = "1" +rand = "0.10" +dirs = "7" +log = { version = "0.4.21", features = ["kv"] } +apple-codesign = { version = "0.29", default-features = false } +chrono = "0.4" +p12 = "0.6" +base64 = "0.22" + +[lints] +workspace = true diff --git a/crates/tauri-macos-sign/README.md b/crates/tauri-macos-sign/README.md new file mode 100644 index 0000000..b9e4ebe --- /dev/null +++ b/crates/tauri-macos-sign/README.md @@ -0,0 +1,3 @@ +# Tauri MacOS Sign + +Utilities for setting up macOS certificates, code signing and notarization for macOS and iOS apps. diff --git a/crates/tauri-macos-sign/src/certificate.rs b/crates/tauri-macos-sign/src/certificate.rs new file mode 100644 index 0000000..11d2cf3 --- /dev/null +++ b/crates/tauri-macos-sign/src/certificate.rs @@ -0,0 +1,71 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use apple_codesign::create_self_signed_code_signing_certificate; +use x509_certificate::{EcdsaCurve, KeyAlgorithm}; + +pub use apple_codesign::CertificateProfile; + +use crate::{Error, Result}; + +/// Self signed certificate options. +pub struct SelfSignedCertificateRequest { + /// Which key type to use + pub algorithm: String, + + /// Profile + pub profile: CertificateProfile, + + /// Team ID (this is a short string attached to your Apple Developer account) + pub team_id: String, + + /// The name of the person this certificate is for + pub person_name: String, + + /// Country Name (C) value for certificate identifier + pub country_name: String, + + /// How many days the certificate should be valid for + pub validity_days: i64, + + /// Certificate password. + pub password: String, +} + +pub fn generate_self_signed(request: SelfSignedCertificateRequest) -> Result> { + let algorithm = match request.algorithm.as_str() { + "ecdsa" => KeyAlgorithm::Ecdsa(EcdsaCurve::Secp256r1), + "ed25519" => KeyAlgorithm::Ed25519, + "rsa" => KeyAlgorithm::Rsa, + value => panic!("algorithm values should have been validated by arg parser: {value}"), + }; + + let validity_duration = chrono::Duration::days(request.validity_days); + + let (cert, key_pair) = create_self_signed_code_signing_certificate( + algorithm, + request.profile, + &request.team_id, + &request.person_name, + &request.country_name, + validity_duration, + ) + .map_err(|error| Error::FailedToCreateSelfSignedCertificate { + error: Box::new(error), + })?; + + let pfx = p12::PFX::new( + &cert + .encode_der() + .map_err(|error| Error::FailedToEncodeDER { error })?, + &key_pair.to_pkcs8_one_asymmetric_key_der(), + None, + &request.password, + "code-signing", + ) + .ok_or(Error::FailedToCreatePFX)?; + let der = pfx.to_der(); + + Ok(der) +} diff --git a/crates/tauri-macos-sign/src/keychain.rs b/crates/tauri-macos-sign/src/keychain.rs new file mode 100644 index 0000000..1cdead3 --- /dev/null +++ b/crates/tauri-macos-sign/src/keychain.rs @@ -0,0 +1,254 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + ffi::OsString, + path::{Path, PathBuf}, + process::Command, +}; + +use crate::{CommandExt, Error, Result, assert_command}; +use rand::distr::{Alphanumeric, SampleString}; + +mod identity; + +pub use identity::Team; + +pub enum SigningIdentity { + Team(Team), + Identifier(String), +} + +pub struct Keychain { + // none means the default keychain must be used + path: Option, + signing_identity: SigningIdentity, +} + +impl Drop for Keychain { + fn drop(&mut self) { + if let Some(path) = &self.path { + let _ = Command::new("security") + .arg("delete-keychain") + .arg(path) + .piped(); + } + } +} + +impl Keychain { + /// Use a certificate in the default keychain. + pub fn with_signing_identity(identity: impl Into) -> Self { + Self { + path: None, + signing_identity: SigningIdentity::Identifier(identity.into()), + } + } + + /// Import certificate from base64 string. + /// certificate_encoded is the p12 certificate base64 encoded. + /// By example you can use; openssl base64 -in MyCertificate.p12 -out MyCertificate-base64.txt + /// Then use the value of the base64 as `certificate_encoded`. + /// You need to set certificate_password to the password you set when you exported your certificate. + /// see: `Export a signing certificate` + pub fn with_certificate( + certificate_encoded: &OsString, + certificate_password: &OsString, + ) -> Result { + let tmp_dir = tempfile::tempdir().map_err(Error::TempDir)?; + let cert_path = tmp_dir.path().join("cert.p12"); + super::decode_base64(certificate_encoded, &cert_path)?; + Self::with_certificate_file(&cert_path, certificate_password) + } + + pub fn with_certificate_file(cert_path: &Path, certificate_password: &OsString) -> Result { + let home_dir = dirs::home_dir().ok_or(Error::ResolveHomeDir)?; + let keychain_path = home_dir.join("Library").join("Keychains").join(format!( + "{}.keychain-db", + Alphanumeric.sample_string(&mut rand::rng(), 16) + )); + let keychain_password = Alphanumeric.sample_string(&mut rand::rng(), 16); + + let keychain_list_output = Command::new("security") + .args(["list-keychain", "-d", "user"]) + .output() + .map_err(|e| Error::CommandFailed { + command: "security list-keychain -d user".to_string(), + error: e, + })?; + + assert_command( + Command::new("security") + .args(["create-keychain", "-p", &keychain_password]) + .arg(&keychain_path) + .piped(), + "failed to create keychain", + ) + .map_err(|error| Error::CommandFailed { + command: "security create-Keychain".to_string(), + error, + })?; + + assert_command( + Command::new("security") + .args(["unlock-keychain", "-p", &keychain_password]) + .arg(&keychain_path) + .piped(), + "failed to set unlock keychain", + ) + .map_err(|error| Error::CommandFailed { + command: "security unlock-keychain".to_string(), + error, + })?; + + assert_command( + Command::new("security") + .arg("import") + .arg(cert_path) + .arg("-P") + .arg(certificate_password) + .args([ + "-T", + "/usr/bin/codesign", + "-T", + "/usr/bin/pkgbuild", + "-T", + "/usr/bin/productbuild", + ]) + .arg("-k") + .arg(&keychain_path) + .piped(), + "failed to import keychain certificate", + ) + .map_err(|error| Error::CommandFailed { + command: "security import".to_string(), + error, + })?; + + assert_command( + Command::new("security") + .args(["set-keychain-settings", "-t", "3600", "-u"]) + .arg(&keychain_path) + .piped(), + "failed to set keychain settings", + ) + .map_err(|error| Error::CommandFailed { + command: "security set-keychain-settings".to_string(), + error, + })?; + + assert_command( + Command::new("security") + .args([ + "set-key-partition-list", + "-S", + "apple-tool:,apple:,codesign:", + "-s", + "-k", + &keychain_password, + ]) + .arg(&keychain_path) + .piped(), + "failed to set keychain settings", + ) + .map_err(|error| Error::CommandFailed { + command: "security set-key-partition-list".to_string(), + error, + })?; + + let current_keychains = String::from_utf8_lossy(&keychain_list_output.stdout) + .split('\n') + .map(|line| { + line + .trim_matches(|c: char| c.is_whitespace() || c == '"') + .to_string() + }) + .filter(|l| !l.is_empty()) + .collect::>(); + + assert_command( + Command::new("security") + .args(["list-keychain", "-d", "user", "-s"]) + .args(current_keychains) + .arg(&keychain_path) + .piped(), + "failed to list keychain", + ) + .map_err(|error| Error::CommandFailed { + command: "security list-keychain".to_string(), + error, + })?; + + let signing_identity = identity::list(&keychain_path) + .map(|l| l.first().cloned())? + .ok_or(Error::ResolveSigningIdentity)?; + + Ok(Self { + path: Some(keychain_path), + signing_identity: SigningIdentity::Team(signing_identity), + }) + } + + pub fn signing_identity(&self) -> String { + match &self.signing_identity { + SigningIdentity::Team(t) => t.certificate_name(), + SigningIdentity::Identifier(i) => i.to_string(), + } + } + + pub fn team_id(&self) -> Option<&str> { + match &self.signing_identity { + SigningIdentity::Team(t) => Some(&t.id), + SigningIdentity::Identifier(_) => None, + } + } + + pub fn sign( + &self, + path: &Path, + entitlements_path: Option<&Path>, + hardened_runtime: bool, + ) -> Result<()> { + let identity = match &self.signing_identity { + SigningIdentity::Team(t) => t.certificate_name(), + SigningIdentity::Identifier(i) => i.clone(), + }; + println!("Signing with identity \"{identity}\""); + + println!("Signing {}", path.display()); + + let mut args = vec!["--force", "-s", &identity]; + + if hardened_runtime { + args.push("--options"); + args.push("runtime"); + } + + let mut codesign = Command::new("codesign"); + codesign.args(args); + if let Some(p) = &self.path { + codesign.arg("--keychain").arg(p); + } + + if let Some(entitlements_path) = entitlements_path { + codesign.arg("--entitlements"); + codesign.arg(entitlements_path); + } + + codesign.arg(path); + + assert_command(codesign.piped(), "failed to sign app").map_err(|error| { + Error::CommandFailed { + command: "codesign".to_string(), + error, + } + })?; + + Ok(()) + } + + pub fn path(&self) -> Option<&Path> { + self.path.as_deref() + } +} diff --git a/crates/tauri-macos-sign/src/keychain/identity.rs b/crates/tauri-macos-sign/src/keychain/identity.rs new file mode 100644 index 0000000..fbe4e15 --- /dev/null +++ b/crates/tauri-macos-sign/src/keychain/identity.rs @@ -0,0 +1,120 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use once_cell::sync::OnceCell; +use regex::Regex; +use std::{collections::BTreeSet, path::Path, process::Command}; +use x509_certificate::certificate::X509Certificate; + +use crate::{Error, Result}; + +fn get_pem_list(keychain_path: &Path, name_substr: &str) -> Result { + Command::new("security") + .arg("find-certificate") + .args(["-p", "-a"]) + .arg("-c") + .arg(name_substr) + .arg(keychain_path) + .stdin(os_pipe::dup_stdin().unwrap()) + .stderr(os_pipe::dup_stderr().unwrap()) + .output() + .map_err(|error| Error::CommandFailed { + command: "security find-certificate".to_string(), + error, + }) +} + +#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)] +pub struct Team { + pub name: String, + pub certificate_name: String, + pub id: String, + pub cert_prefix: &'static str, +} + +impl Team { + fn from_x509(cert_prefix: &'static str, cert: X509Certificate) -> Result { + let common_name = cert + .subject_common_name() + .ok_or(Error::CertificateMissingCommonName)?; + + let organization = cert + .subject_name() + .iter_organization() + .next() + .and_then(|v| v.to_string().ok()); + + let name = if let Some(organization) = organization { + println!("found cert {common_name:?} with organization {organization:?}"); + organization + } else { + println!( + "found cert {common_name:?} but failed to get organization; falling back to displaying common name" + ); + static APPLE_DEV: OnceCell = OnceCell::new(); + APPLE_DEV.get_or_init(|| Regex::new(r"Apple Develop\w+: (.*) \(.+\)").unwrap()) + .captures(&common_name) + .map(|caps| caps[1].to_owned()) + .unwrap_or_else(|| { + println!("regex failed to capture nice part of name in cert {common_name:?}; falling back to displaying full name"); + common_name.clone() + }) + }; + + let id = cert + .subject_name() + .iter_organizational_unit() + .next() + .and_then(|v| v.to_string().ok()) + .ok_or_else(|| Error::CertificateMissingOrganizationUnit { + common_name: common_name.clone(), + })?; + + Ok(Self { + name, + certificate_name: common_name, + id, + cert_prefix, + }) + } + + pub fn certificate_name(&self) -> String { + self.certificate_name.clone() + } +} + +pub fn list(keychain_path: &Path) -> Result> { + let certs = { + let mut certs = Vec::new(); + for cert_prefix in [ + "iOS Distribution:", + "Apple Distribution:", + "Developer ID Application:", + "Mac App Distribution:", + "Apple Development:", + "iOS App Development:", + "Mac Development:", + ] { + let pem_list_out = get_pem_list(keychain_path, cert_prefix)?; + let cert_list = X509Certificate::from_pem_multiple(pem_list_out.stdout) + .map_err(|error| Error::X509Certificate { error })?; + certs.extend(cert_list.into_iter().map(|cert| (cert_prefix, cert))); + } + certs + }; + Ok( + certs + .into_iter() + .flat_map(|(cert_prefix, cert)| { + Team::from_x509(cert_prefix, cert).map_err(|err| { + log::error!("{err}"); + err + }) + }) + // Silly way to sort this and ensure no dupes + .collect::>() + .into_iter() + .collect(), + ) +} diff --git a/crates/tauri-macos-sign/src/lib.rs b/crates/tauri-macos-sign/src/lib.rs new file mode 100644 index 0000000..c6e9382 --- /dev/null +++ b/crates/tauri-macos-sign/src/lib.rs @@ -0,0 +1,368 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + ffi::{OsStr, OsString}, + path::{Path, PathBuf}, + process::{Command, ExitStatus}, +}; + +use serde::Deserialize; + +pub mod certificate; +mod keychain; +mod provisioning_profile; + +pub use keychain::{Keychain, Team}; +pub use provisioning_profile::ProvisioningProfile; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("failed to create temp directory: {0}")] + TempDir(std::io::Error), + #[error("failed to resolve home dir")] + ResolveHomeDir, + #[error("failed to resolve signing identity")] + ResolveSigningIdentity, + #[error("failed to decode provisioning profile")] + FailedToDecodeProvisioningProfile, + #[error("could not find provisioning profile UUID")] + FailedToFindProvisioningProfileUuid, + #[error("{context} {path}: {error}")] + Plist { + context: &'static str, + path: PathBuf, + error: plist::Error, + }, + #[error("failed to upload app to Apple's notarization servers: {error}")] + FailedToUploadApp { error: std::io::Error }, + #[error("failed to notarize app: {0}")] + Notarize(String), + #[error("failed to parse notarytool output as JSON: {output}")] + ParseNotarytoolOutput { output: String }, + #[error("failed to run command {command}: {error}")] + CommandFailed { + command: String, + error: std::io::Error, + }, + #[error("{context} {path}: {error}")] + Fs { + context: &'static str, + path: PathBuf, + error: std::io::Error, + }, + #[error("failed to parse X509 certificate: {error}")] + X509Certificate { + error: x509_certificate::X509CertificateError, + }, + #[error("failed to create PFX from self signed certificate")] + FailedToCreatePFX, + #[error("failed to create self signed certificate: {error}")] + FailedToCreateSelfSignedCertificate { + error: Box, + }, + #[error("failed to encode DER: {error}")] + FailedToEncodeDER { error: std::io::Error }, + #[error("failed to decode base64 certificate: {0}")] + Base64Decode(base64::DecodeError), + #[error("certificate missing common name")] + CertificateMissingCommonName, + #[error("certificate missing organization unit for common name {common_name}")] + CertificateMissingOrganizationUnit { common_name: String }, +} + +pub type Result = std::result::Result; + +trait CommandExt { + // The `pipe` function sets the stdout and stderr to properly + // show the command output in the Node.js wrapper. + fn piped(&mut self) -> std::io::Result; +} + +impl CommandExt for Command { + fn piped(&mut self) -> std::io::Result { + self.stdin(os_pipe::dup_stdin()?); + self.stdout(os_pipe::dup_stdout()?); + self.stderr(os_pipe::dup_stderr()?); + let program = self.get_program().to_string_lossy().into_owned(); + log::debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}"))); + + self.status() + } +} + +pub enum ApiKey { + Path(PathBuf), + Raw(Vec), +} + +pub enum AppleNotarizationCredentials { + AppleId { + apple_id: OsString, + password: OsString, + team_id: OsString, + }, + ApiKey { + issuer: OsString, + key_id: OsString, + key: ApiKey, + }, +} + +#[derive(Deserialize)] +struct NotarytoolSubmitOutput { + id: String, + #[serde(default)] + status: Option, + message: String, +} + +pub fn notarize( + keychain: &Keychain, + app_bundle_path: &Path, + auth: &AppleNotarizationCredentials, +) -> Result<()> { + notarize_inner(keychain, app_bundle_path, auth, true) +} + +pub fn notarize_without_stapling( + keychain: &Keychain, + app_bundle_path: &Path, + auth: &AppleNotarizationCredentials, +) -> Result<()> { + notarize_inner(keychain, app_bundle_path, auth, false) +} + +fn notarize_inner( + keychain: &Keychain, + app_bundle_path: &Path, + auth: &AppleNotarizationCredentials, + wait: bool, +) -> Result<()> { + let bundle_stem = app_bundle_path + .file_stem() + .expect("failed to get bundle filename"); + + let tmp_dir = tempfile::tempdir().map_err(Error::TempDir)?; + let zip_path = tmp_dir + .path() + .join(format!("{}.zip", bundle_stem.to_string_lossy())); + let zip_args = vec![ + "-c", + "-k", + "--keepParent", + "--sequesterRsrc", + app_bundle_path + .to_str() + .expect("failed to convert bundle_path to string"), + zip_path + .to_str() + .expect("failed to convert zip_path to string"), + ]; + + // use ditto to create a PKZip almost identical to Finder + // this remove almost 99% of false alarm in notarization + assert_command( + Command::new("ditto").args(zip_args).piped(), + "failed to zip app with ditto", + ) + .map_err(|error| Error::CommandFailed { + command: "ditto".to_string(), + error, + })?; + + // sign the zip file + keychain.sign(&zip_path, None, false)?; + + let mut notarize_args = vec![ + "notarytool", + "submit", + zip_path + .to_str() + .expect("failed to convert zip_path to string"), + "--output-format", + "json", + ]; + if wait { + notarize_args.push("--wait"); + } + let notarize_args = notarize_args; + + println!("Notarizing {}", app_bundle_path.display()); + + let output = Command::new("xcrun") + .args(notarize_args) + .notarytool_args(auth, tmp_dir.path())? + .output() + .map_err(|error| Error::FailedToUploadApp { error })?; + + if !output.status.success() { + return Err(Error::Notarize( + String::from_utf8_lossy(&output.stderr).into_owned(), + )); + } + + let output_str = String::from_utf8_lossy(&output.stdout); + if let Ok(submit_output) = serde_json::from_str::(&output_str) { + let log_message = format!( + "{} with status {} for id {} ({})", + if wait { "Finished" } else { "Submitted" }, + submit_output.status.as_deref().unwrap_or("Pending"), + submit_output.id, + submit_output.message + ); + // status is empty when not waiting for the notarization to finish + if submit_output.status.map_or(!wait, |s| s == "Accepted") { + println!("Notarizing {log_message}"); + + if wait { + println!("Stapling app..."); + staple_app(app_bundle_path.to_path_buf())?; + } else { + println!("Not waiting for notarization to finish."); + println!("You can use `xcrun notarytool log` to check the notarization progress."); + println!( + "When it's done you can optionally staple your app via `xcrun stapler staple {}`", + app_bundle_path.display() + ); + } + + Ok(()) + } else if let Ok(output) = Command::new("xcrun") + .args(["notarytool", "log"]) + .arg(&submit_output.id) + .notarytool_args(auth, tmp_dir.path())? + .output() + { + Err(Error::Notarize(format!( + "{log_message}\nLog:\n{}", + String::from_utf8_lossy(&output.stdout) + ))) + } else { + Err(Error::Notarize(log_message)) + } + } else { + Err(Error::ParseNotarytoolOutput { + output: output_str.into_owned(), + }) + } +} + +fn staple_app(mut app_bundle_path: PathBuf) -> Result<()> { + let app_bundle_path_clone = app_bundle_path.clone(); + let filename = app_bundle_path_clone + .file_name() + .expect("failed to get bundle filename") + .to_str() + .expect("failed to convert bundle filename to string"); + + app_bundle_path.pop(); + + Command::new("xcrun") + .args(vec!["stapler", "staple", "-v", filename]) + .current_dir(app_bundle_path) + .output() + .map_err(|error| Error::CommandFailed { + command: "xcrun stapler staple".to_string(), + error, + })?; + + Ok(()) +} + +pub trait NotarytoolCmdExt { + fn notarytool_args( + &mut self, + auth: &AppleNotarizationCredentials, + temp_dir: &Path, + ) -> Result<&mut Self>; +} + +impl NotarytoolCmdExt for Command { + fn notarytool_args( + &mut self, + auth: &AppleNotarizationCredentials, + temp_dir: &Path, + ) -> Result<&mut Self> { + match auth { + AppleNotarizationCredentials::AppleId { + apple_id, + password, + team_id, + } => Ok( + self + .arg("--apple-id") + .arg(apple_id) + .arg("--password") + .arg(password) + .arg("--team-id") + .arg(team_id), + ), + AppleNotarizationCredentials::ApiKey { + key, + key_id, + issuer, + } => { + let key_path = match key { + ApiKey::Raw(k) => { + let key_path = temp_dir.join("AuthKey.p8"); + std::fs::write(&key_path, k).map_err(|error| Error::Fs { + context: "failed to write notarization API key to temp file", + path: key_path.clone(), + error, + })?; + key_path + } + ApiKey::Path(p) => p.to_owned(), + }; + + Ok( + self + .arg("--key-id") + .arg(key_id) + .arg("--key") + .arg(key_path) + .arg("--issuer") + .arg(issuer), + ) + } + } + } +} + +fn decode_base64(base64_input: &OsStr, out_path: &Path) -> Result<()> { + use base64::Engine; + + let input = base64_input + .to_str() + .expect("failed to convert base64 to string"); + + // strip whitespace before decoding + let cleaned: String = input.chars().filter(|c| !c.is_ascii_whitespace()).collect(); + + let decoded = base64::engine::general_purpose::STANDARD + .decode(&cleaned) + .map_err(Error::Base64Decode)?; + + std::fs::write(out_path, &decoded).map_err(|error| Error::Fs { + context: "failed to write decoded certificate", + path: out_path.to_path_buf(), + error, + })?; + + Ok(()) +} + +fn assert_command( + response: std::result::Result, + error_message: &str, +) -> std::io::Result<()> { + let status = + response.map_err(|e| std::io::Error::new(e.kind(), format!("{error_message}: {e}")))?; + if !status.success() { + Err(std::io::Error::other(error_message)) + } else { + Ok(()) + } +} diff --git a/crates/tauri-macos-sign/src/provisioning_profile.rs b/crates/tauri-macos-sign/src/provisioning_profile.rs new file mode 100644 index 0000000..9e251a7 --- /dev/null +++ b/crates/tauri-macos-sign/src/provisioning_profile.rs @@ -0,0 +1,64 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ffi::OsStr, path::PathBuf, process::Command}; + +use crate::{Error, Result}; +use rand::distr::{Alphanumeric, SampleString}; + +pub struct ProvisioningProfile { + path: PathBuf, +} + +impl ProvisioningProfile { + pub fn from_base64(base64: &OsStr) -> Result { + let home_dir = dirs::home_dir().ok_or(Error::ResolveHomeDir)?; + let provisioning_profiles_folder = home_dir + .join("Library") + .join("MobileDevice") + .join("Provisioning Profiles"); + std::fs::create_dir_all(&provisioning_profiles_folder).map_err(|error| Error::Fs { + context: "failed to create provisioning profiles folder", + path: provisioning_profiles_folder.clone(), + error, + })?; + + let provisioning_profile_path = provisioning_profiles_folder.join(format!( + "{}.mobileprovision", + Alphanumeric.sample_string(&mut rand::rng(), 16) + )); + super::decode_base64(base64, &provisioning_profile_path)?; + + Ok(Self { + path: provisioning_profile_path, + }) + } + + pub fn uuid(&self) -> Result { + let output = Command::new("security") + .args(["cms", "-D", "-i"]) + .arg(&self.path) + .output() + .map_err(|error| Error::CommandFailed { + command: "security cms -D -i".to_string(), + error, + })?; + + if !output.status.success() { + return Err(Error::FailedToDecodeProvisioningProfile); + } + + let plist = + plist::from_bytes::(&output.stdout).map_err(|error| Error::Plist { + context: "failed to parse provisioning profile as plist", + path: self.path.clone(), + error, + })?; + + plist + .get("UUID") + .and_then(|v| v.as_string().map(ToString::to_string)) + .ok_or(Error::FailedToFindProvisioningProfileUuid) + } +} diff --git a/crates/tauri-macros/CHANGELOG.md b/crates/tauri-macros/CHANGELOG.md new file mode 100644 index 0000000..767e776 --- /dev/null +++ b/crates/tauri-macros/CHANGELOG.md @@ -0,0 +1,864 @@ +# Changelog + +## \[2.6.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.3` +- Upgraded to `tauri-codegen@2.6.3` + +## \[2.6.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.2` +- Upgraded to `tauri-codegen@2.6.2` + +## \[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 + +- Upgraded to `tauri-utils@2.8.3` +- Upgraded to `tauri-codegen@2.5.5` + +## \[2.5.4] + +### Dependencies + +- Upgraded to `tauri-codegen@2.5.4` + +## \[2.5.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.8.2` +- Upgraded to `tauri-codegen@2.5.3` + +## \[2.5.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.8.1` +- Upgraded to `tauri-codegen@2.5.2` + +## \[2.5.1] + +### Bug Fixes + +- [`4b00130b8`](https://www.github.com/tauri-apps/tauri/commit/4b00130b86a27b6f121bf57897b5e92d83bcc0fc) ([#14385](https://www.github.com/tauri-apps/tauri/pull/14385) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix iOS deadlock when running on the simulator from Xcode by properly piping stdout/stderr messages through the Xcode console and OSLog. + +### Dependencies + +- Upgraded to `tauri-codegen@2.5.1` + +## \[2.5.0] + +### Bug Fixes + +- [`69476d8e2`](https://www.github.com/tauri-apps/tauri/commit/69476d8e2314b85bf46046140bc5495fe29b7d29) ([#14170](https://www.github.com/tauri-apps/tauri/pull/14170)) Fix the stack overflow when having too many commands in a single invoke handler in release build + +### Dependencies + +- Upgraded to `tauri-utils@2.8.0` +- Upgraded to `tauri-codegen@2.5.0` +- [`6aa7f2d85`](https://www.github.com/tauri-apps/tauri/commit/6aa7f2d852870aeba1d4dd0e07f8be2bc9b66286) Upgraded to `tauri-utils@2.8.0` + +## \[2.4.0] + +### Dependencies + +- Upgraded to `tauri-utils@2.7.0`. +- Upgraded to `tauri-codegen@2.4.0` + +## \[2.3.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.6.0` +- Upgraded to `tauri-codegen@2.3.1` + +## \[2.3.1] + +### Performance Improvements + +- [`0a552a868`](https://www.github.com/tauri-apps/tauri/commit/0a552a868c559d367d85c264bf052009b048bd8e) ([#13690](https://www.github.com/tauri-apps/tauri/pull/13690) by [@montyc1999](https://www.github.com/tauri-apps/tauri/../../montyc1999)) Cache `rustc -V` output in `#[tauri::command]` macros + +## \[2.2.1] + +### Dependencies + +- Upgraded to `tauri-codegen@2.3.0` +- Upgraded to `tauri-utils@2.5.0` + +## \[2.2.0] + +### Dependencies + +- Upgraded to `tauri-utils@2.4.0` +- Upgraded to `tauri-codegen@2.2.0` +- [`48b12b440`](https://www.github.com/tauri-apps/tauri/commit/48b12b440478937c46fdfef9f9d95194be117020) Update to `tauri-utils@2.4.0` + +## \[2.1.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.3.1` +- Upgraded to `tauri-codegen@2.1.1` + +## \[2.1.0] + +### New Features + +- [`013f8f652`](https://www.github.com/tauri-apps/tauri/commit/013f8f652302f2d49c5ec0a075582033d8b074fb) ([#12890](https://www.github.com/tauri-apps/tauri/pull/12890) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Added `build > removeUnusedCommands` to trigger the build scripts and macros to remove unused commands based on the capabilities you defined. Note this won't be accounting for dynamically added ACLs so make sure to check it when using this. + +### Dependencies + +- Upgraded to `tauri-utils@2.3.0` +- Upgraded to `tauri-codegen@2.1.0` + +## \[2.0.5] + +### Dependencies + +- Upgraded to `tauri-utils@2.2.0` +- Upgraded to `tauri-codegen@2.0.5` + +## \[2.0.4] + +### Dependencies + +- Upgraded to `tauri-utils@2.1.1` +- Upgraded to `tauri-codegen@2.0.4` + +## \[2.0.3] + +### Enhancements + +- [`17c6952ae`](https://www.github.com/tauri-apps/tauri/commit/17c6952aec965fa41e6695ad68461a218afc20f1) ([#11522](https://www.github.com/tauri-apps/tauri/pull/11522) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Enhance the error message when using `async` commands with a reference. + +### Dependencies + +- Upgraded to `tauri-utils@2.1.0` +- Upgraded to `tauri-codegen@2.0.3` + +## \[2.0.1] + +### What's Changed + +- [`0ab2b3306`](https://www.github.com/tauri-apps/tauri/commit/0ab2b330644b6419f6cee1d5377bfb5cdda2ccf9) ([#11205](https://www.github.com/tauri-apps/tauri/pull/11205) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.1` +- Upgraded to `tauri-codegen@2.0.1` + +## \[2.0.0] + +### What's Changed + +- [`382ed482b`](https://www.github.com/tauri-apps/tauri/commit/382ed482bd08157c39e62f9a0aaad8802f1092cb) Bump MSRV to 1.78. +- [`637285790`](https://www.github.com/tauri-apps/tauri/commit/6372857905ae9c0aedb7f482ddf6cf9f9836c9f2) Promote to v2 stable! + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0` +- Upgraded to `tauri-codegen@2.0.0` + +## \[2.0.0-rc.12] + +### New Features + +- [`1d8b67b29`](https://www.github.com/tauri-apps/tauri/commit/1d8b67b2970a09ec478093e127612fac823de805) ([#11162](https://www.github.com/tauri-apps/tauri/pull/11162) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Support async functions for `mobile_entry_point` macro + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.13` +- Upgraded to `tauri-codegen@2.0.0-rc.13` + +## \[2.0.0-rc.11] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.12` +- Upgraded to `tauri-codegen@2.0.0-rc.12` + +## \[2.0.0-rc.10] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.11` +- Upgraded to `tauri-codegen@2.0.0-rc.11` + +## \[2.0.0-rc.9] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.10` +- Upgraded to `tauri-codegen@2.0.0-rc.10` + +## \[2.0.0-rc.8] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.9` +- Upgraded to `tauri-codegen@2.0.0-rc.9` + +## \[2.0.0-rc.7] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.8` +- Upgraded to `tauri-codegen@2.0.0-rc.8` + +## \[2.0.0-rc.6] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.7` +- Upgraded to `tauri-codegen@2.0.0-rc.7` + +## \[2.0.0-rc.6] + +### What's Changed + +- [`f4d5241b3`](https://www.github.com/tauri-apps/tauri/commit/f4d5241b377d0f7a1b58100ee19f7843384634ac) ([#10731](https://www.github.com/tauri-apps/tauri/pull/10731) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Update documentation icon path. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.6` +- Upgraded to `tauri-codegen@2.0.0-rc.6` + +## \[2.0.0-rc.5] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.5` +- Upgraded to `tauri-codegen@2.0.0-rc.5` + +## \[2.0.0-rc.4] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.4` +- Upgraded to `tauri-codegen@2.0.0-rc.4` + +## \[2.0.0-rc.3] + +### Dependencies + +- Upgraded to `tauri-codegen@2.0.0-rc.3` +- Upgraded to `tauri-utils@2.0.0-rc.3` + +## \[2.0.0-rc.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.2` +- Upgraded to `tauri-codegen@2.0.0-rc.2` + +## \[2.0.0-rc.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.1` +- Upgraded to `tauri-codegen@2.0.0-rc.1` + +## \[2.0.0-rc.0] + +### Dependencies + +- Upgraded to `tauri-codegen@2.0.0-rc.0` +- Upgraded to `tauri-utils@2.0.0-rc.0` + +## \[2.0.0-beta.19] + +### What's Changed + +- [`4c239729c`](https://www.github.com/tauri-apps/tauri/commit/4c239729c3e1b899ecbc6793c3682848e8de1729) ([#10167](https://www.github.com/tauri-apps/tauri/pull/10167) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add support for `test = true` in `generate_context!` macro to skip some code generation that could affect some tests, for now it only skips empedding a plist on macOS. + +### Dependencies + +- Upgraded to `tauri-codegen@2.0.0-beta.19` +- Upgraded to `tauri-utils@2.0.0-beta.19` + +## \[2.0.0-beta.18] + +### New Features + +- [`5b769948a`](https://www.github.com/tauri-apps/tauri/commit/5b769948a81cac333f64c870a470ba6525bd5cd3) ([#9959](https://www.github.com/tauri-apps/tauri/pull/9959)) Add `include_image` macro to help embedding instances of `Image` struct at compile-time in rust to be used with window, menu or tray icons. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.18` +- Upgraded to `tauri-codegen@2.0.0-beta.18` + +## \[2.0.0-beta.17] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.17` +- Upgraded to `tauri-codegen@2.0.0-beta.17` + +### Breaking Changes + +- [`1df5cdeb0`](https://www.github.com/tauri-apps/tauri/commit/1df5cdeb06f5464e0eec4055e21b7b7bc8739eed)([#9858](https://www.github.com/tauri-apps/tauri/pull/9858)) Use `tauri.conf.json > identifier` to set the `PackageName` in Android and `BundleId` in iOS. + +## \[2.0.0-beta.16] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.16` +- Upgraded to `tauri-codegen@2.0.0-beta.16` + +## \[2.0.0-beta.15] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.15` +- Upgraded to `tauri-codegen@2.0.0-beta.15` + +## \[2.0.0-beta.14] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.14` +- Upgraded to `tauri-codegen@2.0.0-beta.14` + +## \[2.0.0-beta.13] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.13` +- Upgraded to `tauri-codegen@2.0.0-beta.13` + +## \[2.0.0-beta.12] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.12` +- Upgraded to `tauri-codegen@2.0.0-beta.12` + +## \[2.0.0-beta.11] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.11` +- Upgraded to `tauri-codegen@2.0.0-beta.11` + +## \[2.0.0-beta.10] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.10` +- Upgraded to `tauri-codegen@2.0.0-beta.10` + +## \[2.0.0-beta.9] + +### New Features + +- [`ba0206d8a`](https://www.github.com/tauri-apps/tauri/commit/ba0206d8a30a9b43ec5090dcaabd1a23baa1420c)([#9141](https://www.github.com/tauri-apps/tauri/pull/9141)) The `Context` codegen now accepts a `assets` input to define a custom `tauri::Assets` implementation. + +### Dependencies + +- Upgraded to `tauri-codegen@2.0.0-beta.9` +- Upgraded to `tauri-utils@2.0.0-beta.9` + +## \[2.0.0-beta.8] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.8` +- Upgraded to `tauri-codegen@2.0.0-beta.8` + +## \[2.0.0-beta.7] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.7` +- Upgraded to `tauri-codegen@2.0.0-beta.7` + +## \[2.0.0-beta.6] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.6` +- Upgraded to `tauri-codegen@2.0.0-beta.6` + +## \[2.0.0-beta.5] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.5` +- Upgraded to `tauri-codegen@2.0.0-beta.5` + +## \[2.0.0-beta.4] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.4` +- Upgraded to `tauri-codegen@2.0.0-beta.4` + +## \[2.0.0-beta.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.3` +- Upgraded to `tauri-codegen@2.0.0-beta.3` + +## \[2.0.0-beta.2] + +### Enhancements + +- [`8d16a80d`](https://www.github.com/tauri-apps/tauri/commit/8d16a80d2fb2468667e7987d0cc99dbc7e3b9d0a)([#8802](https://www.github.com/tauri-apps/tauri/pull/8802)) The `generate_context` proc macro now accepts a `capabilities` attribute where the value is an array of file paths that can be [conditionally compiled](https://doc.rust-lang.org/reference/conditional-compilation.html). These capabilities are added to the application along the capabilities defined in the Tauri configuration file. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.2` +- Upgraded to `tauri-codegen@2.0.0-beta.2` + +## \[2.0.0-beta.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.1` +- Upgraded to `tauri-codegen@2.0.0-beta.1` + +## \[2.0.0-beta.0] + +### New Features + +- [`74a2a603`](https://www.github.com/tauri-apps/tauri/commit/74a2a6036a5e57462f161d728cbd8a6f121028ca)([#8661](https://www.github.com/tauri-apps/tauri/pull/8661)) Implement access control list for IPC usage. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.0` +- Upgraded to `tauri-codegen@2.0.0-beta.0` + +## \[2.0.0-alpha.13] + +### Enhancements + +- [`d621d343`](https://www.github.com/tauri-apps/tauri/commit/d621d3437ce3947175eecf345b2c6d1c4c7ce020)([#8607](https://www.github.com/tauri-apps/tauri/pull/8607)) Added tracing for window startup, plugins, `Window::eval`, events, IPC, updater and custom protocol request handlers behind the `tracing` feature flag. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.13` +- Upgraded to `tauri-codegen@2.0.0-alpha.13` + +## \[2.0.0-alpha.12] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.12` +- Upgraded to `tauri-codegen@2.0.0-alpha.12` + +## \[2.0.0-alpha.11] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.11` +- Upgraded to `tauri-codegen@2.0.0-alpha.11` +- [`b6ed4ecb`](https://www.github.com/tauri-apps/tauri/commit/b6ed4ecb3730c346c973f1b34aa0de1b7d43ac44)([#7634](https://www.github.com/tauri-apps/tauri/pull/7634)) Update to syn v2. + +## \[2.0.0-alpha.10] + +### Enhancements + +- [`c6c59cf2`](https://www.github.com/tauri-apps/tauri/commit/c6c59cf2373258b626b00a26f4de4331765dd487) Pull changes from Tauri 1.5 release. + +### Dependencies + +- Upgraded to `tauri-codegen@2.0.0-alpha.10` +- Upgraded to `tauri-utils@2.0.0-alpha.10` + +## \[2.0.0-alpha.9] + +### New Features + +- [`880266a7`](https://www.github.com/tauri-apps/tauri/commit/880266a7f697e1fe58d685de3bb6836ce5251e92)([#8031](https://www.github.com/tauri-apps/tauri/pull/8031)) Bump the MSRV to 1.70. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.9` +- Upgraded to `tauri-codegen@2.0.0-alpha.9` + +### Breaking Changes + +- [`ebcc21e4`](https://www.github.com/tauri-apps/tauri/commit/ebcc21e4b95f4e8c27639fb1bca545b432f52d5e)([#8057](https://www.github.com/tauri-apps/tauri/pull/8057)) Renamed the beforeDevCommand, beforeBuildCommand and beforeBundleCommand hooks environment variables from `TAURI_PLATFORM, TAURI_ARCH, TAURI_FAMILY, TAURI_PLATFORM_VERSION, TAURI_PLATFORM_TYPE and TAURI_DEBUG` to `TAURI_ENV_PLATFORM, TAURI_ENV_ARCH, TAURI_ENV_FAMILY, TAURI_ENV_PLATFORM_VERSION, TAURI_ENV_PLATFORM_TYPE and TAURI_ENV_DEBUG` to differentiate the prefix with other CLI environment variables. + +## \[2.0.0-alpha.8] + +### Enhancements + +- [`100d9ede`](https://www.github.com/tauri-apps/tauri/commit/100d9ede35995d9db21d2087dd5606adfafb89a5)([#7802](https://www.github.com/tauri-apps/tauri/pull/7802)) Use `Target` enum from `tauri_utils::platform`. + +### Dependencies + +- Upgraded to `tauri-codegen@2.0.0-alpha.8` +- Upgraded to `tauri-utils@2.0.0-alpha.8` + +## \[2.0.0-alpha.7] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.7` +- Upgraded to `tauri-codegen@2.0.0-alpha.7` + +### Breaking Changes + +- [`fbeb5b91`](https://www.github.com/tauri-apps/tauri/commit/fbeb5b9185baeda19e865228179e3e44c165f1d9)([#7170](https://www.github.com/tauri-apps/tauri/pull/7170)) Moved `tauri::api::ipc` to `tauri::ipc` and refactored all types. + +## \[2.0.0-alpha.6] + +### Dependencies + +- Updated to latest `tauri-utils` + +## \[2.0.0-alpha.5] + +- [`7a4b1fb9`](https://www.github.com/tauri-apps/tauri/commit/7a4b1fb96da475053c61960f362bbecf18cd00d4)([#6839](https://www.github.com/tauri-apps/tauri/pull/6839)) Added support to attibutes for each command path in the `generate_handler` macro. +- [`96639ca2`](https://www.github.com/tauri-apps/tauri/commit/96639ca239c9e4f75142fc07868ac46822111cff)([#6749](https://www.github.com/tauri-apps/tauri/pull/6749)) Moved the `shell` functionality to its own plugin in the plugins-workspace repository. +- [`3188f376`](https://www.github.com/tauri-apps/tauri/commit/3188f3764978c6d1452ee31d5a91469691e95094)([#6883](https://www.github.com/tauri-apps/tauri/pull/6883)) Bump the MSRV to 1.65. +- [`9a79dc08`](https://www.github.com/tauri-apps/tauri/commit/9a79dc085870e0c1a5df13481ff271b8c6cc3b78)([#6947](https://www.github.com/tauri-apps/tauri/pull/6947)) Removed the module command macros. + +## \[2.0.0-alpha.4] + +- Added `android` configuration object under `tauri > bundle`. + - Bumped due to a bump in tauri-utils. + - [db4c9dc6](https://www.github.com/tauri-apps/tauri/commit/db4c9dc655e07ee2184fe04571f500f7910890cd) feat(core): add option to configure Android's minimum SDK version ([#6651](https://www.github.com/tauri-apps/tauri/pull/6651)) on 2023-04-07 + +## \[2.0.0-alpha.3] + +- Pull changes from Tauri 1.3 release. + - [](https://www.github.com/tauri-apps/tauri/commit/undefined) on undefined + +## \[2.0.0-alpha.2] + +- Resolve Android package name from single word bundle identifiers. + - [60a8b07d](https://www.github.com/tauri-apps/tauri/commit/60a8b07dc7c56c9c45331cb57d9afb410e7eadf3) fix: handle single word bundle identifier when resolving Android domain ([#6313](https://www.github.com/tauri-apps/tauri/pull/6313)) on 2023-02-19 +- Return `bool` in the invoke handler. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 +- Refactored the implementation of the `mobile_entry_point` macro. + - [9feab904](https://www.github.com/tauri-apps/tauri/commit/9feab904bf08b5c168d4779c21d0419409a68d30) feat(core): add API to call Android plugin ([#6239](https://www.github.com/tauri-apps/tauri/pull/6239)) on 2023-02-10 + +## \[2.0.0-alpha.1] + +- Refactor mobile environment variables. + - [dee9460f](https://www.github.com/tauri-apps/tauri/commit/dee9460f9c9bc92e9c638e7691e616849ac2085b) feat: keep CLI alive when iOS app exits, show logs, closes [#5855](https://www.github.com/tauri-apps/tauri/pull/5855) ([#5902](https://www.github.com/tauri-apps/tauri/pull/5902)) on 2022-12-27 +- Bump the MSRV to 1.64. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Removed mobile logging initialization, which will be handled by `tauri-plugin-log`. + - [](https://www.github.com/tauri-apps/tauri/commit/undefined) on undefined + +## \[2.0.0-alpha.0] + +- Added the `mobile_entry_point` macro. + - [98904863](https://www.github.com/tauri-apps/tauri/commit/9890486321c9c79ccfb7c547fafee85b5c3ffa71) feat(core): add `mobile_entry_point` macro ([#4983](https://www.github.com/tauri-apps/tauri/pull/4983)) on 2022-08-21 +- First mobile alpha release! + - [fa3a1098](https://www.github.com/tauri-apps/tauri/commit/fa3a10988a03aed1b66fb17d893b1a9adb90f7cd) feat(ci): prepare 2.0.0-alpha.0 ([#5786](https://www.github.com/tauri-apps/tauri/pull/5786)) on 2022-12-08 + +## \[1.4.3] + +### Dependencies + +- Upgraded to `tauri-utils@1.5.2` +- Upgraded to `tauri-codegen@1.4.2` + +## \[1.4.2] + +### Enhancements + +- [`5e05236b`](https://www.github.com/tauri-apps/tauri/commit/5e05236b4987346697c7caae0567d3c50714c198)([#8289](https://www.github.com/tauri-apps/tauri/pull/8289)) Added tracing for window startup, plugins, `Window::eval`, events, IPC, updater and custom protocol request handlers behind the `tracing` feature flag. + +## \[1.4.1] + +### Dependencies + +- Upgraded to `tauri-utils@1.5.0` +- Upgraded to `tauri-codegen@1.4.1` + +## \[1.4.0] + +### Enhancements + +- [`d68a25e3`](https://www.github.com/tauri-apps/tauri/commit/d68a25e32e012e57a9e5225b589b9ecbea70a887)([#6124](https://www.github.com/tauri-apps/tauri/pull/6124)) Improve compiler error message when generating an async command that has a reference input and don't return a Result. + +## \[1.3.0] + +- Bump minimum supported Rust version to 1.60. + - [5fdc616d](https://www.github.com/tauri-apps/tauri/commit/5fdc616df9bea633810dcb814ac615911d77222c) feat: Use the zbus-backed of notify-rust ([#6332](https://www.github.com/tauri-apps/tauri/pull/6332)) on 2023-03-31 + +## \[1.2.1] + +- Fix `allowlist > app > show/hide` always disabled when `allowlist > app > all: false`. + - Bumped due to a bump in tauri-utils. + - [bb251087](https://www.github.com/tauri-apps/tauri/commit/bb2510876d0bdff736d36bf3a465cdbe4ad2b90c) fix(core): extend allowlist with `app`'s allowlist, closes [#5650](https://www.github.com/tauri-apps/tauri/pull/5650) ([#5652](https://www.github.com/tauri-apps/tauri/pull/5652)) on 2022-11-18 + +## \[1.2.0] + +- - [7d9aa398](https://www.github.com/tauri-apps/tauri/commit/7d9aa3987efce2d697179ffc33646d086c68030c) feat: bump MSRV to 1.59 ([#5296](https://www.github.com/tauri-apps/tauri/pull/5296)) on 2022-09-28 + +## \[1.1.1] + +- Add missing allowlist config for `set_cursor_grab`, `set_cursor_visible`, `set_cursor_icon` and `set_cursor_position` APIs. + - Bumped due to a bump in tauri-utils. + - [c764408d](https://www.github.com/tauri-apps/tauri/commit/c764408da7fae123edd41115bda42fa75a4731d2) fix: Add missing allowlist config for cursor apis, closes [#5207](https://www.github.com/tauri-apps/tauri/pull/5207) ([#5211](https://www.github.com/tauri-apps/tauri/pull/5211)) on 2022-09-16 + +## \[1.1.0] + +- Added support to configuration files in TOML format (Tauri.toml file). + - [ae83d008](https://www.github.com/tauri-apps/tauri/commit/ae83d008f9e1b89bfc8dddaca42aa5c1fbc36f6d) feat: add support to TOML config file `Tauri.toml`, closes [#4806](https://www.github.com/tauri-apps/tauri/pull/4806) ([#4813](https://www.github.com/tauri-apps/tauri/pull/4813)) on 2022-08-02 + +## \[1.0.4] + +- Adjust command imports to fix `items_after_statements` Clippy warning. + - [d3e19e34](https://www.github.com/tauri-apps/tauri/commit/d3e19e3420a023cddc46173a2d1f1e6c5a390a1b) fix(macros): `items_after_statements` Clippy warning, closes [#4639](https://www.github.com/tauri-apps/tauri/pull/4639) on 2022-07-11 +- Remove raw identifier (`r#`) prefix from command arguments. + - [ac72800f](https://www.github.com/tauri-apps/tauri/commit/ac72800fb630738e2502569935ecdc83e3e78055) fix(macros): strip `r#` from command arguments, closes [#4654](https://www.github.com/tauri-apps/tauri/pull/4654) ([#4657](https://www.github.com/tauri-apps/tauri/pull/4657)) on 2022-07-12 + +## \[1.0.3] + +- Add `#[doc(hidden)]` attribute to the `#[command]` generated macro. + - [d4cdf807](https://www.github.com/tauri-apps/tauri/commit/d4cdf80781a7955ac620fe6d394e82d067178c4d) feat(macros): hide command macro from docs, closes [#4550](https://www.github.com/tauri-apps/tauri/pull/4550) ([#4556](https://www.github.com/tauri-apps/tauri/pull/4556)) on 2022-07-01 + +## \[1.0.2] + +- Expose `platform::windows_version` function. + - Bumped due to a bump in tauri-utils. + - [bf764e83](https://www.github.com/tauri-apps/tauri/commit/bf764e83e01e7443e6cc54572001e1c98c357465) feat(utils): expose `windows_version` function ([#4534](https://www.github.com/tauri-apps/tauri/pull/4534)) on 2022-06-30 + +## \[1.0.1] + +- Set the bundle name and app metadata in the Info.plist file in development mode. + - [38f5db6e](https://www.github.com/tauri-apps/tauri/commit/38f5db6e6a8b496b50d486db6fd8204266de3a69) feat(codegen): fill app metadata in development Info.plist on 2022-06-21 +- Set the application icon in development mode on macOS. + - [307c2ebf](https://www.github.com/tauri-apps/tauri/commit/307c2ebfb68238dacab6088f9c6ba310c727c68c) feat(core): set macOS app icon in development ([#4385](https://www.github.com/tauri-apps/tauri/pull/4385)) on 2022-06-19 + +## \[1.0.0] + +- Upgrade to `stable`! + - [f4bb30cc](https://www.github.com/tauri-apps/tauri/commit/f4bb30cc73d6ba9b9ef19ef004dc5e8e6bb901d3) feat(covector): prepare for v1 ([#4351](https://www.github.com/tauri-apps/tauri/pull/4351)) on 2022-06-15 + +## \[1.0.0-rc.11] + +- Read the tray icon path relatively to the config directory. + - Bumped due to a bump in tauri-codegen. + - [562e8ca2](https://www.github.com/tauri-apps/tauri/commit/562e8ca23facf1a8e5fa6c8cdf872357d3523a78) fix(codegen): tray icon path is relative to the config directory on 2022-06-15 + +## \[1.0.0-rc.10] + +- **Breaking change:** The `TrayIcon` enum has been removed and now `Icon` is used instead. + This allows you to use more image formats and use embedded icons on Linux. + - Bumped due to a bump in tauri-codegen. + - [4ce8e228](https://www.github.com/tauri-apps/tauri/commit/4ce8e228134cd3f22973b74ef26ca0d165fbbbd9) refactor(core): use `Icon` for tray icons ([#4342](https://www.github.com/tauri-apps/tauri/pull/4342)) on 2022-06-14 + +## \[1.0.0-rc.9] + +- Added a config flag to bundle the media framework used by webkit2gtk `tauri.conf.json > tauri > bundle > appimage > bundleMediaFramework`. + - Bumped due to a bump in tauri-utils. + - [d335fae9](https://www.github.com/tauri-apps/tauri/commit/d335fae92cdcbb0ee18aad4e54558914afa3e778) feat(bundler): bundle additional gstreamer files, closes [#4092](https://www.github.com/tauri-apps/tauri/pull/4092) ([#4271](https://www.github.com/tauri-apps/tauri/pull/4271)) on 2022-06-10 + +## \[1.0.0-rc.8] + +- **Breaking change:** `PackageInfo::version` is now a `semver::Version` instead of a `String`. + - Bumped due to a bump in tauri-utils. + - [2badbd2d](https://www.github.com/tauri-apps/tauri/commit/2badbd2d7ed51bf33c1b547b4c837b600574bd4a) refactor: force semver versions, change updater `should_install` sig ([#4215](https://www.github.com/tauri-apps/tauri/pull/4215)) on 2022-05-25 + - [a7388e23](https://www.github.com/tauri-apps/tauri/commit/a7388e23c3b9019d48b078cae00a75c74d74d11b) fix(ci): adjust change file to include tauri-utils and tauri-codegen on 2022-05-27 + +## \[1.0.0-rc.7] + +- Allow configuring the display options for the MSI execution allowing quieter updates. + - Bumped due to a bump in tauri-utils. + - [9f2c3413](https://www.github.com/tauri-apps/tauri/commit/9f2c34131952ea83c3f8e383bc3cec7e1450429f) feat(core): configure msiexec display options, closes [#3951](https://www.github.com/tauri-apps/tauri/pull/3951) ([#4061](https://www.github.com/tauri-apps/tauri/pull/4061)) on 2022-05-15 + +## \[1.0.0-rc.6] + +- Added `$schema` support to `tauri.conf.json`. + - Bumped due to a bump in tauri-utils. + - [715cbde3](https://www.github.com/tauri-apps/tauri/commit/715cbde3842a916c4ebeab2cab348e1774b5c192) feat(config): add `$schema` to `tauri.conf.json`, closes [#3464](https://www.github.com/tauri-apps/tauri/pull/3464) ([#4031](https://www.github.com/tauri-apps/tauri/pull/4031)) on 2022-05-03 +- The `dangerous_allow_asset_csp_modification` configuration value has been changed to allow a list of CSP directives to disable. + - Bumped due to a bump in tauri-utils. + - [164078c0](https://www.github.com/tauri-apps/tauri/commit/164078c0b719ccbc12e956fecf8a7d4a3c5044e1) feat: allow limiting dangerousDisableAssetCspModification, closes [#3831](https://www.github.com/tauri-apps/tauri/pull/3831) ([#4021](https://www.github.com/tauri-apps/tauri/pull/4021)) on 2022-05-02 + +## \[1.0.0-rc.5] + +- Read platform-specific configuration files when generating code without the `TAURI_CONFIG` env var. + - Bumped due to a bump in tauri-codegen. + - [edf85bc1](https://www.github.com/tauri-apps/tauri/commit/edf85bc1d18450c92aee17f7f99c163abe432ebd) fix(codegen): read platform-specific config file ([#3966](https://www.github.com/tauri-apps/tauri/pull/3966)) on 2022-04-25 + +## \[1.0.0-rc.4] + +- Replace multiple dependencies who's C code compiled concurrently and caused + the other ones to bloat compile time significantly. + +- `zstd` -> `brotli` + +- `blake3` -> a vendored version of the blake3 reference + +- `ring` -> `getrandom` + +See for more information about +these specific choices. + +- [8661e3e2](https://www.github.com/tauri-apps/tauri/commit/8661e3e24d96c399bfbcdee5d8e9d6beba2265a7) replace dependencies with long build times when used together (closes [#3571](https://www.github.com/tauri-apps/tauri/pull/3571)) ([#3773](https://www.github.com/tauri-apps/tauri/pull/3773)) on 2022-03-27 + +## \[1.0.0-rc.3] + +- Parse window icons at compile time. + - Bumped due to a bump in tauri-codegen. + - [8c935872](https://www.github.com/tauri-apps/tauri/commit/8c9358725a17dcc2acaf4d10c3f654afdff586b0) refactor(core): move `png` and `ico` behind Cargo features ([#3588](https://www.github.com/tauri-apps/tauri/pull/3588)) on 2022-03-05 + +## \[1.0.0-rc.2] + +- Changed the default value for `tauri > bundle > macOS > minimumSystemVersion` to `10.13`. + - Bumped due to a bump in tauri-utils. + - [fce344b9](https://www.github.com/tauri-apps/tauri/commit/fce344b90b7227f8f5514853c2f885fb24d3648e) feat(core): set default value for `minimum_system_version` to 10.13 ([#3497](https://www.github.com/tauri-apps/tauri/pull/3497)) on 2022-02-17 + +## \[1.0.0-rc.1] + +- Change default value for the `freezePrototype` configuration to `false`. + - Bumped due to a bump in tauri-utils. + - [3a4c0160](https://www.github.com/tauri-apps/tauri/commit/3a4c01606184be762adee055ddac803de0d28527) fix(core): change default `freezePrototype` to false, closes [#3416](https://www.github.com/tauri-apps/tauri/pull/3416) [#3406](https://www.github.com/tauri-apps/tauri/pull/3406) ([#3423](https://www.github.com/tauri-apps/tauri/pull/3423)) on 2022-02-12 + +## \[1.0.0-rc.0] + +- Adds support for using JSON5 format for the `tauri.conf.json` file, along with also supporting the `.json5` extension. + +Here is the logic flow that determines if JSON or JSON5 will be used to parse the config: + +1. Check if `tauri.conf.json` exists + a. Parse it with `serde_json` + b. Parse it with `json5` if `serde_json` fails + c. Return original `serde_json` error if all above steps failed +2. Check if `tauri.conf.json5` exists + a. Parse it with `json5` + b. Return error if all above steps failed +3. Return error if all above steps failed + +- [995de57a](https://www.github.com/tauri-apps/tauri/commit/995de57a76cf51215277673e526d7ec32b86b564) Add seamless support for using JSON5 in the config file ([#47](https://www.github.com/tauri-apps/tauri/pull/47)) on 2022-02-03 +- The minimum Rust version is now `1.56`. + - [a9dfc015](https://www.github.com/tauri-apps/tauri/commit/a9dfc015505afe91281c2027954ffcc588b1a59c) feat: update to edition 2021 and set minimum rust to 1.56 ([#2789](https://www.github.com/tauri-apps/tauri/pull/2789)) on 2021-10-22 + +## \[1.0.0-beta.5] + +- Embed Info.plist file contents on binary on dev. + - Bumped due to a bump in tauri-codegen. + - [537ab1b6](https://www.github.com/tauri-apps/tauri/commit/537ab1b6d5a792c550a535619965c9e4126292e6) feat(core): inject src-tauri/Info.plist file on dev and merge on bundle, closes [#1570](https://www.github.com/tauri-apps/tauri/pull/1570) [#2338](https://www.github.com/tauri-apps/tauri/pull/2338) ([#2444](https://www.github.com/tauri-apps/tauri/pull/2444)) on 2021-08-15 +- Fix ES Module detection for default imports with relative paths or scoped packages and exporting of async functions. + - Bumped due to a bump in tauri-codegen. + - [b2b36cfe](https://www.github.com/tauri-apps/tauri/commit/b2b36cfe8dfcccb341638a4cb6dc23a514c54148) fix(core): fixes ES Module detection for default imports with relative paths or scoped packages ([#2380](https://www.github.com/tauri-apps/tauri/pull/2380)) on 2021-08-10 + - [fbf8caf5](https://www.github.com/tauri-apps/tauri/commit/fbf8caf5c419cb4fc3d123be910e094a8e8c4bef) fix(core): ESM detection when using `export async function` ([#2425](https://www.github.com/tauri-apps/tauri/pull/2425)) on 2021-08-14 + +## \[1.0.0-beta.4] + +- `Params` has been removed, along with all the associated types on it. Functions that previously accepted those + associated types now accept strings instead. Type that used a generic parameter `Params` now use `Runtime` instead. If + you use the `wry` feature, then types with a `Runtime` generic parameter should default to `Wry`, letting you omit the + explicit type and let the compiler infer it instead. + +`tauri`: + +- See `Params` note +- If you were using `Params` inside a function parameter or definition, all references to it have been replaced with a + simple runtime that defaults to `Wry`. If you are not using a custom runtime, just remove `Params` from the definition + of functions/items that previously took it. If you are using a custom runtime, you *may* need to pass the runtime type + to these functions. +- If you were using custom types for `Params` (uncommon and if you don't understand you probably were not using it), all + methods that were previously taking the custom type now takes an `Into` or a `&str`. The types were already + required to be string-able, so just make sure to convert it into a string before passing it in if this breaking change + affects you. + +`tauri-macros`: + +- (internal) Added private `default_runtime` proc macro to allow us to give item definitions a custom runtime only when + the specified feature is enabled. + +`tauri-runtime`: + +- See `Params` note +- Removed `Params`, `MenuId`, `Tag`, `TagRef`. +- Added `menu::{MenuHash, MenuId, MenuIdRef}` as type aliases for the internal type that menu types now use. + - All previous menu items that had a `MenuId` generic now use the underlying `MenuId` type without a generic. +- `Runtime`, `RuntimeHandle`, and `Dispatch` have no more generic parameter on `create_window(...)` and instead use the + `Runtime` type directly +- `Runtime::system_tray` has no more `MenuId` generic and uses the string based `SystemTray` type directly. +- (internal) `CustomMenuItem::id_value()` is now hashed on creation and exposed as the `id` field with type `MenuHash`. + +`tauri-runtime-wry`: + +- See `Params` note +- update menu and runtime related types to the ones changed in `tauri-runtime`. + +`tauri-utils`: + +- `Assets::get` signature has changed to take a `&AssetKey` instead of `impl Into` to become trait object + safe. +- [fd8fab50](https://www.github.com/tauri-apps/tauri/commit/fd8fab507c8fa1b113b841af14c6693eb3955f6b) refactor(core): remove `Params` and replace with strings ([#2191](https://www.github.com/tauri-apps/tauri/pull/2191)) on 2021-07-15 + +## \[1.0.0-beta.3] + +- Detect ESM scripts and inject the invoke key directly instead of using an IIFE. + - Bumped due to a bump in tauri-codegen. + - [7765c7fa](https://www.github.com/tauri-apps/tauri/commit/7765c7fa281853ddfb26b6b17534df95eaede804) fix(core): invoke key injection on ES module, improve performance ([#2094](https://www.github.com/tauri-apps/tauri/pull/2094)) on 2021-06-27 +- Improve invoke key code injection performance time rewriting code at compile time. + - Bumped due to a bump in tauri-codegen. + - [7765c7fa](https://www.github.com/tauri-apps/tauri/commit/7765c7fa281853ddfb26b6b17534df95eaede804) fix(core): invoke key injection on ES module, improve performance ([#2094](https://www.github.com/tauri-apps/tauri/pull/2094)) on 2021-06-27 + +## \[1.0.0-beta.2] + +- internal: Refactor all macro code that expects specific bindings to be passed Idents + - [39f8f269](https://www.github.com/tauri-apps/tauri/commit/39f8f269164d2fda3d5b614a193b12bb266e4b4b) refactor(macros): explicitly pass idents ([#1812](https://www.github.com/tauri-apps/tauri/pull/1812)) on 2021-05-13 + +## \[1.0.0-beta.1] + +- Fixes a name collision when the command function is named `invoke`. + - [7862ec5](https://www.github.com/tauri-apps/tauri/commit/7862ec562fa70e3733263ce1f690d6cd2943c0b4) fix(macros): change invoke binding in generate handler ([#1804](https://www.github.com/tauri-apps/tauri/pull/1804)) on 2021-05-12 +- Fixes a name collision when the command function is named `message` or `resolver`. + - [0b87532](https://www.github.com/tauri-apps/tauri/commit/0b875327067ca825ff6f6f26c9b2ce6fcb001e79) fix(macros): fix rest of command collisions ([#1805](https://www.github.com/tauri-apps/tauri/pull/1805)) on 2021-05-12 +- Fixes a name collision when the command function is named `cmd`. + - [d36b726](https://www.github.com/tauri-apps/tauri/commit/d36b7269261d329dd7d7fcd4d5098f3fca167364) fix(macros): collision when command is named `cmd` ([#1802](https://www.github.com/tauri-apps/tauri/pull/1802)) on 2021-05-12 + +## \[1.0.0-beta.0] + +- Only commands with a `async fn` are executed on a separate task. `#[command] fn command_name` runs on the main thread. + - [bb8dafb](https://www.github.com/tauri-apps/tauri/commit/bb8dafbe1ea6edde7385631d41ac05e96a3309ef) feat(core): #\[command] return with autoref specialization workaround fix [#1672](https://www.github.com/tauri-apps/tauri/pull/1672) ([#1734](https://www.github.com/tauri-apps/tauri/pull/1734)) on 2021-05-09 +- `#[command]` now generates a macro instead of a function to allow passing through `Params` and other generics. + `generate_handler!` has been changed to consume the generated `#[command]` macro + - [1453d4b](https://www.github.com/tauri-apps/tauri/commit/1453d4bf842ed6891ec604e0635344c930282189) feat(core): support generics (especially Param) in #\[command] ([#1622](https://www.github.com/tauri-apps/tauri/pull/1622)) on 2021-05-05 +- Improves support for commands returning `Result`. + - [bb8dafb](https://www.github.com/tauri-apps/tauri/commit/bb8dafbe1ea6edde7385631d41ac05e96a3309ef) feat(core): #\[command] return with autoref specialization workaround fix [#1672](https://www.github.com/tauri-apps/tauri/pull/1672) ([#1734](https://www.github.com/tauri-apps/tauri/pull/1734)) on 2021-05-09 +- Adds support to command state, triggered when a command argument is `arg: State<'_, StateType>`. + - [8b6f3de](https://www.github.com/tauri-apps/tauri/commit/8b6f3de0ad47684e72a2ae5f884d8675acfaeeac) feat(core): add state management, closes [#1655](https://www.github.com/tauri-apps/tauri/pull/1655) ([#1665](https://www.github.com/tauri-apps/tauri/pull/1665)) on 2021-05-02 + +## \[1.0.0-beta-rc.1] + +- Fixes the Message `command` name value on plugin invoke handler. + - [422dd5e](https://www.github.com/tauri-apps/tauri/commit/422dd5e2a0a03bb1556915c78f110bfab092c874) fix(core): command name on plugin invoke handler ([#1577](https://www.github.com/tauri-apps/tauri/pull/1577)) on 2021-04-21 + - [f575aaa](https://www.github.com/tauri-apps/tauri/commit/f575aaad71f23d44b2f89cf9ee5d84817dc3bb7a) fix: change files not referencing core packages ([#1619](https://www.github.com/tauri-apps/tauri/pull/1619)) on 2021-04-25 + +## \[1.0.0-beta-rc.0] + +- Update all code files to have our license header. + - [bf82136](https://www.github.com/tauri-apps/tauri/commit/bf8213646689175f8a158b956911f3a43e360690) feat(license): SPDX Headers ([#1449](https://www.github.com/tauri-apps/tauri/pull/1449)) on 2021-04-11 + - [a6def70](https://www.github.com/tauri-apps/tauri/commit/a6def7066eec19c889b0f14cc1e475bf209a332e) Refactor(tauri): move tauri-api and tauri-updater to tauri ([#1455](https://www.github.com/tauri-apps/tauri/pull/1455)) on 2021-04-11 + - [aea6145](https://www.github.com/tauri-apps/tauri/commit/aea614587bddab930d552512b54e18624fbf573e) refactor(repo): add /tooling folder ([#1457](https://www.github.com/tauri-apps/tauri/pull/1457)) on 2021-04-12 +- Added new macros to simplify the creation of commands that can be called by the webview. + - [1f2e7a3](https://www.github.com/tauri-apps/tauri/commit/1f2e7a3226ccf0ee3e30ae0cba3c67f7e219d1f2) feat(core): improved command matching with macros, fixes [#1157](https://www.github.com/tauri-apps/tauri/pull/1157) ([#1301](https://www.github.com/tauri-apps/tauri/pull/1301)) on 2021-02-28 diff --git a/crates/tauri-macros/Cargo.toml b/crates/tauri-macros/Cargo.toml new file mode 100644 index 0000000..269967a --- /dev/null +++ b/crates/tauri-macros/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "tauri-macros" +version = "2.6.3" +description = "Macros for the tauri crate." +exclude = ["CHANGELOG.md", "/target"] +authors.workspace = true +homepage.workspace = true +repository.workspace = true +categories.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = { version = "1", features = ["span-locations"] } +quote = "1" +syn = { version = "2", features = ["full"] } +heck = "0.5" +tauri-codegen = { workspace = true } +tauri-utils = { workspace = true } + +[features] +custom-protocol = [] +compression = ["tauri-codegen/compression"] +isolation = ["tauri-codegen/isolation"] +config-json5 = ["tauri-codegen/config-json5", "tauri-utils/config-json5"] +config-toml = ["tauri-codegen/config-toml", "tauri-utils/config-toml"] +tracing = [] + +[lints] +workspace = true diff --git a/crates/tauri-macros/LICENSE-APACHE-2.0 b/crates/tauri-macros/LICENSE-APACHE-2.0 new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/crates/tauri-macros/LICENSE-APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/tauri-macros/LICENSE-MIT b/crates/tauri-macros/LICENSE-MIT new file mode 100644 index 0000000..b08530d --- /dev/null +++ b/crates/tauri-macros/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/tauri-macros/README.md b/crates/tauri-macros/README.md new file mode 100644 index 0000000..98cd9c7 --- /dev/null +++ b/crates/tauri-macros/README.md @@ -0,0 +1,42 @@ +# tauri-macros + + + +[![status](https://img.shields.io/badge/status-stable-blue.svg)](https://github.com/tauri-apps/tauri/tree/dev) +[![License](https://img.shields.io/badge/License-MIT%20or%20Apache%202-green.svg)](https://opencollective.com/tauri) +[![test core](https://img.shields.io/github/actions/workflow/status/tauri-apps/tauri/test-core.yml?label=test%20core&logo=github)](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri?ref=badge_shield) +[![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/SpmNs4S) +[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app) +[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation) +[![support](https://img.shields.io/badge/sponsor-Open%20Collective-blue.svg)](https://opencollective.com/tauri) + +| Component | Version | +| ------------ | ------------------------------------------------------------------------------------------------------------ | +| tauri-macros | [![](https://img.shields.io/crates/v/tauri-macros?style=flat-square)](https://crates.io/crates/tauri-macros) | + +## About Tauri + +Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily. + +Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task. + +## This module + +Create macros for the context, handler, and commands by leveraging the `tauri-codegen` crate. + +To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document. + +## Semver + +**tauri** is following [Semantic Versioning 2.0](https://semver.org/). + +## Licenses + +Code: (c) 2021 - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. + +Logo: CC-BY-NC-ND + +- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum) diff --git a/crates/tauri-macros/src/command/handler.rs b/crates/tauri-macros/src/command/handler.rs new file mode 100644 index 0000000..9ce4e2d --- /dev/null +++ b/crates/tauri-macros/src/command/handler.rs @@ -0,0 +1,185 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use quote::{format_ident, quote}; +use syn::{ + Attribute, Ident, Path, Token, + parse::{Parse, ParseBuffer, ParseStream}, +}; + +struct CommandDef { + path: Path, + attrs: Vec, +} + +impl Parse for CommandDef { + fn parse(input: ParseStream) -> syn::Result { + let attrs = input.call(Attribute::parse_outer)?; + let path = input.parse()?; + + Ok(CommandDef { path, attrs }) + } +} + +/// The items parsed from [`generate_handle!`](crate::generate_handle). +pub struct Handler { + command_defs: Vec, + commands: Vec, + wrappers: Vec, +} + +impl Parse for Handler { + fn parse(input: &ParseBuffer<'_>) -> syn::Result { + let plugin_name = try_get_plugin_name(input)?; + + let mut command_defs = input + .parse_terminated(CommandDef::parse, Token![,])? + .into_iter() + .collect(); + + filter_unused_commands(plugin_name, &mut command_defs); + let mut commands = Vec::new(); + let mut wrappers = Vec::new(); + + // parse the command names and wrappers from the passed paths + for command_def in &command_defs { + let mut wrapper = command_def.path.clone(); + let last = super::path_to_command(&mut wrapper); + + // the name of the actual command function + let command = last.ident.clone(); + + // set the path to the command function wrapper + last.ident = super::format_command_wrapper(&command); + + commands.push(command); + wrappers.push(wrapper); + } + + Ok(Self { + command_defs, + commands, + wrappers, + }) + } +} + +/// Try to get the plugin name by parsing the input for a `#![plugin(...)]` attribute, +/// if it's not present, try getting it from `CARGO_PKG_NAME` environment variable +fn try_get_plugin_name(input: &ParseBuffer<'_>) -> Result, syn::Error> { + if let Ok(attrs) = input.call(Attribute::parse_inner) { + for attr in attrs { + if attr.path().is_ident("plugin") { + // Parse the content inside #![plugin(...)] + let plugin_name = attr.parse_args::()?.to_string(); + return Ok(Some(if plugin_name == "__TAURI_CHANNEL__" { + // TODO: Remove this in v3 + plugin_name + } else { + plugin_name.replace("_", "-") + })); + } + } + } + Ok( + std::env::var("CARGO_PKG_NAME") + .ok() + .and_then(|var| var.strip_prefix("tauri-plugin-").map(String::from)), + ) +} + +fn filter_unused_commands(plugin_name: Option, command_defs: &mut Vec) { + let allowed_commands = tauri_utils::acl::read_allowed_commands(); + let Some(allowed_commands) = allowed_commands else { + return; + }; + + // TODO: Remove this in v3 + if plugin_name.as_deref() == Some("__TAURI_CHANNEL__") { + // Always allowed + return; + } + + if plugin_name.is_none() && !allowed_commands.has_app_acl { + // All application commands are allowed if we don't have an application ACL + // + // note that inline plugins without the #![plugin()] attribute would also get to this check + // which means inline plugins must have an app manifest to get proper unused command removal + return; + } + + let mut unused_commands = Vec::new(); + + let command_prefix = if let Some(plugin_name) = &plugin_name { + format!("plugin:{plugin_name}|") + } else { + "".into() + }; + + command_defs.retain(|command_def| { + let mut wrapper = command_def.path.clone(); + let last = super::path_to_command(&mut wrapper); + + // the name of the actual command function + let command_name = &last.ident; + + let command = format!("{command_prefix}{command_name}"); + let is_allowed = allowed_commands.commands.contains(&command); + + if !is_allowed { + unused_commands.push(command_name.to_string()); + } + + is_allowed + }); + + if !unused_commands.is_empty() { + let plugin_display_name = plugin_name.as_deref().unwrap_or("application"); + let unused_commands_display = unused_commands.join(", "); + println!("Removed unused commands from {plugin_display_name}: {unused_commands_display}",); + } +} + +impl From for proc_macro::TokenStream { + fn from( + Handler { + command_defs, + commands, + wrappers, + }: Handler, + ) -> Self { + let cmd = format_ident!("__tauri_cmd__"); + let invoke = format_ident!("__tauri_invoke__"); + 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)* #command_name_macros => #wrappers!(#paths, #invoke),)* + _ => { + return false; + }, + } + }) + .into() + } +} diff --git a/crates/tauri-macros/src/command/mod.rs b/crates/tauri-macros/src/command/mod.rs new file mode 100644 index 0000000..fc3bbb6 --- /dev/null +++ b/crates/tauri-macros/src/command/mod.rs @@ -0,0 +1,24 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use proc_macro2::Ident; +use syn::{Path, PathSegment}; + +pub use self::{handler::Handler, wrapper::wrapper}; + +mod handler; +mod wrapper; + +/// The autogenerated wrapper ident. +fn format_command_wrapper(function: &Ident) -> Ident { + quote::format_ident!("__cmd__{}", function) +} + +/// This function will panic if the passed [`syn::Path`] does not have any segments. +fn path_to_command(path: &mut Path) -> &mut PathSegment { + path + .segments + .last_mut() + .expect("parsed syn::Path has no segment") +} diff --git a/crates/tauri-macros/src/command/wrapper.rs b/crates/tauri-macros/src/command/wrapper.rs new file mode 100644 index 0000000..0713baf --- /dev/null +++ b/crates/tauri-macros/src/command/wrapper.rs @@ -0,0 +1,524 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::env::var; + +use heck::{ToLowerCamelCase, ToSnakeCase}; +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; +use quote::{format_ident, quote, quote_spanned}; +use syn::{ + Expr, ExprLit, FnArg, ItemFn, Lit, Meta, Pat, Token, Visibility, + ext::IdentExt, + parse::{Parse, ParseStream}, + parse_macro_input, + punctuated::Punctuated, + spanned::Spanned, +}; +use tauri_utils::acl::REMOVE_UNUSED_COMMANDS_ENV_VAR; + +#[allow(clippy::large_enum_variant)] +enum WrapperAttributeKind { + Meta(Meta), + Async, +} + +impl Parse for WrapperAttributeKind { + fn parse(input: ParseStream) -> syn::Result { + match input.parse::() { + Ok(m) => Ok(Self::Meta(m)), + Err(e) => match input.parse::() { + Ok(_) => Ok(Self::Async), + Err(_) => Err(e), + }, + } + } +} + +struct WrapperAttributes { + root: TokenStream2, + execution_context: ExecutionContext, + argument_case: ArgumentCase, + rename: RenamePolicy, +} + +impl Parse for WrapperAttributes { + fn parse(input: ParseStream) -> syn::Result { + let mut wrapper_attributes = WrapperAttributes { + root: quote!(::tauri), + execution_context: ExecutionContext::Blocking, + argument_case: ArgumentCase::Camel, + rename: RenamePolicy::Keep, + }; + + let attrs = Punctuated::::parse_terminated(input)?; + for attr in attrs { + match attr { + WrapperAttributeKind::Meta(Meta::List(_)) => { + 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 { + 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\"", + )); + } + }; + } + } else if v.path.is_ident("rename") { + if let Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) = v.value + { + let lit = s.value(); + wrapper_attributes.rename = RenamePolicy::Rename(quote!(#lit)); + } else { + return Err(syn::Error::new( + v.span(), + "expected string literal for rename", + )); + } + } else if v.path.is_ident("root") { + if let Expr::Lit(ExprLit { + lit: Lit::Str(s), + attrs: _, + }) = v.value + { + let lit = s.value(); + + wrapper_attributes.root = if lit == "crate" { + quote!($crate) + } else { + let ident = Ident::new(&lit, Span::call_site()); + quote!(#ident) + }; + } + } + } + WrapperAttributeKind::Meta(Meta::Path(_)) => { + return Err(syn::Error::new( + input.span(), + "unexpected input, expected one of `rename_all`, `rename`, `root`, `async`", + )); + } + WrapperAttributeKind::Async => { + wrapper_attributes.execution_context = ExecutionContext::Async; + } + } + } + + Ok(wrapper_attributes) + } +} + +/// The execution context of the command. +enum ExecutionContext { + Async, + Blocking, +} + +/// The case of each argument name. +#[derive(Copy, Clone)] +enum ArgumentCase { + Snake, + Camel, +} + +/// The rename policy for the command. +enum RenamePolicy { + Keep, + Rename(TokenStream2), +} + +/// The bindings we attach to `tauri::Invoke`. +struct Invoke { + message: Ident, + resolver: Ident, + acl: Ident, +} + +/// Create a new [`Wrapper`] from the function and the generated code parsed from the function. +pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { + let mut attrs = parse_macro_input!(attributes as WrapperAttributes); + let function = parse_macro_input!(item as ItemFn); + let wrapper = super::format_command_wrapper(&function.sig.ident); + let visibility = &function.vis; + + if function.sig.asyncness.is_some() { + attrs.execution_context = ExecutionContext::Async; + } + + // 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]) + } + _ => TokenStream2::default(), + }; + + let invoke = Invoke { + message: format_ident!("__tauri_message__"), + resolver: format_ident!("__tauri_resolver__"), + acl: format_ident!("__tauri_acl__"), + }; + + // Tauri currently doesn't support async commands that take a reference as input and don't return + // a result. See: https://github.com/tauri-apps/tauri/issues/2533 + // + // For now, we provide an informative error message to the user in that case. Once #2533 is + // resolved, this check can be removed. + let mut async_command_check = TokenStream2::new(); + if function.sig.asyncness.is_some() { + // This check won't catch all possible problems but it should catch the most common ones. + let mut ref_argument_span = None; + + for arg in &function.sig.inputs { + if let syn::FnArg::Typed(pat) = arg { + match &*pat.ty { + syn::Type::Reference(_) => { + ref_argument_span = Some(pat.span()); + } + syn::Type::Path(path) => { + // Check if the type contains a lifetime argument + let last = path.path.segments.last().unwrap(); + if let syn::PathArguments::AngleBracketed(args) = &last.arguments { + if args + .args + .iter() + .any(|arg| matches!(arg, syn::GenericArgument::Lifetime(_))) + { + ref_argument_span = Some(pat.span()); + } + } + } + _ => {} + } + + if let Some(span) = ref_argument_span { + if let syn::ReturnType::Type(_, return_type) = &function.sig.output { + // To check if the return type is `Result` we require it to check a trait that is + // only implemented by `Result`. That way we don't exclude renamed result types + // which we wouldn't otherwise be able to detect purely from the token stream. + // The "error message" displayed to the user is simply the trait name. + let diagnostic = quote!(#[diagnostic::on_unimplemented(message = "async commands that contain references as inputs must return a `Result`")]); + + async_command_check = quote_spanned! {return_type.span() => + #[allow(unreachable_code, clippy::diverging_sub_expression, clippy::used_underscore_binding)] + const _: () = if false { + #diagnostic + trait AsyncCommandMustReturnResult {} + impl AsyncCommandMustReturnResult for ::std::result::Result {} + let _check: #return_type = unreachable!(); + let _: &dyn AsyncCommandMustReturnResult = &_check; + }; + }; + } else { + return quote_spanned! { + span => compile_error!("async commands that contain references as inputs must return a `Result`"); + }.into(); + } + } + } + } + } + + let plugin_name = var("CARGO_PKG_NAME") + .expect("missing `CARGO_PKG_NAME` environment variable") + .strip_prefix("tauri-plugin-") + .map(|name| quote!(::core::option::Option::Some(#name))) + .unwrap_or_else(|| quote!(::core::option::Option::None)); + + let body = match attrs.execution_context { + ExecutionContext::Async => body_async(&plugin_name, &function, &invoke, &attrs) + .unwrap_or_else(syn::Error::into_compile_error), + ExecutionContext::Blocking => body_blocking(&plugin_name, &function, &invoke, &attrs) + .unwrap_or_else(syn::Error::into_compile_error), + }; + + let Invoke { + message, + resolver, + acl, + } = invoke; + + let root = attrs.root; + + let kind = match attrs.execution_context { + ExecutionContext::Async if function.sig.asyncness.is_none() => "sync_threadpool", + ExecutionContext::Async => "async", + ExecutionContext::Blocking => "sync", + }; + + let loc = function.span().start(); + let line = loc.line; + let col = loc.column; + + let maybe_span = if cfg!(feature = "tracing") { + quote!({ + let _span = tracing::debug_span!( + "ipc::request::handler", + cmd = #message.command(), + kind = #kind, + loc.line = #line, + loc.col = #col, + is_internal = false, + ) + .entered(); + }) + } else { + quote!() + }; + + // Allow this to be unused when we're building with `build > removeUnusedCommands` for dead code elimination + let maybe_allow_unused = if var(REMOVE_UNUSED_COMMANDS_ENV_VAR).is_ok() { + quote!(#[allow(unused)]) + } else { + 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 + + #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)] + macro_rules! #wrapper { + // double braces because the item is expected to be a block expression + ($path:path, $invoke:ident) => { + // The IIFE here is for preventing stack overflow on Windows, + // see https://github.com/tauri-apps/tauri/issues/12488 + { + move || { + #[allow(unused_imports)] + use #root::ipc::private::*; + // prevent warnings when the body is a `compile_error!` or if the command has no arguments + #[allow(unused_variables)] + let #root::ipc::Invoke { message: #message, resolver: #resolver, acl: #acl } = $invoke; + + #maybe_span + + #body + } + }() + }; + } + + // allow the macro to be resolved with the same path as the command function + #[allow(unused_imports)] + #visibility use {#wrapper, #command_name_macro_ident}; + ) + .into() +} + +/// Generates an asynchronous command response from the arguments and return value of a function. +/// +/// See the [`tauri::command`] module for all the items and traits that make this possible. +/// +/// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html +fn body_async( + plugin_name: &TokenStream2, + function: &ItemFn, + invoke: &Invoke, + attributes: &WrapperAttributes, +) -> syn::Result { + let Invoke { + message, + resolver, + acl, + } = invoke; + parse_args(plugin_name, function, message, acl, attributes).map(|args| { + #[cfg(feature = "tracing")] + quote! { + use tracing::Instrument; + + let span = tracing::debug_span!("ipc::request::run"); + #resolver.respond_async_serialized(async move { + let result = $path(#(#args?),*); + let kind = (&result).async_kind(); + kind.future(result).await + } + .instrument(span)); + return true; + } + + #[cfg(not(feature = "tracing"))] + quote! { + #resolver.respond_async_serialized(async move { + let result = $path(#(#args?),*); + let kind = (&result).async_kind(); + kind.future(result).await + }); + return true; + } + }) +} + +/// Generates a blocking command response from the arguments and return value of a function. +/// +/// See the [`tauri::command`] module for all the items and traits that make this possible. +/// +/// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html +fn body_blocking( + plugin_name: &TokenStream2, + function: &ItemFn, + invoke: &Invoke, + attributes: &WrapperAttributes, +) -> syn::Result { + let Invoke { + message, + resolver, + acl, + } = invoke; + let args = parse_args(plugin_name, function, message, acl, attributes)?; + + // the body of a `match` to early return any argument that wasn't successful in parsing. + let match_body = quote!({ + Ok(arg) => arg, + Err(err) => { #resolver.invoke_error(err); return true }, + }); + + let maybe_span = if cfg!(feature = "tracing") { + quote!(let _span = tracing::debug_span!("ipc::request::run").entered();) + } else { + quote!() + }; + + Ok(quote! { + #maybe_span + let result = $path(#(match #args #match_body),*); + let kind = (&result).blocking_kind(); + kind.block(result, #resolver); + return true; + }) +} + +/// Parse all arguments for the command wrapper to use from the signature of the command function. +fn parse_args( + plugin_name: &TokenStream2, + function: &ItemFn, + message: &Ident, + acl: &Ident, + attributes: &WrapperAttributes, +) -> syn::Result> { + function + .sig + .inputs + .iter() + .map(|arg| { + parse_arg( + plugin_name, + &function.sig.ident, + arg, + message, + acl, + attributes, + ) + }) + .collect() +} + +/// Transform a [`FnArg`] into a command argument. +fn parse_arg( + plugin_name: &TokenStream2, + command: &Ident, + arg: &FnArg, + message: &Ident, + acl: &Ident, + attributes: &WrapperAttributes, +) -> syn::Result { + // we have no use for self arguments + let mut arg = match arg { + FnArg::Typed(arg) => arg.pat.as_ref().clone(), + FnArg::Receiver(arg) => { + return Err(syn::Error::new( + arg.span(), + "unable to use self as a command function parameter", + )); + } + }; + + // we only support patterns that allow us to extract some sort of keyed identifier + let mut key = match &mut arg { + Pat::Ident(arg) => arg.ident.unraw().to_string(), + Pat::Wild(_) => "".into(), // we always convert to camelCase, so "_" will end up empty anyways + Pat::Struct(s) => super::path_to_command(&mut s.path).ident.to_string(), + Pat::TupleStruct(s) => super::path_to_command(&mut s.path).ident.to_string(), + err => { + return Err(syn::Error::new( + err.span(), + "only named, wildcard, struct, and tuple struct arguments allowed", + )); + } + }; + + // also catch self arguments that use FnArg::Typed syntax + if key == "self" { + return Err(syn::Error::new( + key.span(), + "unable to use self as a command function parameter", + )); + } + + match attributes.argument_case { + ArgumentCase::Camel => { + key = key.to_lower_camel_case(); + } + ArgumentCase::Snake => { + key = key.to_snake_case(); + } + } + + 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: #command_name, + key: #key, + message: &#message, + acl: &#acl, + } + ))) +} diff --git a/crates/tauri-macros/src/context.rs b/crates/tauri-macros/src/context.rs new file mode 100644 index 0000000..91f9cbb --- /dev/null +++ b/crates/tauri-macros/src/context.rs @@ -0,0 +1,169 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{ToTokens, quote}; +use std::path::PathBuf; +use syn::{ + Expr, ExprLit, Lit, LitBool, LitStr, Meta, PathArguments, PathSegment, Token, + parse::{Parse, ParseBuffer}, + punctuated::Punctuated, +}; +use tauri_codegen::{ContextData, context_codegen, get_config}; +use tauri_utils::{config::parse::does_supported_file_name_exist, platform::Target}; + +pub(crate) struct ContextItems { + config_file: PathBuf, + root: syn::Path, + capabilities: Option>, + assets: Option, + test: bool, +} + +impl Parse for ContextItems { + fn parse(input: &ParseBuffer<'_>) -> syn::parse::Result { + let target = std::env::var("TARGET") + .or_else(|_| std::env::var("TAURI_ENV_TARGET_TRIPLE")) + .as_deref() + .map(Target::from_triple) + .unwrap_or_else(|_| Target::current()); + + let mut root = None; + let mut capabilities = None; + let mut assets = None; + let mut test = false; + let config_file = input.parse::().ok().map(|raw| { + let _ = input.parse::(); + let path = PathBuf::from(raw.value()); + if path.is_relative() { + std::env::var("CARGO_MANIFEST_DIR") + .map(|m| PathBuf::from(m).join(path)) + .map_err(|e| e.to_string()) + } else { + Ok(path) + } + .and_then(|path| { + if does_supported_file_name_exist(target, &path) { + Ok(path) + } else { + Err(format!( + "no file at path {} exists, expected tauri config file", + path.display() + )) + } + }) + }); + + while let Ok(meta) = input.parse::() { + match meta { + Meta::Path(p) => { + root.replace(p); + } + Meta::NameValue(v) => { + let ident = v.path.require_ident()?; + match ident.to_string().as_str() { + "capabilities" => { + if let Expr::Array(array) = v.value { + capabilities.replace( + array + .elems + .into_iter() + .map(|e| { + if let Expr::Lit(ExprLit { + attrs: _, + lit: Lit::Str(s), + }) = e + { + Ok(s.value().into()) + } else { + Err(syn::Error::new( + input.span(), + "unexpected expression for capability", + )) + } + }) + .collect::, syn::Error>>()?, + ); + } else { + return Err(syn::Error::new( + input.span(), + "unexpected value for capabilities", + )); + } + } + "assets" => { + assets.replace(v.value); + } + "test" => { + if let Expr::Lit(ExprLit { + lit: Lit::Bool(LitBool { value, .. }), + .. + }) = v.value + { + test = value; + } else { + return Err(syn::Error::new(input.span(), "unexpected value for test")); + } + } + name => { + return Err(syn::Error::new( + input.span(), + format!("unknown attribute {name}"), + )); + } + } + } + Meta::List(_) => { + return Err(syn::Error::new(input.span(), "unexpected list input")); + } + } + + let _ = input.parse::(); + } + + Ok(Self { + config_file: config_file + .unwrap_or_else(|| { + std::env::var("CARGO_MANIFEST_DIR") + .map(|m| PathBuf::from(m).join("tauri.conf.json")) + .map_err(|e| e.to_string()) + }) + .map_err(|e| input.error(e))?, + root: root.unwrap_or_else(|| { + let mut segments = Punctuated::new(); + segments.push(PathSegment { + ident: Ident::new("tauri", Span::call_site()), + arguments: PathArguments::None, + }); + syn::Path { + leading_colon: Some(Token![::](Span::call_site())), + segments, + } + }), + capabilities, + assets, + test, + }) + } +} + +pub(crate) fn generate_context(context: ContextItems) -> TokenStream { + let context = get_config(&context.config_file) + .map_err(|e| e.to_string()) + .map(|(config, config_parent)| ContextData { + dev: cfg!(not(feature = "custom-protocol")), + config, + config_parent, + root: context.root.to_token_stream(), + capabilities: context.capabilities, + assets: context.assets, + test: context.test, + }) + .and_then(|data| context_codegen(data).map_err(|e| e.to_string())); + + match context { + Ok(code) => code, + Err(error) => quote!(compile_error!(#error)), + } +} diff --git a/crates/tauri-macros/src/lib.rs b/crates/tauri-macros/src/lib.rs new file mode 100644 index 0000000..375d1d4 --- /dev/null +++ b/crates/tauri-macros/src/lib.rs @@ -0,0 +1,394 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Create macros for `tauri::Context`, invoke handler and commands leveraging the `tauri-codegen` crate. +//! +//! Don't depend on this crate directly, use the re-exported types from tauri instead. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" +)] + +use std::path::PathBuf; + +use crate::context::ContextItems; +use proc_macro::TokenStream; +use quote::{ToTokens, quote}; +use syn::{LitStr, parse_macro_input, parse2}; +use tauri_codegen::image::CachedIcon; + +mod command; +mod menu; +mod mobile; +mod runtime; + +#[macro_use] +mod context; + +/// Mark a function as a command handler. It creates a wrapper function with the necessary glue code. +/// +/// The wrapped function can then be passed to [`generate_handler!`] so it can be called +/// from the frontend with `invoke()`. +/// +/// ```rust,ignore +/// #[tauri::command] +/// fn greet(name: String) -> String { +/// format!("Hello, {name}!") +/// } +/// ``` +/// +/// # Options +/// +/// The attribute accepts a comma separated list of the following options: +/// +/// ## `async` +/// +/// Runs the command on the async runtime instead of blocking the main thread. +/// +/// `async fn` commands are always executed asynchronously, so this option is only needed +/// for synchronous functions that should not run on the main thread: +/// +/// ```rust,ignore +/// #[tauri::command(async)] +/// fn expensive_computation() -> u64 { +/// // the body runs on the async runtime, so the main thread is not blocked +/// 42 +/// } +/// ``` +/// +/// ## `rename_all` +/// +/// Sets the case convention used to match the command arguments with the keys of the +/// payload sent by the frontend. Either `"camelCase"` (default) or `"snake_case"`. +/// +/// ```rust,ignore +/// // called from JavaScript with `invoke("send_message", { messageBody: "Hello" })` +/// #[tauri::command] +/// fn send_message(message_body: String) {} +/// +/// // called from JavaScript with `invoke("send_message", { message_body: "Hello" })` +/// #[tauri::command(rename_all = "snake_case")] +/// fn send_message_snake(message_body: String) {} +/// ``` +/// +/// ## `rename` +/// +/// Changes the name used to call the command from the frontend. +/// By default it is the name of the function. +/// +/// ```rust,ignore +/// // called from JavaScript with `invoke("greetUser")` +/// // and still registered as `generate_handler![greet_user]` +/// #[tauri::command(rename = "greetUser")] +/// fn greet_user() {} +/// ``` +/// +/// ## `root` +/// +/// Path to the `tauri` crate, used when it is renamed in `Cargo.toml` or re-exported +/// by another crate. Defaults to `::tauri`, and the special value `"crate"` resolves +/// to `$crate` (used internally by Tauri itself). +/// +/// ```rust,ignore +/// // Cargo.toml: tauri_framework = { package = "tauri", version = "2" } +/// #[tauri::command(root = "tauri_framework")] +/// fn my_command() {} +/// ``` +/// +/// # Inline plugins +/// +/// When the command belongs to a plugin that is part of your application instead of a +/// standalone crate, annotate it in the [`generate_handler!`] list with the +/// `#![plugin(your_plugin_name)]` inner attribute so `build > removeUnusedCommands` can +/// match it against the plugin permissions. +/// +/// # Stability +/// The output of this macro is managed internally by Tauri, +/// and should not be accessed directly on normal applications. +/// It may have breaking changes in the future. +#[proc_macro_attribute] +pub fn command(attributes: TokenStream, item: TokenStream) -> TokenStream { + command::wrapper(attributes, item) +} + +/// Marks a function as the entry point of a mobile application. +/// +/// It must be applied to the function that builds and runs your Tauri application on the +/// library target (`run()` on `src-tauri/src/lib.rs` for apps created by the Tauri CLI), +/// which is the function the generated Android and iOS projects call on startup. +/// +/// The canonical usage only applies it on mobile targets, so the same function can be called +/// by the `main.rs` of the desktop binary: +/// +/// ```rust,ignore +/// #[cfg_attr(mobile, tauri::mobile_entry_point)] +/// pub fn run() { +/// tauri::Builder::default() +/// .run(tauri::generate_context!()) +/// .expect("error while running tauri application"); +/// } +/// ``` +/// +/// The macro generates a `start_app` C symbol (checked by the Tauri CLI) that catches +/// panics instead of unwinding across the FFI boundary, blocks on the function when it is +/// `async`, sets up the stdout logger on iOS and the JNI bindings on Android using the +/// package name derived from the `identifier` in your Tauri configuration. +/// +/// Because the Android package name is read from environment variables set by `tauri-build`, +/// your application must have a build script calling [`tauri_build::build`] - otherwise the +/// macro fails to compile with a `env var not set` error. +/// +/// [`tauri_build::build`]: https://docs.rs/tauri-build/latest/tauri_build/fn.build.html +#[proc_macro_attribute] +pub fn mobile_entry_point(attributes: TokenStream, item: TokenStream) -> TokenStream { + mobile::entry_point(attributes, item) +} + +/// Accepts a list of command functions. Creates a handler that allows commands to be called from JS with invoke(). +/// +/// You can optionally annotate the commands with a inner attribute tag `#![plugin(your_plugin_name)]` +/// for `build > removeUnusedCommands` to work for plugins not defined in a standalone crate like `tauri-plugin-fs` +/// +/// # Examples +/// +/// ```rust,ignore +/// use tauri_macros::{command, generate_handler}; +/// #[command] +/// fn command_one() { +/// println!("command one called"); +/// } +/// #[command] +/// fn command_two() { +/// println!("command two called"); +/// } +/// fn main() { +/// let _handler = generate_handler![command_one, command_two]; +/// } +/// ``` +/// +/// # Stability +/// +/// The output of this macro is managed internally by Tauri, +/// and should not be accessed directly on normal applications. +/// It may have breaking changes in the future. +#[proc_macro] +pub fn generate_handler(item: TokenStream) -> TokenStream { + parse_macro_input!(item as command::Handler).into() +} + +/// Reads a Tauri config file and generates a `::tauri::Context` based on the content. +/// +/// The context embeds the frontend assets, the application icons, the resolved Access Control List +/// and the parsed configuration into the binary, and is passed to +/// `tauri::Builder::run`/`tauri::Builder::build`. +/// +/// ```rust,ignore +/// tauri::Builder::default() +/// .run(tauri::generate_context!()) +/// .expect("error while running tauri application"); +/// ``` +/// +/// # Options +/// +/// All options are optional and can be combined in a comma separated list. +/// +/// ## Configuration file path +/// +/// A string literal as the first argument sets the path of the Tauri configuration file to read, +/// relative to `CARGO_MANIFEST_DIR`. Defaults to `tauri.conf.json` on the crate directory. +/// Platform specific configuration files (e.g. `tauri.windows.conf.json`) that sit next to it are +/// merged as usual. +/// +/// ```rust,ignore +/// tauri::generate_context!("../tauri.conf.json"); +/// ``` +/// +/// ## Root path +/// +/// A path (any item that is not a `key = value` pair) changes the crate path the generated code +/// refers to. Defaults to `::tauri`, and is only needed when the `tauri` crate is renamed, +/// re-exported by another crate, or is the crate being compiled (`crate`). +/// +/// ```rust,ignore +/// tauri::generate_context!("../tauri.conf.json", ::my_framework::tauri); +/// ``` +/// +/// ## `capabilities` +/// +/// A list of additional capability files to include in the generated Access Control List, +/// on top of the ones defined in the `capabilities` directory and in the +/// `app > security > capabilities` configuration value. +/// +/// Each item is a path (relative to the current working directory of the compiler, +/// usually the crate directory) to a JSON or TOML file containing a capability, +/// a list of capabilities or a named list of capabilities. +/// +/// ```rust,ignore +/// tauri::generate_context!(capabilities = ["./capabilities/extra.json"]); +/// ``` +/// +/// ## `assets` +/// +/// An expression resolving to a custom [`tauri::Assets`] implementation, used instead of +/// embedding the files from `build > frontendDist`. Useful for serving the frontend from a +/// custom source, or for skipping asset embedding on tests. +/// +/// ```rust,ignore +/// tauri::generate_context!(assets = tauri::test::noop_assets()); +/// ``` +/// +/// ## `test` +/// +/// When `true`, skips code generation that misbehaves when the context is created inside a +/// test binary - currently the `Info.plist` embedding performed on macOS development builds. +/// Defaults to `false`. +/// +/// ```rust,ignore +/// let context = tauri::generate_context!("../tauri.conf.json", test = true); +/// ``` +/// +/// [`tauri::Assets`]: https://docs.rs/tauri/latest/tauri/trait.Assets.html +/// +/// # Stability +/// The output of this macro is managed internally by Tauri, +/// and should not be accessed directly on normal applications. +/// It may have breaking changes in the future. +#[proc_macro] +pub fn generate_context(items: TokenStream) -> TokenStream { + // this macro is exported from the context module + let path = parse_macro_input!(items as ContextItems); + context::generate_context(path).into() +} + +/// Adds the default type for the last parameter (assumed to be runtime) for a specific feature. +/// +/// e.g. To default the runtime generic to type `crate::Wry` when the `wry` feature is enabled, the +/// syntax would look like `#[default_runtime(crate::Wry, wry)`. This is **always** set for the last +/// generic, so make sure the last generic is the runtime when using this macro. +#[doc(hidden)] +#[proc_macro_attribute] +pub fn default_runtime(attributes: TokenStream, input: TokenStream) -> TokenStream { + let attributes = parse_macro_input!(attributes as runtime::Attributes); + let input = parse_macro_input!(input as runtime::Input); + runtime::default_runtime(attributes, input).into() +} + +/// Accepts a closure-like syntax to call arbitrary code on a menu item +/// after matching against `kind` and retrieving it from `resources_table` using `rid`. +/// +/// You can optionally pass a 5th parameter to select which item kinds +/// to match against, by providing a `|` separated list of item kinds +/// ```ignore +/// do_menu_item!(resources_table, rid, kind, |i| i.set_text(text), Check | Submenu); +/// ``` +/// You could also provide a negated list +/// ```ignore +/// do_menu_item!(resources_table, rid, kind, |i| i.set_text(text), !Check); +/// do_menu_item!(resources_table, rid, kind, |i| i.set_text(text), !Check | !Submenu); +/// ``` +/// but you can't have mixed negations and positive kinds. +/// ```ignore +/// do_menu_item!(resources_table, rid, kind, |i| i.set_text(text), !Check | Submenu); +/// ``` +/// +/// ## Examples +/// +/// ```ignore +/// let rid = 23; +/// let kind = ItemKind::Check; +/// let resources_table = app.resources_table(); +/// do_menu_item!(resources_table, rid, kind, |i| i.set_text(text)) +/// ``` +/// which will expand into: +/// ```ignore +/// let rid = 23; +/// let kind = ItemKind::Check; +/// let resources_table = app.resources_table(); +/// match kind { +/// ItemKind::Submenu => { +/// let i = resources_table.get::>(rid)?; +/// i.set_text(text) +/// } +/// ItemKind::MenuItem => { +/// let i = resources_table.get::>(rid)?; +/// i.set_text(text) +/// } +/// ItemKind::Predefined => { +/// let i = resources_table.get::>(rid)?; +/// i.set_text(text) +/// } +/// ItemKind::Check => { +/// let i = resources_table.get::>(rid)?; +/// i.set_text(text) +/// } +/// ItemKind::Icon => { +/// let i = resources_table.get::>(rid)?; +/// i.set_text(text) +/// } +/// _ => return Err(crate::Error::UnexpectedMenuKind), +/// } +/// ``` +#[doc(hidden)] +#[proc_macro] +pub fn do_menu_item(input: TokenStream) -> TokenStream { + let tokens = parse_macro_input!(input as menu::DoMenuItemInput); + menu::do_menu_item(tokens).into() +} + +/// Convert a .png or .ico icon to an Image +/// for things like `tauri::tray::TrayIconBuilder` to consume, +/// relative paths are resolved from `CARGO_MANIFEST_DIR`, not current file +/// +/// ### Examples +/// +/// ```ignore +/// const APP_ICON: Image<'_> = include_image!("./icons/32x32.png"); +/// +/// // then use it with tray +/// TrayIconBuilder::new().icon(APP_ICON).build().unwrap(); +/// +/// // or with window +/// WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) +/// .icon(APP_ICON) +/// .unwrap() +/// .build() +/// .unwrap(); +/// +/// // or with any other functions that takes `Image` struct +/// ``` +/// +/// Note: this stores the image in raw pixels to the final binary, +/// so keep the icon size (width and height) small +/// or else it's going to bloat your final executable +#[proc_macro] +pub fn include_image(tokens: TokenStream) -> TokenStream { + let path = match parse2::(tokens.into()) { + Ok(path) => path, + Err(err) => return err.into_compile_error().into(), + }; + let path = PathBuf::from(path.value()); + let resolved_path = if path.is_relative() { + if let Ok(base_dir) = std::env::var("CARGO_MANIFEST_DIR").map(PathBuf::from) { + base_dir.join(path) + } else { + return quote!(compile_error!("$CARGO_MANIFEST_DIR is not defined")).into(); + } + } else { + path + }; + if !resolved_path.exists() { + let error_string = format!( + "Provided Image path \"{}\" doesn't exists", + resolved_path.display() + ); + return quote!(compile_error!(#error_string)).into(); + } + + match CachedIcon::new("e!(::tauri), &resolved_path).map_err(|error| error.to_string()) { + Ok(icon) => icon.into_token_stream(), + Err(error) => quote!(compile_error!(#error)), + } + .into() +} diff --git a/crates/tauri-macros/src/menu.rs b/crates/tauri-macros/src/menu.rs new file mode 100644 index 0000000..2133bb3 --- /dev/null +++ b/crates/tauri-macros/src/menu.rs @@ -0,0 +1,136 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; +use syn::{ + Expr, Token, + parse::{Parse, ParseStream}, + punctuated::Punctuated, +}; + +pub struct DoMenuItemInput { + resources_table: Ident, + rid: Ident, + kind: Ident, + var: Ident, + expr: Expr, + kinds: Vec, +} + +#[derive(Clone)] +struct NegatedIdent { + negated: bool, + ident: Ident, +} + +impl NegatedIdent { + fn new(ident: &str) -> Self { + Self { + negated: false, + ident: Ident::new(ident, Span::call_site()), + } + } + + fn is_negated(&self) -> bool { + self.negated + } +} + +impl Parse for NegatedIdent { + fn parse(input: ParseStream) -> syn::Result { + let negated_token = input.parse::(); + let ident: Ident = input.parse()?; + Ok(NegatedIdent { + negated: negated_token.is_ok(), + ident, + }) + } +} + +impl Parse for DoMenuItemInput { + fn parse(input: ParseStream) -> syn::Result { + let resources_table: Ident = input.parse()?; + let _: Token![,] = input.parse()?; + let rid: Ident = input.parse()?; + let _: Token![,] = input.parse()?; + let kind: Ident = input.parse()?; + let _: Token![,] = input.parse()?; + let _: Token![|] = input.parse()?; + let var: Ident = input.parse()?; + let _: Token![|] = input.parse()?; + let expr: Expr = input.parse()?; + let _: syn::Result = input.parse(); + let kinds = Punctuated::::parse_terminated(input)?; + + Ok(Self { + resources_table, + rid, + kind, + var, + expr, + kinds: kinds.into_iter().collect(), + }) + } +} + +pub fn do_menu_item(input: DoMenuItemInput) -> TokenStream { + let DoMenuItemInput { + rid, + resources_table, + kind, + expr, + var, + mut kinds, + } = input; + + let defaults = vec![ + NegatedIdent::new("Submenu"), + NegatedIdent::new("MenuItem"), + NegatedIdent::new("Predefined"), + NegatedIdent::new("Check"), + NegatedIdent::new("Icon"), + ]; + + if kinds.is_empty() { + kinds.extend(defaults.clone()); + } + + let has_negated = kinds.iter().any(|n| n.is_negated()); + if has_negated { + kinds.extend(defaults); + kinds.sort_by(|a, b| a.ident.cmp(&b.ident)); + kinds.dedup_by(|a, b| a.ident == b.ident); + } + + let (kinds, types): (Vec, Vec) = kinds + .into_iter() + .filter_map(|nident| { + if nident.is_negated() { + None + } else { + match nident.ident { + i if i == "MenuItem" => Some((i, Ident::new("MenuItem", Span::call_site()))), + i if i == "Submenu" => Some((i, Ident::new("Submenu", Span::call_site()))), + i if i == "Predefined" => Some((i, Ident::new("PredefinedMenuItem", Span::call_site()))), + i if i == "Check" => Some((i, Ident::new("CheckMenuItem", Span::call_site()))), + i if i == "Icon" => Some((i, Ident::new("IconMenuItem", Span::call_site()))), + _ => None, + } + } + }) + .unzip(); + + quote! { + match #kind { + #( + ItemKind::#kinds => { + let #var = #resources_table.get::<#types>(#rid)?; + #expr + } + )* + _ => return Err(crate::Error::UnexpectedMenuKind), + } + } +} diff --git a/crates/tauri-macros/src/mobile.rs b/crates/tauri-macros/src/mobile.rs new file mode 100644 index 0000000..ae5765e --- /dev/null +++ b/crates/tauri-macros/src/mobile.rs @@ -0,0 +1,95 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use std::env::var; +use syn::{ItemFn, parse_macro_input, spanned::Spanned}; + +fn get_env_var(name: &str, error: &mut Option, function: &ItemFn) -> TokenStream2 { + match var(name) { + Ok(value) => { + let ident = format_ident!("{value}"); + quote!(#ident) + } + Err(_) => { + error.replace( + syn::Error::new( + function.span(), + format!("`{name}` env var not set, do you have a build script with tauri-build?",), + ) + .into_compile_error(), + ); + quote!() + } + } +} + +pub fn entry_point(_attributes: TokenStream, item: TokenStream) -> TokenStream { + let function = parse_macro_input!(item as ItemFn); + let function_name = function.sig.ident.clone(); + + let mut error = None; + let domain = get_env_var("TAURI_ANDROID_PACKAGE_NAME_PREFIX", &mut error, &function); + let app_name = get_env_var("TAURI_ANDROID_PACKAGE_NAME_APP_NAME", &mut error, &function); + + let (wrapper, wrapper_name) = if function.sig.asyncness.is_some() { + let wrapper_name = syn::Ident::new(&format!("{function_name}_wrapper"), function_name.span()); + ( + quote! { + #function + + fn #wrapper_name() { + ::tauri::async_runtime::block_on(#function_name()); + } + }, + wrapper_name, + ) + } else { + ( + quote! { + #function + }, + function_name, + ) + }; + + if let Some(e) = error { + quote!(#e).into() + } else { + quote!( + fn stop_unwind T, T>(f: F) -> T { + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) { + Ok(t) => t, + Err(err) => { + eprintln!("attempt to unwind out of `rust` with err: {:?}", err); + std::process::abort() + } + } + } + + #wrapper + + fn _start_app() { + #[cfg(target_os = "ios")] + ::tauri::log_stdout(); + #[cfg(target_os = "android")] + { + ::tauri::android_binding!(#domain, #app_name, _start_app, ::tauri::wry); + } + stop_unwind(#wrapper_name); + } + + // be careful when renaming this, the `start_app` symbol is checked by the CLI + #[cfg(not(target_os = "android"))] + #[unsafe(no_mangle)] + #[inline(never)] + pub extern "C" fn start_app() { + _start_app() + } + ) + .into() + } +} diff --git a/crates/tauri-macros/src/runtime.rs b/crates/tauri-macros/src/runtime.rs new file mode 100644 index 0000000..69f3de2 --- /dev/null +++ b/crates/tauri-macros/src/runtime.rs @@ -0,0 +1,105 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use syn::parse::{Parse, ParseStream}; +use syn::{ + DeriveInput, Error, GenericParam, Ident, ItemTrait, ItemType, Token, Type, TypeParam, parse_quote, +}; + +#[derive(Clone)] +pub(crate) enum Input { + Derive(DeriveInput), + Trait(ItemTrait), + Type(ItemType), +} + +impl Parse for Input { + fn parse(input: ParseStream) -> syn::Result { + input + .parse::() + .map(Self::Derive) + .or_else(|_| input.parse().map(Self::Trait)) + .or_else(|_| input.parse().map(Self::Type)) + .map_err(|_| { + Error::new( + input.span(), + "default_runtime only supports `struct`, `enum`, `type`, or `trait` definitions", + ) + }) + } +} + +impl Input { + fn last_param_mut(&mut self) -> Option<&mut GenericParam> { + match self { + Input::Derive(d) => d.generics.params.last_mut(), + Input::Trait(t) => t.generics.params.last_mut(), + Input::Type(t) => t.generics.params.last_mut(), + } + } +} + +impl ToTokens for Input { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Input::Derive(d) => d.to_tokens(tokens), + Input::Trait(t) => t.to_tokens(tokens), + Input::Type(t) => t.to_tokens(tokens), + } + } +} + +/// The default runtime type to enable when the provided feature is enabled. +pub(crate) struct Attributes { + default_type: Type, + feature: Ident, +} + +impl Parse for Attributes { + fn parse(input: ParseStream<'_>) -> syn::Result { + let default_type = input.parse()?; + input.parse::()?; + Ok(Attributes { + default_type, + feature: input.parse()?, + }) + } +} + +pub(crate) fn default_runtime(attributes: Attributes, input: Input) -> TokenStream { + // create a new copy to manipulate for the wry feature flag + let mut wry = input.clone(); + let wry_runtime = wry + .last_param_mut() + .expect("default_runtime requires the item to have at least 1 generic parameter"); + + // set the default value of the last generic parameter to the provided runtime type + match wry_runtime { + GenericParam::Type( + param @ TypeParam { + eq_token: None, + default: None, + .. + }, + ) => { + param.eq_token = Some(parse_quote!(=)); + param.default = Some(attributes.default_type); + } + _ => { + panic!("DefaultRuntime requires the last parameter to not have a default value") + } + }; + + let feature = attributes.feature.to_string(); + + quote!( + #[cfg(feature = #feature)] + #wry + + #[cfg(not(feature = #feature))] + #input + ) +} diff --git a/crates/tauri-plugin/CHANGELOG.md b/crates/tauri-plugin/CHANGELOG.md new file mode 100644 index 0000000..1a289a3 --- /dev/null +++ b/crates/tauri-plugin/CHANGELOG.md @@ -0,0 +1,396 @@ +# Changelog + +## \[2.6.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.3` + +## \[2.6.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.2` + +## \[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 + +- Upgraded to `tauri-utils@2.8.3` + +## \[2.5.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.8.2` + +## \[2.5.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.8.1` + +## \[2.5.1] + +### Bug Fixes + +- [`4b6b8690a`](https://www.github.com/tauri-apps/tauri/commit/4b6b8690ab886ebdf1307951cffbe03e31280baa) ([#14347](https://www.github.com/tauri-apps/tauri/pull/14347) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.5.0] + +### Dependencies + +- Upgraded to `tauri-utils@2.8.0` +- [`6aa7f2d85`](https://www.github.com/tauri-apps/tauri/commit/6aa7f2d852870aeba1d4dd0e07f8be2bc9b66286) Upgraded to `tauri-utils@2.8.0` + +## \[2.4.0] + +### New Features + +- [`a0113a8c6`](https://www.github.com/tauri-apps/tauri/commit/a0113a8c6471eccc43b3202cc009901a04e41baa) ([#13888](https://www.github.com/tauri-apps/tauri/pull/13888) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Added `build::mobile::update_info_plist` to allow a plugin to update the iOS project Info.plist file. + +### Dependencies + +- Upgraded to `tauri-utils@2.7.0` + +## \[2.3.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.6.0` + +## \[2.2.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.5.0` + +## \[2.2.0] + +### Dependencies + +- Upgraded to `tauri-utils@2.4.0` +- [`48b12b440`](https://www.github.com/tauri-apps/tauri/commit/48b12b440478937c46fdfef9f9d95194be117020) Update to `tauri-utils@2.4.0` + +## \[2.1.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.3.1` + +## \[2.1.0] + +### New Features + +- [`013f8f652`](https://www.github.com/tauri-apps/tauri/commit/013f8f652302f2d49c5ec0a075582033d8b074fb) ([#12890](https://www.github.com/tauri-apps/tauri/pull/12890) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Added `build > removeUnusedCommands` to trigger the build scripts and macros to remove unused commands based on the capabilities you defined. Note this won't be accounting for dynamically added ACLs so make sure to check it when using this. + +### Performance Improvements + +- [`1cd8f55ee`](https://www.github.com/tauri-apps/tauri/commit/1cd8f55eed326d61860fee62ba2d2f4464bdcfcc) ([#13033](https://www.github.com/tauri-apps/tauri/pull/13033) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Don't ship global `bundle.global.js` if `app > withGlobalTauri` is set to false + +### Dependencies + +- Upgraded to `tauri-utils@2.3.0` + +## \[2.0.5] + +### Dependencies + +- Upgraded to `tauri-utils@2.2.0` + +## \[2.0.4] + +### Dependencies + +- Upgraded to `tauri-utils@2.1.1` + +## \[2.0.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.1.0` + +## \[2.0.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.2` + +## \[2.0.1] + +### What's Changed + +- [`0ab2b3306`](https://www.github.com/tauri-apps/tauri/commit/0ab2b330644b6419f6cee1d5377bfb5cdda2ccf9) ([#11205](https://www.github.com/tauri-apps/tauri/pull/11205) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.1` + +## \[2.0.0] + +### What's Changed + +- [`382ed482b`](https://www.github.com/tauri-apps/tauri/commit/382ed482bd08157c39e62f9a0aaad8802f1092cb) Bump MSRV to 1.78. +- [`637285790`](https://www.github.com/tauri-apps/tauri/commit/6372857905ae9c0aedb7f482ddf6cf9f9836c9f2) Promote to v2 stable! + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0` + +## \[2.0.0-rc.13] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.13` + +## \[2.0.0-rc.12] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.12` + +## \[2.0.0-rc.11] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.11` + +## \[2.0.0-rc.10] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.10` + +## \[2.0.0-rc.9] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.9` + +## \[2.0.0-rc.8] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.8` + +## \[2.0.0-rc.7] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.7` + +## \[2.0.0-rc.6] + +### What's Changed + +- [`f4d5241b3`](https://www.github.com/tauri-apps/tauri/commit/f4d5241b377d0f7a1b58100ee19f7843384634ac) ([#10731](https://www.github.com/tauri-apps/tauri/pull/10731) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Update documentation icon path. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.6` + +## \[2.0.0-rc.5] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.5` + +## \[2.0.0-rc.4] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.4` + +## \[2.0.0-rc.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.3` + +## \[2.0.0-rc.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.2` + +## \[2.0.0-rc.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.1` + +## \[2.0.0-rc.0] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.0` + +### Breaking Changes + +- [`758d28c8a`](https://www.github.com/tauri-apps/tauri/commit/758d28c8a2d5c9567158e339326b765f72da983e) ([#10390](https://www.github.com/tauri-apps/tauri/pull/10390)) Core plugin permissions are now prefixed with `core:`, the `core:default` permission set can now be used and the `core` plugin name is reserved. + The `tauri migrate` tool will automate the migration process, which involves prefixing all `app`, `event`, `image`, `menu`, `path`, `resources`, `tray`, `webview` and `window` permissions with `core:`. + +## \[2.0.0-beta.19] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.19` + +## \[2.0.0-beta.18] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.18` + +## \[2.0.0-beta.17] + +### New Features + +- [`8a1ae2dea`](https://www.github.com/tauri-apps/tauri/commit/8a1ae2deaf3086e531ada25b1627f900e2e421fb)([#9843](https://www.github.com/tauri-apps/tauri/pull/9843)) Added an option to use a Xcode project for the iOS plugin instead of a plain SwiftPM project. + +### What's Changed + +- [`9ac930380`](https://www.github.com/tauri-apps/tauri/commit/9ac930380a5df3fe700e68e75df8684d261ca292)([#9850](https://www.github.com/tauri-apps/tauri/pull/9850)) Emit `cargo:rustc-check-cfg` instruction so Cargo validates custom cfg attributes on Rust 1.80 (or nightly-2024-05-05). + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.17` + +## \[2.0.0-beta.16] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.16` + +## \[2.0.0-beta.15] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.15` + +## \[2.0.0-beta.14] + +### Enhancements + +- [`bf2635ab6`](https://www.github.com/tauri-apps/tauri/commit/bf2635ab6241a5b82569eafc939046d6e245f3ad)([#9632](https://www.github.com/tauri-apps/tauri/pull/9632)) Improve the error message that is shown when the `links` property is missing from a Tauri Plugin. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.14` + +## \[2.0.0-beta.13] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.13` + +## \[2.0.0-beta.12] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.12` + +## \[2.0.0-beta.11] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.11` + +## \[2.0.0-beta.10] + +### New Features + +- [`e227fe02f`](https://www.github.com/tauri-apps/tauri/commit/e227fe02f986e145c0731a64693e1c830a9eb5b0)([#9156](https://www.github.com/tauri-apps/tauri/pull/9156)) Allow plugins to define (at compile time) JavaScript that are initialized when `withGlobalTauri` is true. +- [`e227fe02f`](https://www.github.com/tauri-apps/tauri/commit/e227fe02f986e145c0731a64693e1c830a9eb5b0)([#9156](https://www.github.com/tauri-apps/tauri/pull/9156)) Added `Builder::global_api_script_path` to define a JavaScript file containing the initialization script for the plugin API bindings when `withGlobalTauri` is used. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.10` + +## \[2.0.0-beta.9] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.9` + +## \[2.0.0-beta.8] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.8` + +## \[2.0.0-beta.7] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.7` + +## \[2.0.0-beta.6] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.6` + +### Breaking Changes + +- [`3657ad82`](https://www.github.com/tauri-apps/tauri/commit/3657ad82f88ce528551d032d521c52eed3f396b4)([#9008](https://www.github.com/tauri-apps/tauri/pull/9008)) Allow defining permissions for the application commands via `tauri_build::Attributes::app_manifest`. + +## \[2.0.0-beta.5] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.5` + +## \[2.0.0-beta.4] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.4` + +## \[2.0.0-beta.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.3` + +## \[2.0.0-beta.2] + +### Enhancements + +- [`dd7571a7`](https://www.github.com/tauri-apps/tauri/commit/dd7571a7808676c8063a4983b9c6687dfaf03a09)([#8815](https://www.github.com/tauri-apps/tauri/pull/8815)) Do not generate JSON schema and markdown reference file if the plugin does not define any permissions and delete those files if they exist. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.2` + +## \[2.0.0-beta.1] + +### Bug Fixes + +- [`4e101f80`](https://www.github.com/tauri-apps/tauri/commit/4e101f801657e7d01ce8c22f9c6468067d0caab2)([#8756](https://www.github.com/tauri-apps/tauri/pull/8756)) Rerun build script when a new permission is added. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.1` + +## \[2.0.0-beta.0] + +### New Features + +- [`74a2a603`](https://www.github.com/tauri-apps/tauri/commit/74a2a6036a5e57462f161d728cbd8a6f121028ca)([#8661](https://www.github.com/tauri-apps/tauri/pull/8661)) Implement access control list for IPC usage. diff --git a/crates/tauri-plugin/Cargo.toml b/crates/tauri-plugin/Cargo.toml new file mode 100644 index 0000000..a328ba7 --- /dev/null +++ b/crates/tauri-plugin/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "tauri-plugin" +version = "2.6.3" +description = "Build script and runtime Tauri plugin definitions" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +categories.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[package.metadata.docs.rs] +features = ["build", "runtime"] + +[features] +build = [ + "dep:anyhow", + "dep:serde", + "dep:serde_json", + "dep:glob", + "dep:plist", + "dep:walkdir", +] +runtime = [] + +[dependencies] +anyhow = { version = "1", optional = true } +serde = { version = "1", optional = true } +tauri-utils = { workspace = true, features = ["build-2"] } +serde_json = { version = "1", optional = true } +glob = { version = "0.3", optional = true } +# Our code requires at least 0.8.22 so don't simplify this to 0.8 +schemars = { version = "0.8.22", features = ["preserve_order"] } +walkdir = { version = "2", optional = true } + +[target."cfg(target_os = \"macos\")".dependencies] +plist = { version = "1", optional = true } + +[lints] +workspace = true diff --git a/crates/tauri-plugin/src/build/mobile.rs b/crates/tauri-plugin/src/build/mobile.rs new file mode 100644 index 0000000..9461c4f --- /dev/null +++ b/crates/tauri-plugin/src/build/mobile.rs @@ -0,0 +1,231 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Mobile-specific build utilities. + +use std::{ + fs::{copy, create_dir, create_dir_all, remove_dir_all}, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result}; + +use super::{build_var, cfg_alias}; + +/// Patches the entitlements file of the iOS project of the application that depends on this +/// plugin, so the plugin can request the capabilities its native code needs +/// (push notifications, app groups, HealthKit, iCloud...). +/// +/// It edits `/_iOS/_iOS.entitlements` of the Xcode project +/// generated by `tauri ios init`, reading its path from the `TAURI_IOS_PROJECT_PATH` and +/// `TAURI_IOS_APP_NAME` environment variables set by the Tauri CLI. It is a no-op when they are +/// not set (a plain `cargo build`, or a desktop build) or when the file does not exist, and the +/// file is only written when the closure actually changed it. +/// +/// Must be called from the plugin `build.rs`, ideally guarded by a +/// [`plugin_config`](super::plugin_config) value so the app can opt in to the entitlement. +/// Only available on macOS hosts, where the iOS project can be built. +/// +/// # Examples +/// +/// ```rust,no_run +/// tauri_plugin::mobile::update_entitlements(|entitlements| { +/// entitlements.insert("aps-environment".into(), "development".into()); +/// }) +/// .expect("failed to update entitlements"); +/// ``` +#[cfg(target_os = "macos")] +pub fn update_entitlements(f: F) -> Result<()> { + if let (Some(project_path), Ok(app_name)) = ( + std::env::var_os("TAURI_IOS_PROJECT_PATH").map(PathBuf::from), + std::env::var("TAURI_IOS_APP_NAME"), + ) { + update_plist_file( + project_path + .join(format!("{app_name}_iOS")) + .join(format!("{app_name}_iOS.entitlements")), + f, + )?; + } + + Ok(()) +} + +/// Patches the `Info.plist` of the iOS project of the application that depends on this plugin, +/// which is where iOS expects the usage descriptions, URL schemes and background modes a +/// plugin needs to be declared. +/// +/// It edits `/_iOS/Info.plist` of the Xcode project generated by +/// `tauri ios init`, reading its path from the `TAURI_IOS_PROJECT_PATH` and +/// `TAURI_IOS_APP_NAME` environment variables set by the Tauri CLI. It is a no-op when they are +/// not set (a plain `cargo build`, or a desktop build) or when the file does not exist, and the +/// file is only written when the closure actually changed it. +/// +/// Must be called from the plugin `build.rs`. Values that are specific to the application, such +/// as the text shown on a permission prompt, should be read from the plugin configuration with +/// [`plugin_config`](super::plugin_config) instead of hardcoded. +/// Only available on macOS hosts, where the iOS project can be built. +/// +/// # Examples +/// +/// ```rust,no_run +/// tauri_plugin::mobile::update_info_plist(|plist| { +/// plist.insert( +/// "NSCameraUsageDescription".into(), +/// "This app needs the camera to scan QR codes".into(), +/// ); +/// }) +/// .expect("failed to update Info.plist"); +/// ``` +#[cfg(target_os = "macos")] +pub fn update_info_plist(f: F) -> Result<()> { + if let (Some(project_path), Ok(app_name)) = ( + std::env::var_os("TAURI_IOS_PROJECT_PATH").map(PathBuf::from), + std::env::var("TAURI_IOS_APP_NAME"), + ) { + update_plist_file( + project_path + .join(format!("{app_name}_iOS")) + .join("Info.plist"), + f, + )?; + } + + 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<()> { + tauri_utils::build::update_android_manifest(block_identifier, parent, insert) +} + +pub(crate) fn setup( + android_path: Option, + #[allow(unused_variables)] ios_path: Option, +) -> Result<()> { + let target_os = build_var("CARGO_CFG_TARGET_OS")?; + let mobile = target_os == "android" || target_os == "ios"; + cfg_alias("mobile", mobile); + cfg_alias("desktop", !mobile); + + match target_os.as_str() { + "android" => { + if let Some(path) = android_path { + let manifest_dir = build_var("CARGO_MANIFEST_DIR").map(PathBuf::from)?; + let source = manifest_dir.join(path); + + let tauri_library_path = std::env::var("DEP_TAURI_ANDROID_LIBRARY_PATH") + .expect("missing `DEP_TAURI_ANDROID_LIBRARY_PATH` environment variable. Make sure `tauri` is a dependency of the plugin."); + println!("cargo:rerun-if-env-changed=DEP_TAURI_ANDROID_LIBRARY_PATH"); + + create_dir_all(source.join(".tauri")).context("failed to create .tauri directory")?; + copy_folder( + Path::new(&tauri_library_path), + &source.join(".tauri").join("tauri-api"), + &[], + ) + .context("failed to copy tauri-api to the plugin project")?; + + println!("cargo:android_library_path={}", source.display()); + } + } + #[cfg(target_os = "macos")] + "ios" => { + if let Some(path) = ios_path { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .map(PathBuf::from) + .unwrap(); + let tauri_library_path = std::env::var("DEP_TAURI_IOS_LIBRARY_PATH") + .expect("missing `DEP_TAURI_IOS_LIBRARY_PATH` environment variable. Make sure `tauri` is a dependency of the plugin."); + + let tauri_dep_path = path.parent().unwrap().join(".tauri"); + create_dir_all(&tauri_dep_path).context("failed to create .tauri directory")?; + copy_folder( + Path::new(&tauri_library_path), + &tauri_dep_path.join("tauri-api"), + &[".build", "Package.resolved", "Tests"], + ) + .context("failed to copy tauri-api to the plugin project")?; + tauri_utils::build::link_apple_library( + &std::env::var("CARGO_PKG_NAME").unwrap(), + manifest_dir.join(path), + ); + } + } + _ => (), + } + + Ok(()) +} + +fn copy_folder(source: &Path, target: &Path, ignore_paths: &[&str]) -> Result<()> { + let _ = remove_dir_all(target); + + for entry in walkdir::WalkDir::new(source) { + let entry = entry?; + let rel_path = entry.path().strip_prefix(source)?; + let rel_path_str = rel_path.to_string_lossy(); + if ignore_paths + .iter() + .any(|path| rel_path_str.starts_with(path)) + { + continue; + } + let dest_path = target.join(rel_path); + + if entry.file_type().is_dir() { + create_dir(&dest_path) + .with_context(|| format!("failed to create directory {}", dest_path.display()))?; + } else { + copy(entry.path(), &dest_path).with_context(|| { + format!( + "failed to copy {} to {}", + entry.path().display(), + dest_path.display() + ) + })?; + println!("cargo:rerun-if-changed={}", entry.path().display()); + } + } + + Ok(()) +} + +#[cfg(target_os = "macos")] +fn update_plist_file, F: FnOnce(&mut plist::Dictionary)>( + path: P, + f: F, +) -> Result<()> { + use std::io::Cursor; + + let path = path.as_ref(); + if path.exists() { + 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); + let mut plist_buf = Vec::new(); + let writer = Cursor::new(&mut plist_buf); + plist::to_writer_xml(writer, &plist)?; + let new_plist_str = String::from_utf8(plist_buf)?; + if new_plist_str != plist_str { + std::fs::write(path, new_plist_str)?; + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + #[test] + 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/build/mod.rs b/crates/tauri-plugin/src/build/mod.rs new file mode 100644 index 0000000..62caa2f --- /dev/null +++ b/crates/tauri-plugin/src/build/mod.rs @@ -0,0 +1,220 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, +}; + +use anyhow::Result; +use tauri_utils::acl::{self, Error}; + +pub mod mobile; + +use serde::de::DeserializeOwned; + +use std::{env, io::Cursor}; + +const RESERVED_PLUGIN_NAMES: &[&str] = &["core", "tauri"]; + +/// Reads the configuration of the plugin with the given name from the environment. +/// +/// Tauri applications configure plugins through the `plugins > $plugin-name` object of their +/// `tauri.conf.json`. The Tauri CLI forwards that object to the build script of the plugin +/// through the `TAURI__PLUGIN_CONFIG` environment variable (uppercased, with `-` +/// replaced by `_`), so plugins can generate platform-specific files from it - for example +/// adding a usage description to the iOS `Info.plist` with `mobile::update_info_plist` +/// (macOS hosts only). +/// +/// Returns `None` when the variable is not set, which is the case when the crate is built +/// without the Tauri CLI (e.g. a plain `cargo build`), so the build script must always be able +/// to run without a configuration. +/// +/// Also emits a `cargo:rerun-if-env-changed` instruction for the variable. +/// +/// # Examples +/// +/// ```rust,no_run +/// // usually a type deriving `serde::Deserialize` +/// let config = tauri_plugin::plugin_config::("my-plugin"); +/// let timeout = config +/// .and_then(|c| c.get("timeout").and_then(|t| t.as_u64())) +/// .unwrap_or(30); +/// ``` +/// +/// # Panics +/// +/// Panics if the value of the environment variable is not a valid JSON representation of `T`, +/// which means the plugin configuration on the app's `tauri.conf.json` does not match the +/// expected format. +pub fn plugin_config(name: &str) -> Option { + let config_env_var_name = format!( + "TAURI_{}_PLUGIN_CONFIG", + name.to_uppercase().replace('-', "_") + ); + if let Ok(config_str) = env::var(&config_env_var_name) { + println!("cargo:rerun-if-env-changed={config_env_var_name}"); + serde_json::from_reader(Cursor::new(config_str)) + .map(Some) + .expect("failed to parse configuration") + } else { + None + } +} + +/// Builder of the Tauri plugin build script. +/// +/// It must be executed from the `build.rs` of every Tauri plugin crate - see +/// [`Self::try_build`] for everything it does. +/// +/// # Examples +/// +/// ```rust,no_run +/// const COMMANDS: &[&str] = &["ping", "execute"]; +/// +/// tauri_plugin::Builder::new(COMMANDS) +/// .android_path("android") +/// .ios_path("ios") +/// .build(); +/// ``` +pub struct Builder<'a> { + commands: &'a [&'static str], + global_scope_schema: Option, + global_api_script_path: Option, + android_path: Option, + ios_path: Option, +} + +impl<'a> Builder<'a> { + /// Creates a new builder for a plugin exposing the given commands. + /// + /// The command names must be written in snake_case, matching the name of the Rust functions + /// annotated with `#[tauri::command]`. An `allow-$command` and a `deny-$command` permission + /// is generated for each of them in the `permissions/autogenerated/commands` directory, + /// where `$command` is the command name with `_` replaced by `-`. + /// + /// Note that the default permission of the plugin is **not** autogenerated: + /// it must be defined in a `permissions/default.toml` (or `.json`) file. + pub fn new(commands: &'a [&'static str]) -> Self { + Self { + commands, + global_scope_schema: None, + global_api_script_path: None, + android_path: None, + ios_path: None, + } + } + + /// Sets the global scope JSON schema. + pub fn global_scope_schema(mut self, schema: schemars::schema::RootSchema) -> Self { + self.global_scope_schema.replace(schema); + self + } + + /// Sets the path to the script that is injected in the webview when the `withGlobalTauri` configuration is set to true. + /// + /// This is usually an IIFE that injects the plugin API JavaScript bindings to `window.__TAURI__`. + pub fn global_api_script_path>(mut self, path: P) -> Self { + self.global_api_script_path.replace(path.into()); + self + } + + /// Sets the Android project path. + pub fn android_path>(mut self, android_path: P) -> Self { + self.android_path.replace(android_path.into()); + self + } + + /// Sets the iOS project path. + pub fn ios_path>(mut self, ios_path: P) -> Self { + self.ios_path.replace(ios_path.into()); + self + } + + /// [`Self::try_build`] but will exit automatically if an error is found. + pub fn build(self) { + if let Err(error) = self.try_build() { + println!("{}: {error:#}", env!("CARGO_PKG_NAME")); + std::process::exit(1); + } + } + + /// Ensure this crate is properly configured to be a Tauri plugin. + /// + /// # Errors + /// + /// Errors will occur if environmental variables expected to be set inside of [build scripts] + /// are not found, or if the crate violates Tauri plugin conventions. + pub fn try_build(self) -> Result<()> { + // convention: plugin names should not use underscores + let name = build_var("CARGO_PKG_NAME")?; + if name.contains('_') { + anyhow::bail!("plugin names cannot contain underscores"); + } + if RESERVED_PLUGIN_NAMES.contains(&name.as_str()) { + anyhow::bail!("plugin name `{name}` is reserved"); + } + + let out_dir = PathBuf::from(build_var("OUT_DIR")?); + + // requirement: links MUST be set and MUST match the name + let _links = std::env::var("CARGO_MANIFEST_LINKS").map_err(|_| Error::LinksMissing)?; + + let autogenerated = Path::new("permissions").join(acl::build::AUTOGENERATED_FOLDER_NAME); + std::fs::create_dir_all(&autogenerated).expect("unable to create permissions dir"); + + let commands_dir = autogenerated.join("commands"); + if !self.commands.is_empty() { + acl::build::autogenerate_command_permissions(&commands_dir, self.commands, "", true); + } + + println!("cargo:rerun-if-changed=permissions"); + let permissions = + acl::build::define_permissions("./permissions/**/*.*", &name, &out_dir, |_| true)?; + + if permissions.is_empty() { + let _ = std::fs::remove_file(format!( + "./permissions/{}/{}", + acl::PERMISSION_SCHEMAS_FOLDER_NAME, + acl::PERMISSION_SCHEMA_FILE_NAME + )); + let _ = std::fs::remove_file(autogenerated.join(acl::build::PERMISSION_DOCS_FILE_NAME)); + } else { + acl::schema::generate_permissions_schema(&permissions, "./permissions")?; + acl::build::generate_docs( + &permissions, + &autogenerated, + name.strip_prefix("tauri-plugin-").unwrap_or(&name), + )?; + } + + let mut permissions_map = BTreeMap::new(); + permissions_map.insert(name.clone(), permissions); + tauri_utils::acl::build::generate_allowed_commands(&out_dir, None, permissions_map)?; + + if let Some(global_scope_schema) = self.global_scope_schema { + acl::build::define_global_scope_schema(global_scope_schema, &name, &out_dir)?; + } + + if let Some(path) = self.global_api_script_path { + tauri_utils::plugin::define_global_api_script_path(&path); + } + + mobile::setup(self.android_path, self.ios_path)?; + + Ok(()) + } +} + +fn cfg_alias(alias: &str, has_feature: bool) { + println!("cargo:rustc-check-cfg=cfg({alias})"); + if has_feature { + println!("cargo:rustc-cfg={alias}"); + } +} + +/// Grab an env var that is expected to be set inside of build scripts. +fn build_var(key: &'static str) -> Result { + std::env::var(key).map_err(|_| Error::BuildVar(key)) +} diff --git a/crates/tauri-plugin/src/lib.rs b/crates/tauri-plugin/src/lib.rs new file mode 100644 index 0000000..059da5d --- /dev/null +++ b/crates/tauri-plugin/src/lib.rs @@ -0,0 +1,85 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Interface for building Tauri plugins. +//! +//! A Tauri plugin is a Rust crate that extends a Tauri application with commands, state, +//! lifecycle hooks and - optionally - Android and iOS native code. This crate holds the pieces +//! of that interface that are not part of the [`tauri`] crate itself, split in two Cargo +//! features: +//! +//! - **`build`**: helpers for the plugin `build.rs`. [`Builder`] checks the crate against the +//! Tauri plugin conventions (the `links` key must be set and match the crate name, the name +//! cannot contain underscores or be a reserved one), autogenerates the +//! `allow-$command`/`deny-$command` permissions for the plugin commands, parses the +//! permission files, generates their JSON schema and reference documentation, defines the +//! global scope schema and links the Android/iOS projects of the plugin. +//! [`plugin_config`] reads the plugin configuration the Tauri CLI forwards to the build +//! script, and the [`mobile`] module has helpers to patch the generated iOS `Info.plist` and +//! entitlements and the Android manifest. +//! - **`runtime`**: reserved for the runtime side of the plugin interface. It currently +//! exports nothing - use [`tauri::plugin`] to define the plugin itself. +//! +//! # Examples +//! +//! A typical plugin `build.rs`: +//! +//! ```rust,ignore +//! const COMMANDS: &[&str] = &["ping", "execute"]; +//! +//! fn main() { +//! tauri_plugin::Builder::new(COMMANDS) +//! .android_path("android") +//! .ios_path("ios") +//! .build(); +//! } +//! ``` +//! +//! With a configuration type and an iOS `Info.plist` change: +//! +//! ```rust,ignore +//! #[derive(serde::Deserialize)] +//! #[serde(rename_all = "camelCase")] +//! struct Config { +//! camera_usage_description: Option, +//! } +//! +//! fn main() { +//! if let Some(config) = tauri_plugin::plugin_config::("my-plugin") { +//! if let Some(description) = config.camera_usage_description { +//! tauri_plugin::mobile::update_info_plist(|plist| { +//! plist.insert("NSCameraUsageDescription".into(), description.into()); +//! }) +//! .expect("failed to update Info.plist"); +//! } +//! } +//! +//! tauri_plugin::Builder::new(&["take_picture"]).build(); +//! } +//! ``` +//! +//! [`tauri`]: https://docs.rs/tauri/latest/tauri/ +//! [`tauri::plugin`]: https://docs.rs/tauri/latest/tauri/plugin/index.html +//! [`Builder`]: https://docs.rs/tauri-plugin/latest/tauri_plugin/struct.Builder.html +//! [`plugin_config`]: https://docs.rs/tauri-plugin/latest/tauri_plugin/fn.plugin_config.html +//! [`mobile`]: https://docs.rs/tauri-plugin/latest/tauri_plugin/mobile/index.html + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" +)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +#[cfg(feature = "build")] +mod build; +#[cfg(feature = "runtime")] +mod runtime; + +#[cfg(feature = "build")] +#[cfg_attr(docsrs, doc(cfg(feature = "build")))] +pub use build::*; +#[cfg(feature = "runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] +#[allow(unused)] +pub use runtime::*; diff --git a/crates/tauri-plugin/src/runtime.rs b/crates/tauri-plugin/src/runtime.rs new file mode 100644 index 0000000..3d77814 --- /dev/null +++ b/crates/tauri-plugin/src/runtime.rs @@ -0,0 +1,3 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT diff --git a/crates/tauri-runtime-wry/CHANGELOG.md b/crates/tauri-runtime-wry/CHANGELOG.md new file mode 100644 index 0000000..fbb2e1a --- /dev/null +++ b/crates/tauri-runtime-wry/CHANGELOG.md @@ -0,0 +1,1517 @@ +# Changelog + +## \[2.11.4] + +### Bug Fixes + +- [`ba865f2ef`](https://www.github.com/tauri-apps/tauri/commit/ba865f2ef5a4c48f14ed00619699e27a90db47c5) ([#15614](https://www.github.com/tauri-apps/tauri/pull/15614) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix getting the DPI internally leaks `HDC` handles on Windows. This also improves the resizing speed on undecorated windows + +## \[2.11.3] + +### Bug Fixes + +- [`f1fe2d683`](https://www.github.com/tauri-apps/tauri/commit/f1fe2d683c0db87707e4ed788e5aae5f6022d913) ([#15504](https://www.github.com/tauri-apps/tauri/pull/15504) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix `cookies_for_url` can cause deadlocks if called with another windowing/webview method +- [`056069ad0`](https://www.github.com/tauri-apps/tauri/commit/056069ad0f5b537de37ccadac08c1a89507f23f1) ([#15502](https://www.github.com/tauri-apps/tauri/pull/15502) by [@bclarke123](https://www.github.com/tauri-apps/tauri/../../bclarke123)) Fix a `RefCell` `BorrowMutError` panic on mobile: the `Resumed`/`Suspended` event branch held a `windows` borrow across the window-event handlers and the `RunEvent` callback, so any of them that created or closed a window (e.g. from a resume/suspend handler) panicked. + +### Dependencies + +- Upgraded to `tauri-utils@2.9.3` +- Upgraded to `tauri-runtime@2.11.3` + +## \[2.11.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.2` +- Upgraded to `tauri-runtime@2.11.2` + +## \[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 + +- Upgraded to `tauri-utils@2.8.3` +- Upgraded to `tauri-runtime@2.10.1` + +## \[2.10.0] + +### Bug Fixes + +- [`c1d82eb3a`](https://www.github.com/tauri-apps/tauri/commit/c1d82eb3a3fa4b555745ba699edf1cc532030117) ([#14628](https://www.github.com/tauri-apps/tauri/pull/14628) by [@KushalMeghani1644](https://www.github.com/tauri-apps/tauri/../../KushalMeghani1644)) On Linux, keep the WebContext alive to prevent zombie WebKit processes after repeatedly closing all windows and re-opening them. +- [`9b242e40c`](https://www.github.com/tauri-apps/tauri/commit/9b242e40c844189c877a91e513ae6196202d5ae9) ([#14700](https://www.github.com/tauri-apps/tauri/pull/14700) by [@mewi99](https://www.github.com/tauri-apps/tauri/../../mewi99)) Fix compilation errors when targeting BSD. + +### Dependencies + +- Upgraded to `tauri-utils@2.8.2` +- Upgraded to `tauri-runtime@2.10.0` +- [`75057c7c0`](https://www.github.com/tauri-apps/tauri/commit/75057c7c08f0d4d3dd8d10cea4e2217e5d61fe1a) ([#14778](https://www.github.com/tauri-apps/tauri/pull/14778) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) **Breaking Change** for `with_webview` users: Updated webkit2gtk-rs crates to `v2.0.2`. +- [`75057c7c0`](https://www.github.com/tauri-apps/tauri/commit/75057c7c08f0d4d3dd8d10cea4e2217e5d61fe1a) ([#14778](https://www.github.com/tauri-apps/tauri/pull/14778) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Update wry to `v0.54`. + +## \[2.9.3] + +### Bug Fixes + +- [`251203b89`](https://www.github.com/tauri-apps/tauri/commit/251203b8963419cb3b40741767393e8f3c909ef9) ([#14637](https://www.github.com/tauri-apps/tauri/pull/14637) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix `Monitor::work_area` returns logical position and size inside the `PhysicalRect` on Linux + +## \[2.9.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.8.1` +- Upgraded to `tauri-runtime@2.9.2` + +## \[2.9.1] + +### Bug Fixes + +- [`4b6b8690a`](https://www.github.com/tauri-apps/tauri/commit/4b6b8690ab886ebdf1307951cffbe03e31280baa) ([#14347](https://www.github.com/tauri-apps/tauri/pull/14347) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +### Dependencies + +- Upgraded to `tauri-runtime@2.9.1` + +## \[2.9.0] + +### New Features + +- [`f5851ee00`](https://www.github.com/tauri-apps/tauri/commit/f5851ee00d6d1f4d560a220ca5a728fedd525092) ([#14089](https://www.github.com/tauri-apps/tauri/pull/14089)) Adds the `scroll_bar_style` option to the Webview and WebviewWindow builders. + The possible values for this option are gated behind conditional compilation + flags, and will need to be applied using conditional compilation if customised. + +### Bug Fixes + +- [`684791efa`](https://www.github.com/tauri-apps/tauri/commit/684791efa6f3c671a0435d456ac208bca871d8c1) ([#14276](https://www.github.com/tauri-apps/tauri/pull/14276)) Always try to create macOS WebKit webview, even if webkit runtime doesn't get detected correctly + +### Dependencies + +- Upgraded to `tauri-utils@2.8.0` +- Upgraded to `tauri-runtime@2.9.0` + +## \[2.8.1] + +### Bug Fixes + +- [`03e7c1193`](https://www.github.com/tauri-apps/tauri/commit/03e7c1193208716170f120a1d4a39cea0bc21064) ([#14080](https://www.github.com/tauri-apps/tauri/pull/14080) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Ignore initial navigation to `about:blank` so `on_new_window` does not give a warning on first navigation on macOS. + +## \[2.8.0] + +### New Features + +- [`68874c68c`](https://www.github.com/tauri-apps/tauri/commit/68874c68c566638b4c21a3aa67844d1bdaeb6dab) ([#13564](https://www.github.com/tauri-apps/tauri/pull/13564) by [@robertrpf](https://www.github.com/tauri-apps/tauri/../../robertrpf)) Add window focusable attribute and set_focusable API. +- [`22d6bcacb`](https://www.github.com/tauri-apps/tauri/commit/22d6bcacbb2001eb292ebd8c5d021447700f9512) ([#14008](https://www.github.com/tauri-apps/tauri/pull/14008) by [@WSH032](https://www.github.com/tauri-apps/tauri/../../WSH032)) Implement `App::set_device_event_filter` for `AppHandle` also. +- [`33d0b3f0c`](https://www.github.com/tauri-apps/tauri/commit/33d0b3f0c133edebb1c716e2f5942d70509ae347) ([#13876](https://www.github.com/tauri-apps/tauri/pull/13876) by [@thlstsul](https://www.github.com/tauri-apps/tauri/../../thlstsul)) Added `WebviewBuilder::on_document_title_changed` and `WebviewWindowBuilder::on_document_title_changed`. +- [`33d0b3f0c`](https://www.github.com/tauri-apps/tauri/commit/33d0b3f0c133edebb1c716e2f5942d70509ae347) ([#13876](https://www.github.com/tauri-apps/tauri/pull/13876) by [@thlstsul](https://www.github.com/tauri-apps/tauri/../../thlstsul)) Added `WebviewBuilder::on_new_window` and `WebviewWindowBuilder::on_new_window`. +- [`dfadcb764`](https://www.github.com/tauri-apps/tauri/commit/dfadcb764bdf84089a5487005a7b4f3b7cf09494) ([#13661](https://www.github.com/tauri-apps/tauri/pull/13661) by [@WSH032](https://www.github.com/tauri-apps/tauri/../../WSH032)) Added `WebviewDispatch::set_cookie()` and `WebviewDispatch::delete_cookie()`. +- [`5110a762e`](https://www.github.com/tauri-apps/tauri/commit/5110a762e9db978a28a15400bf76e3c864da2a86) ([#13830](https://www.github.com/tauri-apps/tauri/pull/13830) by [@Sky-walkerX](https://www.github.com/tauri-apps/tauri/../../Sky-walkerX)) Added `Window::set_simple_fullscreen`. + +### Bug Fixes + +- [`82e264552`](https://www.github.com/tauri-apps/tauri/commit/82e264552ed3e1d62135edc5803a2c5d92f9c5c7) ([#13968](https://www.github.com/tauri-apps/tauri/pull/13968) by [@s5bug](https://www.github.com/tauri-apps/tauri/../../s5bug)) Use WM_NCDESTROY instead of WM_DESTROY to free window userdata, fixing a double-free occurring in the Windows resizing handler for undecorated windows which caused STATUS_HEAP_CORRUPTION +- [`196ace3c0`](https://www.github.com/tauri-apps/tauri/commit/196ace3c048f81b20ab4d38f09c2f200978722b8) ([#13970](https://www.github.com/tauri-apps/tauri/pull/13970) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Return `tauri_runtime::Error::CreateWindow` instead of panic on window creation failed + +### Dependencies + +- Upgraded to `tauri-utils@2.7.0` +- Upgraded to `tauri-runtime@2.8.0` + +## \[2.7.2] + +### What's Changed + +- [`3025d9095`](https://www.github.com/tauri-apps/tauri/commit/3025d90951bfa152fc6f4255ac5a3fdd0f6353ca) ([#13822](https://www.github.com/tauri-apps/tauri/pull/13822) by [@clearlysid](https://www.github.com/tauri-apps/tauri/../../clearlysid)) Makes some methods on `WindowIdStore`, `WindowsStore` and `WindowWrapper` public + +### Dependencies + +- Upgraded to `tauri-utils@2.6.0` +- Upgraded to `tauri-runtime@2.7.1` + +## \[2.7.1] + +### Bug Fixes + +- [`f010ca5e9`](https://www.github.com/tauri-apps/tauri/commit/f010ca5e9199e6baae65990ab75a83ea29e4fe18) ([#13719](https://www.github.com/tauri-apps/tauri/pull/13719) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Fixed a compile error when using tauri without the `common-controls-v6` feature enabled. + +## \[2.7.0] + +### New Features + +- [`9c16eefa3`](https://www.github.com/tauri-apps/tauri/commit/9c16eefa319b4697bac1d1019bbb5f93eca63173) ([#13629](https://www.github.com/tauri-apps/tauri/pull/13629) by [@sftse](https://www.github.com/tauri-apps/tauri/../../sftse)) Added `x11` Cargo feature (enabled by default). Disabling it is useful for apps that only support Wayland, reducing its size. + **NOTE**: When manually disabling tauri default features, you must enable the `x11` feature to support it. + +### Enhancements + +- [`96ecfca42`](https://www.github.com/tauri-apps/tauri/commit/96ecfca428e4e5d9ff5d5eeed3f94a06a466ed02) ([#13406](https://www.github.com/tauri-apps/tauri/pull/13406) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Check if the webview runtime is accessible when creating a webview, returning an error if it doesn't. + +### Bug Fixes + +- [`f0662e41f`](https://www.github.com/tauri-apps/tauri/commit/f0662e41f4f78ec5a3f88aa76a7367d37d740291) ([#13365](https://www.github.com/tauri-apps/tauri/pull/13365) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix monitor check on the window prevent overflow implementation. +- [`4acae1ec0`](https://www.github.com/tauri-apps/tauri/commit/4acae1ec02f2e0b0501d6b79b0bb862893296abd) ([#13443](https://www.github.com/tauri-apps/tauri/pull/13443) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix `AppHandle::set_theme` crashes or has no effect on macOS if not ran on main thread +- [`78d15e892`](https://www.github.com/tauri-apps/tauri/commit/78d15e892d30fb3f8494f0c00685fb455740a181) ([#13558](https://www.github.com/tauri-apps/tauri/pull/13558) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Allow web fullscreen APIs to work on Windows (e.g. `video.requestFullscreen` and `document.exitFullscreen`) + +### Dependencies + +- Upgraded to `tauri-utils@2.5.0` +- Upgraded to `tauri-runtime@2.7.0` +- [`9c16eefa3`](https://www.github.com/tauri-apps/tauri/commit/9c16eefa319b4697bac1d1019bbb5f93eca63173) ([#13629](https://www.github.com/tauri-apps/tauri/pull/13629) by [@sftse](https://www.github.com/tauri-apps/tauri/../../sftse)) Updated tao to 0.34, wry to 0.52 and webview2-com to 0.38. + +## \[2.6.0] + +### New Features + +- [`dd4f13ce4`](https://www.github.com/tauri-apps/tauri/commit/dd4f13ce4b3cd89cde2fa3f18a063c272f215621) ([#13185](https://www.github.com/tauri-apps/tauri/pull/13185)) MacOS: Add `set_dock_visibility` method to support setting the visibility of the application in the dock. +- [`ea36294cb`](https://www.github.com/tauri-apps/tauri/commit/ea36294cbca98f7725c91d1464fd92e77c89698a) ([#13208](https://www.github.com/tauri-apps/tauri/pull/13208)) Added `WebviewAttributes::input_accessory_view_builder` on iOS. +- [`c1cd0a2dd`](https://www.github.com/tauri-apps/tauri/commit/c1cd0a2ddb5bc3e99451cbe399b5fc9f0035f571) ([#13090](https://www.github.com/tauri-apps/tauri/pull/13090)) macOS/iOS: add option to disable or enable link previews when building a webview (the webkit api has it enabled by default) + + - `WebViewBuilder.allow_link_preview(allow_link_preview: bool)` + - `WebviewWindowBuilder.allow_link_preview(allow_link_preview: bool)` +- [`b072e2b29`](https://www.github.com/tauri-apps/tauri/commit/b072e2b2967640ae4fa1af466ae878c156551edd) ([#9687](https://www.github.com/tauri-apps/tauri/pull/9687)) Add `preventOverflow` config option to prevent the window from overflowing the monitor size on creation + +### Bug Fixes + +- [`07953fb9c`](https://www.github.com/tauri-apps/tauri/commit/07953fb9c3dc09e86657aa16020978e89b03ed56) ([#13222](https://www.github.com/tauri-apps/tauri/pull/13222)) Emit `WindowEvent::Focused` events when using the multiwebview (unstable feature flag) mode on Windows. + +### Dependencies + +- Upgraded to `tauri-utils@2.4.0` +- Upgraded to `tauri-runtime@2.6.0` +- [`bb5faa21f`](https://www.github.com/tauri-apps/tauri/commit/bb5faa21f418dd765ce81b495b56e9c519251b6d) ([#13163](https://www.github.com/tauri-apps/tauri/pull/13163)) Update tao to 0.33. +- [`bb5faa21f`](https://www.github.com/tauri-apps/tauri/commit/bb5faa21f418dd765ce81b495b56e9c519251b6d) ([#13163](https://www.github.com/tauri-apps/tauri/pull/13163)) Update webview2-com to 0.37. +- [`bb5faa21f`](https://www.github.com/tauri-apps/tauri/commit/bb5faa21f418dd765ce81b495b56e9c519251b6d) ([#13163](https://www.github.com/tauri-apps/tauri/pull/13163)) Update windows to 0.61. +- [`bb5faa21f`](https://www.github.com/tauri-apps/tauri/commit/bb5faa21f418dd765ce81b495b56e9c519251b6d) ([#13163](https://www.github.com/tauri-apps/tauri/pull/13163)) Update wry to 0.51. + +## \[2.5.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.3.1` +- Upgraded to `tauri-runtime@2.5.1` + +## \[2.5.0] + +### New Features + +- [`be2e6b85f`](https://www.github.com/tauri-apps/tauri/commit/be2e6b85fed226732b4a98f68cc5d72b4f8f5a13) ([#12944](https://www.github.com/tauri-apps/tauri/pull/12944) by [@Simon-Laux](https://www.github.com/tauri-apps/tauri/../../Simon-Laux)) add `Window.is_always_on_top()` and `WebviewWindow.is_always_on_top()` +- [`20c190691`](https://www.github.com/tauri-apps/tauri/commit/20c19069125c89b2d45a2127278c9ffc2df35fc2) ([#12821](https://www.github.com/tauri-apps/tauri/pull/12821) by [@Simon-Laux](https://www.github.com/tauri-apps/tauri/../../Simon-Laux)) Add `WebviewBuilder.disable_javascript` and `WebviewWindowBuilder.disable_javascript` api to disable JavaScript. +- [`658e5f5d1`](https://www.github.com/tauri-apps/tauri/commit/658e5f5d1dc1bd970ae572a42447448d064a7fee) ([#12668](https://www.github.com/tauri-apps/tauri/pull/12668) by [@thomaseizinger](https://www.github.com/tauri-apps/tauri/../../thomaseizinger)) Add `App::run_return` function. Contrary to `App::run`, this will **not** exit the process but instead return the requested exit-code. This allows the host app to perform further cleanup after Tauri has exited. `App::run_return` is not available on iOS and fallbacks to the regular `App::run` functionality. + + The `App::run_iteration` function is deprecated as part of this because calling it in a loop - as suggested by the name - will cause a busy-loop. +- [`c698a6d6f`](https://www.github.com/tauri-apps/tauri/commit/c698a6d6f3e02548444a4aa0e5220bbc6fc05c74) ([#12818](https://www.github.com/tauri-apps/tauri/pull/12818) by [@Simon-Laux](https://www.github.com/tauri-apps/tauri/../../Simon-Laux)) feat: add `Webview.reload` and `WebviewWindow.reload` +- [`30f5a1553`](https://www.github.com/tauri-apps/tauri/commit/30f5a1553d3c0ce460c9006764200a9210915a44) ([#12366](https://www.github.com/tauri-apps/tauri/pull/12366) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Added `trafficLightPosition` window configuration to set the traffic light buttons position on macOS. +- [`30f5a1553`](https://www.github.com/tauri-apps/tauri/commit/30f5a1553d3c0ce460c9006764200a9210915a44) ([#12366](https://www.github.com/tauri-apps/tauri/pull/12366) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Added `traffic_light_position` window builder method to set the traffic light buttons position on macOS. +- [`cedb24d49`](https://www.github.com/tauri-apps/tauri/commit/cedb24d494b84111daa3206c05196c8b89f1e994) ([#12665](https://www.github.com/tauri-apps/tauri/pull/12665) by [@charrondev](https://www.github.com/tauri-apps/tauri/../../charrondev)) Added `WebviewDispatch::cookies()` and `WebviewDispatch::cookies_for_url()`. + +### Dependencies + +- Upgraded to `tauri-runtime@2.5.0` +- Upgraded to `tauri-utils@2.3.0` + +## \[2.4.1] + +### Bug Fixes + +- [`e103e87f1`](https://www.github.com/tauri-apps/tauri/commit/e103e87f155cf7fa51baa0a48a476463216c0d62) ([#12848](https://www.github.com/tauri-apps/tauri/pull/12848) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Fix crash on Windows because of missing functions on older Windows systems, regression in 2.3.0 + +## \[2.4.0] + +### Enhancements + +- [`a2d36b8c3`](https://www.github.com/tauri-apps/tauri/commit/a2d36b8c34a8dcfc6736797ca5cd4665faf75e7e) ([#12181](https://www.github.com/tauri-apps/tauri/pull/12181) by [@bastiankistner](https://www.github.com/tauri-apps/tauri/../../bastiankistner)) Add an option to change the default background throttling policy (currently for WebKit only). +- [`d6520a21c`](https://www.github.com/tauri-apps/tauri/commit/d6520a21ce02c3e2be2955999946c2cb7bdb07aa) ([#12541](https://www.github.com/tauri-apps/tauri/pull/12541) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Updated `wry` to 0.50, `windows` to 0.60, `webview2-com` to 0.36, and `objc2` to 0.6. This can be a **breaking change** if you use the `with_webview` API! + +### Dependencies + +- Upgraded to `tauri-runtime@2.4.0` +- Upgraded to `tauri-utils@2.2.0` + +## \[2.3.0] + +### New Features + +- [`18bd639f6`](https://www.github.com/tauri-apps/tauri/commit/18bd639f6e22c0188aa219739f367b5bf5ab0398) ([#11798](https://www.github.com/tauri-apps/tauri/pull/11798) by [@lars-berger](https://www.github.com/tauri-apps/tauri/../../lars-berger)) Add `WebviewWindowBuilder/WebviewBuilder::data_store_identifier` on macOS. +- [`dc4d79477`](https://www.github.com/tauri-apps/tauri/commit/dc4d79477665bc3bfefb4048772414cf5d78e3df) ([#11628](https://www.github.com/tauri-apps/tauri/pull/11628) by [@SpikeHD](https://www.github.com/tauri-apps/tauri/../../SpikeHD)) Add `WebviewWindowBuilder/WebviewBuilder::extensions_path` on Linux and Windows. +- [`020ea0556`](https://www.github.com/tauri-apps/tauri/commit/020ea05561348dcd6d2a7df358f8a5190f661ba2) ([#11661](https://www.github.com/tauri-apps/tauri/pull/11661) by [@ahqsoftwares](https://www.github.com/tauri-apps/tauri/../../ahqsoftwares)) Add badging APIs: + + - `Window/WebviewWindow::set_badge_count` for Linux, macOS and IOS. + - `Window/WebviewWindow::set_overlay_icon` for Windows Only. + - `Window/WebviewWindow::set_badge_label`for macOS Only. + +### Dependencies + +- Upgraded to `tauri-runtime@2.3.0` +- Upgraded to `tauri-utils@2.1.1` + +## \[2.2.0] + +### New Features + +- [`4d545ab3c`](https://www.github.com/tauri-apps/tauri/commit/4d545ab3ca228c8a21b966b709f84a0da2864479) ([#11486](https://www.github.com/tauri-apps/tauri/pull/11486) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Added `Window::set_background_color` and `WindowBuilder::background_color`. +- [`f37e97d41`](https://www.github.com/tauri-apps/tauri/commit/f37e97d410c4a219e99f97692da05ca9d8e0ba3a) ([#11477](https://www.github.com/tauri-apps/tauri/pull/11477) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `WebviewWindowBuilder/WebviewBuilder::use_https_scheme` to choose whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android +- [`cbc095ec5`](https://www.github.com/tauri-apps/tauri/commit/cbc095ec5fe7de29b5c9265576d4e071ec159c1c) ([#11451](https://www.github.com/tauri-apps/tauri/pull/11451) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `WebviewWindowBuilder::devtools` and `WebviewBuilder::devtools` to enable or disable devtools for a specific webview. +- [`2a75c64b5`](https://www.github.com/tauri-apps/tauri/commit/2a75c64b5431284e7340e8743d4ea56a62c75466) ([#11469](https://www.github.com/tauri-apps/tauri/pull/11469) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Added `WindowBuilder/WebviewWindowBuilder::window_classname` method to specify the name of the window class on Windows. + +### Bug Fixes + +- [`229d7f8e2`](https://www.github.com/tauri-apps/tauri/commit/229d7f8e220cc8d5ca06eff1ed85cb7d047c1d6c) ([#11616](https://www.github.com/tauri-apps/tauri/pull/11616) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Fix regression in creating child webviews on macOS and Windows, covering the whole window. +- [`8c6d1e8e6`](https://www.github.com/tauri-apps/tauri/commit/8c6d1e8e6c852667bb223b5f4823948868c26d98) ([#11401](https://www.github.com/tauri-apps/tauri/pull/11401) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Fix `App/AppHandle/Window/Webview/WebviewWindow::cursor_position` getter method failing on Linux with `GDK may only be used from the main thread`. +- [`129414faa`](https://www.github.com/tauri-apps/tauri/commit/129414faa4e027c9035d56614682cacc0335a6a0) ([#11569](https://www.github.com/tauri-apps/tauri/pull/11569) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Fix webview not focused by default. + +### Dependencies + +- Upgraded to `tauri-utils@2.1.0` +- Upgraded to `tauri-runtime@2.2.0` + +## \[2.1.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.2` + +## \[2.1.1] + +### Bug Fixes + +- [`ef2482dde`](https://www.github.com/tauri-apps/tauri/commit/ef2482ddecf533181211ee435931fac650495bc5) ([#11366](https://www.github.com/tauri-apps/tauri/pull/11366)) Update wry to 0.46.1 to fix a crash on macOS older than Sequoia. + +## \[2.1.0] + +### Bug Fixes + +- [`2d087ee4b`](https://www.github.com/tauri-apps/tauri/commit/2d087ee4b7d3e8849933f81284e4f5ed1aaa6455) ([#11268](https://www.github.com/tauri-apps/tauri/pull/11268) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) On Linux, fix commands, that use `Webview` or `WebviewWindow` as an argument, receiving an incorrect webview when using multi webviews. +- [`2d087ee4b`](https://www.github.com/tauri-apps/tauri/commit/2d087ee4b7d3e8849933f81284e4f5ed1aaa6455) ([#11268](https://www.github.com/tauri-apps/tauri/pull/11268) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) On Linux, fix events only emitted to first webview only when using multi webviews. +- [`2d087ee4b`](https://www.github.com/tauri-apps/tauri/commit/2d087ee4b7d3e8849933f81284e4f5ed1aaa6455) ([#11268](https://www.github.com/tauri-apps/tauri/pull/11268) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) On Linux, fix custom protocols receiving an incorrect webview label when using multi webviews + +### Dependencies + +- Upgraded to `tauri-runtime@2.1.0` + +## \[2.0.1] + +### What's Changed + +- [`0ab2b3306`](https://www.github.com/tauri-apps/tauri/commit/0ab2b330644b6419f6cee1d5377bfb5cdda2ccf9) ([#11205](https://www.github.com/tauri-apps/tauri/pull/11205) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.1` +- Upgraded to `tauri-runtime@2.0.1` + +## \[2.0.0] + +### What's Changed + +- [`382ed482b`](https://www.github.com/tauri-apps/tauri/commit/382ed482bd08157c39e62f9a0aaad8802f1092cb) Bump MSRV to 1.78. +- [`637285790`](https://www.github.com/tauri-apps/tauri/commit/6372857905ae9c0aedb7f482ddf6cf9f9836c9f2) Promote to v2 stable! + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0` +- Upgraded to `tauri-runtime@2.0.0` + +## \[2.0.0-rc.14] + +### New Features + +- [`a247170e1`](https://www.github.com/tauri-apps/tauri/commit/a247170e1f620a9b012274b11cfe51e90327d6e9) ([#11056](https://www.github.com/tauri-apps/tauri/pull/11056) by [@SpikeHD](https://www.github.com/tauri-apps/tauri/../../SpikeHD)) Expose the ability to enabled browser extensions in WebView2 on Windows. +- [`9014a3f17`](https://www.github.com/tauri-apps/tauri/commit/9014a3f1765ca406ea5c3e5224267a79c52cd53d) ([#11066](https://www.github.com/tauri-apps/tauri/pull/11066) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `WebviewWindow::clear_all_browsing_data` and `Webview::clear_all_browsing_data` to clear the webview browsing data. +- [`95df53a2e`](https://www.github.com/tauri-apps/tauri/commit/95df53a2ed96873cd35a4b14a5e312d07e4e3004) ([#11143](https://www.github.com/tauri-apps/tauri/pull/11143) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Add the ability to set theme dynamically using `Window::set_theme`, `App::set_theme` +- [`d9d2502b4`](https://www.github.com/tauri-apps/tauri/commit/d9d2502b41e39efde679e30c8955006e2ba9ea64) ([#11140](https://www.github.com/tauri-apps/tauri/pull/11140) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `WebviewDispatch::hide` and `WebviewDispatch::show` methods. +- [`de7414aab`](https://www.github.com/tauri-apps/tauri/commit/de7414aab935e45540594ea930eb60bae4dbc979) ([#11154](https://www.github.com/tauri-apps/tauri/pull/11154) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `Window::set_enabled` and `Window::is_enabled` methods + +### Bug Fixes + +- [`62b3a5cd1`](https://www.github.com/tauri-apps/tauri/commit/62b3a5cd1c804440c2130ab36cc3eadb3baf61cb) ([#11043](https://www.github.com/tauri-apps/tauri/pull/11043) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Fix `localStorage` not shared between webviews that use the same data directory. + +### Dependencies + +- Upgraded to `tauri-runtime@2.0.0-rc.13` +- Upgraded to `tauri-utils@2.0.0-rc.13` + +## \[2.0.0-rc.13] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.12` +- Upgraded to `tauri-runtime@2.0.0-rc.12` + +## \[2.0.0-rc.12] + +### Enhancements + +- [`bc4804d48`](https://www.github.com/tauri-apps/tauri/commit/bc4804d4841efefd57fd1f3e147550a3340e2b31) ([#10924](https://www.github.com/tauri-apps/tauri/pull/10924) by [@madsmtm](https://www.github.com/tauri-apps/tauri/../../madsmtm)) Use `objc2` internally and in examples, leading to better memory safety. + +### Breaking Changes + +- [`bc4804d48`](https://www.github.com/tauri-apps/tauri/commit/bc4804d4841efefd57fd1f3e147550a3340e2b31) ([#10924](https://www.github.com/tauri-apps/tauri/pull/10924) by [@madsmtm](https://www.github.com/tauri-apps/tauri/../../madsmtm)) Change the pointer type of `PlatformWebview`'s `inner`, `controller`, `ns_window` and `view_controller` to `c_void`, to avoid publically depending on `objc`. + +## \[2.0.0-rc.11] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.11` +- Upgraded to `tauri-runtime@2.0.0-rc.11` + +## \[2.0.0-rc.10] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.10` +- Upgraded to `tauri-runtime@2.0.0-rc.10` + +## \[2.0.0-rc.9] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.9` +- Upgraded to `tauri-runtime@2.0.0-rc.9` + +## \[2.0.0-rc.8] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.8` +- Upgraded to `tauri-runtime@2.0.0-rc.8` +- [`77056b194`](https://www.github.com/tauri-apps/tauri/commit/77056b194a2aa8be1b9865d707b741a6ed72ec56) ([#10895](https://www.github.com/tauri-apps/tauri/pull/10895) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Update tao to 0.30 and wry to 0.43. + +### Breaking Changes + +- [`5048a7293`](https://www.github.com/tauri-apps/tauri/commit/5048a7293b87b5b93aaefd42dedc0e551e08086c) ([#10840](https://www.github.com/tauri-apps/tauri/pull/10840) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) The `linux-ipc-protocol` feature is now always enabled, so the Cargo feature flag was removed. + This increases the minimum webkit2gtk version to a release that does not affect the minimum target Linux distros for Tauri apps. + +## \[2.0.0-rc.7] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.7` +- Upgraded to `tauri-runtime@2.0.0-rc.7` + +## \[2.0.0-rc.6] + +### Bug Fixes + +- [`793ee0531`](https://www.github.com/tauri-apps/tauri/commit/793ee0531730597e6008c9c0dedabbab7a2bef53) ([#10700](https://www.github.com/tauri-apps/tauri/pull/10700) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Allow hyphens and underscores on app identifiers. + +### What's Changed + +- [`f4d5241b3`](https://www.github.com/tauri-apps/tauri/commit/f4d5241b377d0f7a1b58100ee19f7843384634ac) ([#10731](https://www.github.com/tauri-apps/tauri/pull/10731) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Update documentation icon path. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.6` +- Upgraded to `tauri-runtime@2.0.0-rc.6` + +## \[2.0.0-rc.5] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.5` +- Upgraded to `tauri-runtime@2.0.0-rc.5` + +## \[2.0.0-rc.4] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.4` +- Upgraded to `tauri-runtime@2.0.0-rc.4` + +## \[2.0.0-rc.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.3` +- Upgraded to `tauri-runtime@2.0.0-rc.3` +- [`d39c392b7`](https://www.github.com/tauri-apps/tauri/commit/d39c392b7cec746da423211f9c74632abe4b6af5) ([#10655](https://www.github.com/tauri-apps/tauri/pull/10655) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Update `tao` to 0.29 and `wry` to 0.42. + +## \[2.0.0-rc.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.2` +- Upgraded to `tauri-runtime@2.0.0-rc.2` + +## \[2.0.0-rc.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.1` +- Upgraded to `tauri-runtime@2.0.0-rc.1` + +## \[2.0.0-rc.0] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.0` +- Upgraded to `tauri-runtime@2.0.0-rc.0` + +## \[2.0.0-beta.21] + +### What's Changed + +- [`9546548ec`](https://www.github.com/tauri-apps/tauri/commit/9546548ec0c83ba620b1bc9d1d424a7009d0b423) ([#10297](https://www.github.com/tauri-apps/tauri/pull/10297) by [@pewsheen](https://www.github.com/tauri-apps/tauri/../../pewsheen)) On macOS, set default titlebar style to `Visible` to prevent webview move out of the view. +- [`da25f7353`](https://www.github.com/tauri-apps/tauri/commit/da25f7353070477ba969851e974379d7666d6806) ([#10242](https://www.github.com/tauri-apps/tauri/pull/10242) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `inner_size_constraints` method on `WindowBuilder` trait and `set_size_constraints` method on `WindowDispatch` trait. + +### Dependencies + +- Upgraded to `tauri-runtime@2.0.0-beta.21` + +## \[2.0.0-beta.20] + +### Bug Fixes + +- [`afb102c59`](https://www.github.com/tauri-apps/tauri/commit/afb102c59ba0de27e330589269001e0d2a01576d) ([#10211](https://www.github.com/tauri-apps/tauri/pull/10211) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix window edge not working after setting resizable false and decorated false dynamically + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.19` +- Upgraded to `tauri-runtime@2.0.0-beta.20` + +## \[2.0.0-beta.19] + +### Bug Fixes + +- [`f29b78811`](https://www.github.com/tauri-apps/tauri/commit/f29b78811080bc8313459f34545152d939c62bf6) ([#9862](https://www.github.com/tauri-apps/tauri/pull/9862)) On Windows, handle resizing undecorated windows natively which improves performance and fixes a couple of annoyances with previous JS implementation: + + - No more cursor flickering when moving the cursor across an edge. + - Can resize from top even when `data-tauri-drag-region` element exists there. + - Upon starting rezing, clicks don't go through elements behind it so no longer accidental clicks. + +### What's Changed + +- [`669b9c6b5`](https://www.github.com/tauri-apps/tauri/commit/669b9c6b5af791129b77ee440dacaa98288c906b) ([#9621](https://www.github.com/tauri-apps/tauri/pull/9621)) Set the gtk application to the identifier defined in `tauri.conf.json` to ensure the app uniqueness. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.18` +- Upgraded to `tauri-runtime@2.0.0-beta.19` +- [`d4c908cfb`](https://www.github.com/tauri-apps/tauri/commit/d4c908cfb8c567abdaf99b85f65f482ea81967e5) ([#10048](https://www.github.com/tauri-apps/tauri/pull/10048)) Update `windows` crate to version `0.57` and `webview2-com` crate to version `0.31` + +## \[2.0.0-beta.18] + +### Enhancements + +- [`276c4b143`](https://www.github.com/tauri-apps/tauri/commit/276c4b14385e17cff15a2e5b57fd2a7cddef9f08)([#9832](https://www.github.com/tauri-apps/tauri/pull/9832)) Added `WindowBuilder::get_theme`. + +### What's Changed + +- [`9ac930380`](https://www.github.com/tauri-apps/tauri/commit/9ac930380a5df3fe700e68e75df8684d261ca292)([#9850](https://www.github.com/tauri-apps/tauri/pull/9850)) Emit `cargo:rustc-check-cfg` instruction so Cargo validates custom cfg attributes on Rust 1.80 (or nightly-2024-05-05). + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.17` +- Upgraded to `tauri-runtime@2.0.0-beta.18` + +## \[2.0.0-beta.17] + +### Security fixes + +- [`d950ac123`](https://www.github.com/tauri-apps/tauri/commit/d950ac1239817d17324c035e5c4769ee71fc197d) Only process IPC commands from the main frame. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.16` +- Upgraded to `tauri-runtime@2.0.0-beta.17` + +## \[2.0.0-beta.16] + +### New Features + +- [`78839b6d2`](https://www.github.com/tauri-apps/tauri/commit/78839b6d2f1005a5e6e1a54b0305136bae0c3a7c)([#4865](https://www.github.com/tauri-apps/tauri/pull/4865)) Add `RunEvent::Reopen` for handle click on dock icon on macOS. + +### Bug Fixes + +- [`c0bcc6c0b`](https://www.github.com/tauri-apps/tauri/commit/c0bcc6c0b74ef7167de85002a5c29b6f731bae41)([#9717](https://www.github.com/tauri-apps/tauri/pull/9717)) Fixes redraw tracing span not closing. + +### What's Changed + +- [`783ef0f2d`](https://www.github.com/tauri-apps/tauri/commit/783ef0f2d331f520fa827c3112f36c0b519b9292)([#9647](https://www.github.com/tauri-apps/tauri/pull/9647)) Changed `WebviewDispatch::url` getter to return a result. + +### Dependencies + +- Upgraded to `tauri-runtime@2.0.0-beta.16` +- Upgraded to `tauri-utils@2.0.0-beta.15` + +## \[2.0.0-beta.15] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.14` +- Upgraded to `tauri-runtime@2.0.0-beta.15` + +## \[2.0.0-beta.14] + +### New Features + +- [`477bb8cd4`](https://www.github.com/tauri-apps/tauri/commit/477bb8cd4ea88ade3f6c1f268ad1701a68150161)([#9297](https://www.github.com/tauri-apps/tauri/pull/9297)) Add `App/AppHandle/Window/Webview/WebviewWindow::cursor_position` getter to get the current cursor position. + +### Dependencies + +- Upgraded to `tauri-runtime@2.0.0-beta.14` + +## \[2.0.0-beta.13] + +### What's Changed + +- [`005fe8ce1`](https://www.github.com/tauri-apps/tauri/commit/005fe8ce1ef71ea46a7d86f98bdf397ca81eb920)([#9410](https://www.github.com/tauri-apps/tauri/pull/9410)) Fix `closable`, `maximizable` and `minimizable` options not taking effect when used in tauri.conf.json or from JS APIs. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.13` +- Upgraded to `tauri-runtime@2.0.0-beta.13` + +## \[2.0.0-beta.12] + +### New Features + +- [`58a7a552d`](https://www.github.com/tauri-apps/tauri/commit/58a7a552d739b77b71d61af11c53f7f2dc7a6e7e)([#9378](https://www.github.com/tauri-apps/tauri/pull/9378)) Added the `set_zoom` function to the webview API. +- [`58a7a552d`](https://www.github.com/tauri-apps/tauri/commit/58a7a552d739b77b71d61af11c53f7f2dc7a6e7e)([#9378](https://www.github.com/tauri-apps/tauri/pull/9378)) Add `zoom_hotkeys_enabled` to enable browser native zoom controls on creating webviews. + +### Bug Fixes + +- [`2f20fdf1d`](https://www.github.com/tauri-apps/tauri/commit/2f20fdf1d6b92fa8b9b38caf7321c3ce3e895f1b)([#9361](https://www.github.com/tauri-apps/tauri/pull/9361)) Fixes an issue causing compilation to fail for i686 and armv7 32-bit targets. +- [`4c2e7477e`](https://www.github.com/tauri-apps/tauri/commit/4c2e7477e6869e2ce0578265825bbd42a5f28393)([#9309](https://www.github.com/tauri-apps/tauri/pull/9309)) Fix window centering not taking taskbar into account on Windows +- [`02eaf0787`](https://www.github.com/tauri-apps/tauri/commit/02eaf07872b90225eead22ecdd6ff0a9ed5dd0ff)([#9428](https://www.github.com/tauri-apps/tauri/pull/9428)) Fixes `inner_size` crash when the window has no webviews. +- [`f22ea2998`](https://www.github.com/tauri-apps/tauri/commit/f22ea2998619cc09c2d426f7b42211a80eed578e)([#9465](https://www.github.com/tauri-apps/tauri/pull/9465)) Revert the [fix](https://github.com/tauri-apps/tauri/pull/9246) for webview's visibility doesn't change with the app window on Windows as it caused white flashes on show/restore. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.12` +- Upgraded to `tauri-runtime@2.0.0-beta.12` + +## \[2.0.0-beta.11] + +### Bug Fixes + +- [`4c0c780e0`](https://www.github.com/tauri-apps/tauri/commit/4c0c780e00d8851be38cb1c22f636d9e4ed34a23)([#2690](https://www.github.com/tauri-apps/tauri/pull/2690)) Fix window inner size evaluation on macOS. +- [`5bd47b446`](https://www.github.com/tauri-apps/tauri/commit/5bd47b44673f74b1b4e8d704b7a95539915ede76)([#9246](https://www.github.com/tauri-apps/tauri/pull/9246)) Fix webview's visibility doesn't change with the app window + +### What's Changed + +- [`06833f4fa`](https://www.github.com/tauri-apps/tauri/commit/06833f4fa8e63ecc55fe3fc874a9e397e77a5709)([#9100](https://www.github.com/tauri-apps/tauri/pull/9100)) Updated `http` crate to `1.1` + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.11` +- Upgraded to `tauri-runtime@2.0.0-beta.11` +- [`06833f4fa`](https://www.github.com/tauri-apps/tauri/commit/06833f4fa8e63ecc55fe3fc874a9e397e77a5709)([#9100](https://www.github.com/tauri-apps/tauri/pull/9100)) Upgraded to `wry@0.38.0` + +### Breaking Changes + +- [`06833f4fa`](https://www.github.com/tauri-apps/tauri/commit/06833f4fa8e63ecc55fe3fc874a9e397e77a5709)([#9100](https://www.github.com/tauri-apps/tauri/pull/9100)) The IPC handler closure now receives a `http::Request` instead of a String representing the request body. +- [`06833f4fa`](https://www.github.com/tauri-apps/tauri/commit/06833f4fa8e63ecc55fe3fc874a9e397e77a5709)([#9100](https://www.github.com/tauri-apps/tauri/pull/9100)) Rename `FileDrop` to `DragDrop` on structs, enums and enum variants. Also renamed `file_drop` to `drag_drop` on fields and function names. + +## \[2.0.0-beta.10] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.10` +- Upgraded to `tauri-runtime@2.0.0-beta.10` + +## \[2.0.0-beta.9] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.9` +- Upgraded to `tauri-runtime@2.0.0-beta.9` + +## \[2.0.0-beta.8] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.8` +- Upgraded to `tauri-runtime@2.0.0-beta.8` + +## \[2.0.0-beta.7] + +### New Features + +- [`46de49aaa`](https://www.github.com/tauri-apps/tauri/commit/46de49aaad4a148fafc31d591be0e2ed12256507)([#9059](https://www.github.com/tauri-apps/tauri/pull/9059)) Added `set_auto_resize` method for the webview. + +### Enhancements + +- [`46de49aaa`](https://www.github.com/tauri-apps/tauri/commit/46de49aaad4a148fafc31d591be0e2ed12256507)([#9059](https://www.github.com/tauri-apps/tauri/pull/9059)) When using the `unstable` feature flag, `WebviewWindow` will internally use the child webview interface for flexibility. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.7` +- Upgraded to `tauri-runtime@2.0.0-beta.7` + +## \[2.0.0-beta.6] + +### Bug Fixes + +- [`222a96b7`](https://www.github.com/tauri-apps/tauri/commit/222a96b74b145fb48d3f0c109897962d56fae57a)([#8999](https://www.github.com/tauri-apps/tauri/pull/8999)) Fixes auto resize and positioning when using the multiwebview mode. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.6` +- Upgraded to `tauri-runtime@2.0.0-beta.6` + +## \[2.0.0-beta.5] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.5` +- Upgraded to `tauri-runtime@2.0.0-beta.5` + +## \[2.0.0-beta.4] + +### New Features + +- [`fdcaf935`](https://www.github.com/tauri-apps/tauri/commit/fdcaf935fa75ecfa2806939c4faad4fe9e880386)([#8939](https://www.github.com/tauri-apps/tauri/pull/8939)) Added the `reparent` function to the webview API. + +### Bug Fixes + +- [`6e3bd4b9`](https://www.github.com/tauri-apps/tauri/commit/6e3bd4b9f815ddde8b5eaf9f69991d4de80bb584)([#8942](https://www.github.com/tauri-apps/tauri/pull/8942)) Fix window centering not taking monitor scale into account + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.4` +- Upgraded to `tauri-runtime@2.0.0-beta.4` +- [`d75713ac`](https://www.github.com/tauri-apps/tauri/commit/d75713ac6c6115534e520303f5c38aa78704de69)([#8936](https://www.github.com/tauri-apps/tauri/pull/8936)) Upgraded to `wry@0.37.0` + +## \[2.0.0-beta.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.3` +- Upgraded to `tauri-runtime@2.0.0-beta.3` + +## \[2.0.0-beta.2] + +### What's Changed + +- [`76ce9f61`](https://www.github.com/tauri-apps/tauri/commit/76ce9f61dd3c5bdd589c7557543894e1f770dd16)([#3002](https://www.github.com/tauri-apps/tauri/pull/3002)) Enhance centering a newly created window, it will no longer jump to center after being visible. +- [`16e550ec`](https://www.github.com/tauri-apps/tauri/commit/16e550ec1503765158cdc3bb2a20e70ec710e981)([#8844](https://www.github.com/tauri-apps/tauri/pull/8844)) Add `WebviewEvent`, `RunEvent::WebviewEvent` and `WebviewDispatch::on_webview_event`. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.2` +- Upgraded to `tauri-runtime@2.0.0-beta.2` +- [`2f55bfec`](https://www.github.com/tauri-apps/tauri/commit/2f55bfecbf0244f3b5aa1ad7622182fca3fcdcbb)([#8795](https://www.github.com/tauri-apps/tauri/pull/8795)) Update `wry` to 0.36. + +### Breaking Changes + +- [`2f55bfec`](https://www.github.com/tauri-apps/tauri/commit/2f55bfecbf0244f3b5aa1ad7622182fca3fcdcbb)([#8795](https://www.github.com/tauri-apps/tauri/pull/8795)) Update raw-window-handle to 0.6. + +## \[2.0.0-beta.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.1` +- Upgraded to `tauri-runtime@2.0.0-beta.1` + +## \[2.0.0-beta.0] + +### New Features + +- [`af610232`](https://www.github.com/tauri-apps/tauri/commit/af6102327376884364b2075b468bdf08ee0d02aa)([#8710](https://www.github.com/tauri-apps/tauri/pull/8710)) Added `Window::destroy` to force close a window. +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) Add multiwebview support behind the `unstable` feature flag. See `WindowBuilder` and `WebviewBuilder` for more information. +- [`00e15675`](https://www.github.com/tauri-apps/tauri/commit/00e1567584721644797b587205187f9cbe4e5cd1)([#8708](https://www.github.com/tauri-apps/tauri/pull/8708)) Added `RuntimeHandle::request_exit` function. + +### Bug Fixes + +- [`95da1a27`](https://www.github.com/tauri-apps/tauri/commit/95da1a27476e01e06f6ce0335df8535b662dd9c4)([#8713](https://www.github.com/tauri-apps/tauri/pull/8713)) Fix calling `set_activation_policy` when the event loop is running. + +### What's Changed + +- [`9f8037c2`](https://www.github.com/tauri-apps/tauri/commit/9f8037c2882abac19582025001675370f0d7b669)([#8633](https://www.github.com/tauri-apps/tauri/pull/8633)) On Windows, fix decorated window not transparent initially until resized. +- [`9eaeb5a8`](https://www.github.com/tauri-apps/tauri/commit/9eaeb5a8cd95ae24b5e66205bdc2763cb7f965ce)([#8622](https://www.github.com/tauri-apps/tauri/pull/8622)) Added `WindowBuilder::transient_for` and Renamed `WindowBuilder::owner_window` to `WindowBuilder::owner` and `WindowBuilder::parent_window` to `WindowBuilder::parent`. +- [`7f033f6d`](https://www.github.com/tauri-apps/tauri/commit/7f033f6dcd54c69a4193765a5c1584755ba92c61)([#8537](https://www.github.com/tauri-apps/tauri/pull/8537)) Add `Window::start_resize_dragging` and `ResizeDirection` enum. +- [`6639a579`](https://www.github.com/tauri-apps/tauri/commit/6639a579c76d45210f33a72d37e21d4c5a9d334b)([#8441](https://www.github.com/tauri-apps/tauri/pull/8441)) Added the `WindowConfig::proxy_url` `WebviewBuilder::proxy_url() / WebviewWindowBuilder::proxy_url()` options when creating a webview. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.0` +- Upgraded to `tauri-runtime@2.0.0-beta.0` +- Upgrated to `tao@0.25`. + +### Breaking Changes + +- [`af610232`](https://www.github.com/tauri-apps/tauri/commit/af6102327376884364b2075b468bdf08ee0d02aa)([#8710](https://www.github.com/tauri-apps/tauri/pull/8710)) `WindowDispatch::close` now triggers the `CloseRequested` flow. +- [`9eaeb5a8`](https://www.github.com/tauri-apps/tauri/commit/9eaeb5a8cd95ae24b5e66205bdc2763cb7f965ce)([#8622](https://www.github.com/tauri-apps/tauri/pull/8622)) Changed `WindowBuilder::with_config` to take a reference to a `WindowConfig` instead of an owned value. + +## \[1.0.0-alpha.9] + +### New Features + +- [`29ced5ce`](https://www.github.com/tauri-apps/tauri/commit/29ced5ceec40b2934094ade2db9a8855f294e1d1)([#8159](https://www.github.com/tauri-apps/tauri/pull/8159)) Added download event closure via `PendingWindow::download_handler`. + +### Enhancements + +- [`d621d343`](https://www.github.com/tauri-apps/tauri/commit/d621d3437ce3947175eecf345b2c6d1c4c7ce020)([#8607](https://www.github.com/tauri-apps/tauri/pull/8607)) Added tracing for window startup, plugins, `Window::eval`, events, IPC, updater and custom protocol request handlers behind the `tracing` feature flag. + +### What's Changed + +- [`cb640c8e`](https://www.github.com/tauri-apps/tauri/commit/cb640c8e949a3d78d78162e2e61b51bf8afae983)([#8393](https://www.github.com/tauri-apps/tauri/pull/8393)) Fix `RunEvent::WindowEvent(event: WindowEvent::FileDrop(FileDropEvent))` never triggered and always prevent default OS behavior when `disable_file_drop_handler` is not used. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.13` +- Upgraded to `tauri-runtime@1.0.0-alpha.8` + +## \[1.0.0-alpha.8] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.12` +- Upgraded to `tauri-runtime@1.0.0-alpha.7` + +## \[1.0.0-alpha.7] + +### Dependencies + +- Upgraded to `tauri-runtime@1.0.0-alpha.6` +- [\`\`](https://www.github.com/tauri-apps/tauri/commit/undefined) Update to wry v0.35. + +## \[1.0.0-alpha.6] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.11` +- Upgraded to `tauri-runtime@1.0.0-alpha.5` + +## \[1.0.0-alpha.5] + +### New Features + +- [`74d2464d`](https://www.github.com/tauri-apps/tauri/commit/74d2464d0e490fae341ad73bdf2964cf215fe6c5)([#8116](https://www.github.com/tauri-apps/tauri/pull/8116)) Added `on_page_load` hook for `PendingWindow`. + +### Enhancements + +- [`c6c59cf2`](https://www.github.com/tauri-apps/tauri/commit/c6c59cf2373258b626b00a26f4de4331765dd487) Pull changes from Tauri 1.5 release. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.10` +- Upgraded to `tauri-runtime@1.0.0-alpha.4` +- [`9580df1d`](https://www.github.com/tauri-apps/tauri/commit/9580df1d7b027befb9e5f025ea2cbaf2dcc82c8e)([#8084](https://www.github.com/tauri-apps/tauri/pull/8084)) Upgrade `gtk` to 0.18. +- [`c7c2507d`](https://www.github.com/tauri-apps/tauri/commit/c7c2507da16a9beb71bf06745fe7ac1325ab7c2a)([#8035](https://www.github.com/tauri-apps/tauri/pull/8035)) Update `windows` to version `0.51` and `webview2-com` to version `0.27` +- [`9580df1d`](https://www.github.com/tauri-apps/tauri/commit/9580df1d7b027befb9e5f025ea2cbaf2dcc82c8e)([#8084](https://www.github.com/tauri-apps/tauri/pull/8084)) Updated to wry@0.34, removing the `dox` feature flag. + +## \[1.0.0-alpha.4] + +### New Features + +- [`c085adda`](https://www.github.com/tauri-apps/tauri/commit/c085addab58ba851398373c6fd13f9cb026d71e8)([#8009](https://www.github.com/tauri-apps/tauri/pull/8009)) Added `set_progress_bar` to `Window`. +- [`c1ec0f15`](https://www.github.com/tauri-apps/tauri/commit/c1ec0f155118527361dd5645d920becbc8afd569)([#7933](https://www.github.com/tauri-apps/tauri/pull/7933)) Added `Window::set_always_on_bottom` and the `always_on_bottom` option when creating a window. +- [`880266a7`](https://www.github.com/tauri-apps/tauri/commit/880266a7f697e1fe58d685de3bb6836ce5251e92)([#8031](https://www.github.com/tauri-apps/tauri/pull/8031)) Bump the MSRV to 1.70. + +### Dependencies + +- Upgraded to `tauri-runtime@1.0.0-alpha.3` +- Upgraded to `tauri-utils@2.0.0-alpha.9` + +### Breaking Changes + +- [`8b166e9b`](https://www.github.com/tauri-apps/tauri/commit/8b166e9bf82e69ddb3200a3a825614980bd8d433)([#7949](https://www.github.com/tauri-apps/tauri/pull/7949)) Check if automation is enabled with the `TAURI_WEBVIEW_AUTOMATION` environment variable instead of `TAURI_AUTOMATION`. +- [`2558fab8`](https://www.github.com/tauri-apps/tauri/commit/2558fab861006936296e8511e43ccd69a38f61b0)([#7939](https://www.github.com/tauri-apps/tauri/pull/7939)) Changed `WebviewId` to be an alias for `u32` instead of `u64` + +## \[1.0.0-alpha.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.8` +- Upgraded to `tauri-runtime@1.0.0-alpha.2` + +## \[1.0.0-alpha.2] + +### Bug Fixes + +- [`d5074af5`](https://www.github.com/tauri-apps/tauri/commit/d5074af562b2b5cb6c5711442097c4058af32db6)([#7801](https://www.github.com/tauri-apps/tauri/pull/7801)) Fixes custom protocol not working on Windows. + +## \[1.0.0-alpha.1] + +### Enhancements + +- [`0d63732b`](https://www.github.com/tauri-apps/tauri/commit/0d63732b962e71b98430f8d7b34ea5b59a2e8bb4)([#7754](https://www.github.com/tauri-apps/tauri/pull/7754)) Update wry to 0.32 to include asynchronous custom protocol support. + +### What's Changed + +- [`6177150b`](https://www.github.com/tauri-apps/tauri/commit/6177150b6f83b52ca359d6e20f7e540f7554e4eb)([#7601](https://www.github.com/tauri-apps/tauri/pull/7601)) Changed `FileDropEvent` to include drop and hover position. + +### Dependencies + +- Upgraded to `tauri-runtime@1.0.0-alpha.1` + +### Breaking Changes + +- [`0d63732b`](https://www.github.com/tauri-apps/tauri/commit/0d63732b962e71b98430f8d7b34ea5b59a2e8bb4)([#7754](https://www.github.com/tauri-apps/tauri/pull/7754)) `tauri-runtime` no longer implements its own HTTP types and relies on the `http` crate instead. + +## \[1.0.0-alpha.0] + +### New Features + +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) Add `Dispatch::default_vbox` +- [`84c41597`](https://www.github.com/tauri-apps/tauri/commit/84c4159754b2e59244211ed9e1fc702d851a0562)([#6394](https://www.github.com/tauri-apps/tauri/pull/6394)) Added `primary_monitor` and `available_monitors` to `Runtime` and `RuntimeHandle`. +- [`3b98141a`](https://www.github.com/tauri-apps/tauri/commit/3b98141aa26f74c641a4090874247b97079bd58a)([#3736](https://www.github.com/tauri-apps/tauri/pull/3736)) Added the `Opened` variant to `RunEvent`. +- [`2a000e15`](https://www.github.com/tauri-apps/tauri/commit/2a000e150d02dff28c8b20ad097b29e209160045)([#7235](https://www.github.com/tauri-apps/tauri/pull/7235)) Implement navigate method + +### Dependencies + +- Upgraded to `tauri-runtime@1.0.0-alpha.0` +- Upgraded to `tauri-utils@2.0.0-alpha.7` + +### Breaking Changes + +- [`fbeb5b91`](https://www.github.com/tauri-apps/tauri/commit/fbeb5b9185baeda19e865228179e3e44c165f1d9)([#7170](https://www.github.com/tauri-apps/tauri/pull/7170)) Removed the `linux-headers` feature (now always enabled) and added `linux-protocol-body`. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) `Dispatch::create_window`, `Runtime::create_window` and `RuntimeHandle::create_window` has been changed to accept a 3rd parameter which is a closure that takes `RawWindow` and to be executed right after the window is created and before the webview is added to the window. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) System tray and menu related APIs and structs have all been removed and are now implemented in tauri outside of the runtime-space. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) `Runtime::new` and `Runtime::new_any_thread` now accept a `RuntimeInitArgs`. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) Removed `system-tray` feature flag + +## \[0.13.0-alpha.6] + +### New Features + +- [`e0f0dce2`](https://www.github.com/tauri-apps/tauri/commit/e0f0dce220730e2822fc202463aedf0166145de7)([#6442](https://www.github.com/tauri-apps/tauri/pull/6442)) Added the `window_effects` option when creating a window and `Window::set_effects` to change it at runtime. + +## \[0.13.0-alpha.5] + +- [`39f1b04f`](https://www.github.com/tauri-apps/tauri/commit/39f1b04f7be4966488484829cd54c8ce72a04200)([#6943](https://www.github.com/tauri-apps/tauri/pull/6943)) Moved the `event` JS APIs to a plugin. +- [`3188f376`](https://www.github.com/tauri-apps/tauri/commit/3188f3764978c6d1452ee31d5a91469691e95094)([#6883](https://www.github.com/tauri-apps/tauri/pull/6883)) Bump the MSRV to 1.65. +- [`cebd7526`](https://www.github.com/tauri-apps/tauri/commit/cebd75261ac71b98976314a450cb292eeeec1515)([#6728](https://www.github.com/tauri-apps/tauri/pull/6728)) Moved the `clipboard` feature to its own plugin in the plugins-workspace repository. +- [`3f17ee82`](https://www.github.com/tauri-apps/tauri/commit/3f17ee82f6ff21108806edb7b00500b8512b8dc7)([#6737](https://www.github.com/tauri-apps/tauri/pull/6737)) Moved the `global-shortcut` feature to its own plugin in the plugins-workspace repository. +- [`31444ac1`](https://www.github.com/tauri-apps/tauri/commit/31444ac196add770f2ad18012d7c18bce7538f22)([#6725](https://www.github.com/tauri-apps/tauri/pull/6725)) Update `wry` to `0.28` + +## \[0.13.0-alpha.4] + +- Added `android` configuration object under `tauri > bundle`. + - Bumped due to a bump in tauri-utils. + - [db4c9dc6](https://www.github.com/tauri-apps/tauri/commit/db4c9dc655e07ee2184fe04571f500f7910890cd) feat(core): add option to configure Android's minimum SDK version ([#6651](https://www.github.com/tauri-apps/tauri/pull/6651)) on 2023-04-07 + +## \[0.13.0-alpha.3] + +- Pull changes from Tauri 1.3 release. + - [](https://www.github.com/tauri-apps/tauri/commit/undefined) on undefined + +## \[0.13.0-alpha.2] + +- Add `find_class`, `run_on_android_context` on `RuntimeHandle`. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 +- Allow a wry plugin to be registered at runtime. + - [ae296f3d](https://www.github.com/tauri-apps/tauri/commit/ae296f3de16fb6a8badbad5555075a5861681fe5) refactor(tauri-runtime-wry): register runtime plugin after run() ([#6478](https://www.github.com/tauri-apps/tauri/pull/6478)) on 2023-03-17 +- Added the `shadow` option when creating a window and `Window::set_shadow`. + - [a81750d7](https://www.github.com/tauri-apps/tauri/commit/a81750d779bc72f0fdb7de90b7fbddfd8049b328) feat(core): add shadow APIs ([#6206](https://www.github.com/tauri-apps/tauri/pull/6206)) on 2023-02-08 +- Implemented `with_webview` on Android and iOS. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 + +## \[0.13.0-alpha.1] + +- Update gtk to 0.16. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Bump the MSRV to 1.64. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Update wry to 0.26. + - [f0a1d9cd](https://www.github.com/tauri-apps/tauri/commit/f0a1d9cdbcfb645ce1c5f1cdd597f764991772cd) chore: update rfd and wry versions ([#6174](https://www.github.com/tauri-apps/tauri/pull/6174)) on 2023-02-03 + +## \[0.13.0-alpha.0] + +- Support `with_webview` for Android platform alowing execution of JNI code in context. + - [8ea87e9c](https://www.github.com/tauri-apps/tauri/commit/8ea87e9c9ca8ba4c7017c8281f78aacd08f45785) feat(android): with_webview access for jni execution ([#5148](https://www.github.com/tauri-apps/tauri/pull/5148)) on 2022-09-08 + +## \[0.14.5] + +### What's Changed + +- [`d42668ce`](https://www.github.com/tauri-apps/tauri/commit/d42668ce17494ab778f436aaa9b216d6db3f0b31)([#9003](https://www.github.com/tauri-apps/tauri/pull/9003)) Fix panic during intialization on wayland when using `clipboard` feature, instead propagate the error during API usage. + +## \[0.14.4] + +### Bug Fixes + +- [`24210735`](https://www.github.com/tauri-apps/tauri/commit/2421073576a6d45783176be57b0188668558aff7)([#8117](https://www.github.com/tauri-apps/tauri/pull/8117)) Fixes a crash on macOS when accessing the windows map. +- [`510b6226`](https://www.github.com/tauri-apps/tauri/commit/510b62261c70331ce3f5bfd24137dac1bc4a0bbe)([#8822](https://www.github.com/tauri-apps/tauri/pull/8822)) Add missing `arboard` feature flag to prevent panics in wayland session. + +## \[0.14.3] + +### Bug Fixes + +- [`0d0501cb`](https://www.github.com/tauri-apps/tauri/commit/0d0501cb7b5e767c51a3697a148acfe84211a7ad)([#8394](https://www.github.com/tauri-apps/tauri/pull/8394)) Use `arboard` instead of `tao` clipboard implementation to prevent a crash. +- [`b2f83f03`](https://www.github.com/tauri-apps/tauri/commit/b2f83f03a872baa91e2b6bbb22a3e7a5cd975dc0)([#8402](https://www.github.com/tauri-apps/tauri/pull/8402)) Use `Arc` instead of `Rc` to prevent crashes on macOS. + +### Dependencies + +- Upgraded to `tauri-utils@1.5.2` +- Upgraded to `tauri-runtime@0.14.2` + +## \[0.14.2] + +### Enhancements + +- [`5e05236b`](https://www.github.com/tauri-apps/tauri/commit/5e05236b4987346697c7caae0567d3c50714c198)([#8289](https://www.github.com/tauri-apps/tauri/pull/8289)) Added tracing for window startup, plugins, `Window::eval`, events, IPC, updater and custom protocol request handlers behind the `tracing` feature flag. + +## \[0.14.1] + +### Enhancements + +- [`9aa34ada`](https://www.github.com/tauri-apps/tauri/commit/9aa34ada5769dbefa7dfe5f7a6288b3d20b294e4)([#7645](https://www.github.com/tauri-apps/tauri/pull/7645)) Add setting to switch to `http://.localhost/` for custom protocols on Windows. + +### Bug Fixes + +- [`4bf1e85e`](https://www.github.com/tauri-apps/tauri/commit/4bf1e85e6bf85a7ec92d50c8465bc0588a6399d8)([#7722](https://www.github.com/tauri-apps/tauri/pull/7722)) Properly respect the `focused` option when creating the webview. + +### Dependencies + +- Upgraded to `tauri-utils@1.5.0` +- Upgraded to `tauri-runtime@0.14.1` + +## \[0.14.0] + +### New Features + +- [`c4d6fb4b`](https://www.github.com/tauri-apps/tauri/commit/c4d6fb4b1ea8acf02707a9fe5dcab47c1c5bae7b)([#2353](https://www.github.com/tauri-apps/tauri/pull/2353)) Added the `maximizable`, `minimizable` and `closable` methods to `WindowBuilder`. +- [`c4d6fb4b`](https://www.github.com/tauri-apps/tauri/commit/c4d6fb4b1ea8acf02707a9fe5dcab47c1c5bae7b)([#2353](https://www.github.com/tauri-apps/tauri/pull/2353)) Added `set_maximizable`, `set_minimizable`, `set_closable`, `is_maximizable`, `is_minimizable` and `is_closable` methods to the `Dispatch` trait. +- [`000104bc`](https://www.github.com/tauri-apps/tauri/commit/000104bc3bc0c9ff3d20558ab9cf2080f126e9e0)([#6472](https://www.github.com/tauri-apps/tauri/pull/6472)) Add `Window::is_focused` getter. + +### Enhancements + +- [`d2710e9d`](https://www.github.com/tauri-apps/tauri/commit/d2710e9d2e8fd93975ef6494512370faa8cb3b7e)([#6944](https://www.github.com/tauri-apps/tauri/pull/6944)) Unpin `time`, `ignore`, and `winnow` crate versions. Developers now have to pin crates if needed themselves. A list of crates that need pinning to adhere to Tauri's MSRV will be visible in Tauri's GitHub workflow: https://github.com/tauri-apps/tauri/blob/dev/.github/workflows/test-core.yml#L85. + +### Bug Fixes + +- [`b41b57eb`](https://www.github.com/tauri-apps/tauri/commit/b41b57ebb27befd366db5befaafb6043c18fdfef)([#7105](https://www.github.com/tauri-apps/tauri/pull/7105)) Fix panics when registering an invalid global shortcuts or checking it is registered and return proper errors instead. + +### What's Changed + +- [`076e1a81`](https://www.github.com/tauri-apps/tauri/commit/076e1a81a50468e3dfb34ae9ca7e77c5e1758daa)([#7119](https://www.github.com/tauri-apps/tauri/pull/7119)) Use `u32` instead of `u64` for js event listener ids + +## \[0.13.0] + +- Added the `additional_browser_args` option when creating a window. + - [3dc38b15](https://www.github.com/tauri-apps/tauri/commit/3dc38b150ea8c59c8ba67fd586f921016928f47c) feat(core): expose additional_browser_args to window config (fix: [#5757](https://www.github.com/tauri-apps/tauri/pull/5757)) ([#5799](https://www.github.com/tauri-apps/tauri/pull/5799)) on 2022-12-14 +- Added the `content_protected` option when creating a window and `Window::set_content_protected` to change it at runtime. + - [4ab5545b](https://www.github.com/tauri-apps/tauri/commit/4ab5545b7a831c549f3c65e74de487ede3ab7ce5) feat: add content protection api, closes [#5132](https://www.github.com/tauri-apps/tauri/pull/5132) ([#5513](https://www.github.com/tauri-apps/tauri/pull/5513)) on 2022-12-13 +- Added `Builder::device_event_filter` and `App::set_device_event_filter` methods. + - [73fd60ee](https://www.github.com/tauri-apps/tauri/commit/73fd60eef2b60f5dc84525ef9c315f4d80c4414f) expose set_device_event_filter in tauri ([#5562](https://www.github.com/tauri-apps/tauri/pull/5562)) on 2022-12-13 +- Fixes tray events not being delivered. + - [138cb8d7](https://www.github.com/tauri-apps/tauri/commit/138cb8d739b15bccdb388e555c20f17ffe16318c) fix(tauri-runtime-wry): tray event listener not registered ([#6270](https://www.github.com/tauri-apps/tauri/pull/6270)) on 2023-02-14 +- Add `is_minimized()` window method. + - [62144ef3](https://www.github.com/tauri-apps/tauri/commit/62144ef3be63b237869e511826edfb938e2c7174) feat: add is_minimized (fix [#3878](https://www.github.com/tauri-apps/tauri/pull/3878)) ([#5618](https://www.github.com/tauri-apps/tauri/pull/5618)) on 2022-12-13 +- Disable cursor mouse events on Linux. + - [8c842a54](https://www.github.com/tauri-apps/tauri/commit/8c842a54a6f3dc5327b4d737df7123dcddaa5769) feature: disable mouse event when building windows on Linux, closes [#5913](https://www.github.com/tauri-apps/tauri/pull/5913) ([#6025](https://www.github.com/tauri-apps/tauri/pull/6025)) on 2023-01-16 +- Bump minimum supported Rust version to 1.60. + - [5fdc616d](https://www.github.com/tauri-apps/tauri/commit/5fdc616df9bea633810dcb814ac615911d77222c) feat: Use the zbus-backed of notify-rust ([#6332](https://www.github.com/tauri-apps/tauri/pull/6332)) on 2023-03-31 +- Pin raw-window-handle to 0.5.0 to keep MSRV. + - [c46c09f3](https://www.github.com/tauri-apps/tauri/commit/c46c09f31d9f5169ca8a7e62406a9ea170e3a5c5) fix(deps): pin raw-window-handle to 0.5.0 ([#6480](https://www.github.com/tauri-apps/tauri/pull/6480)) on 2023-03-17 +- Add `title` getter on window. + - [233e43b0](https://www.github.com/tauri-apps/tauri/commit/233e43b0c34fada1ca025378533a0b76931a6540) feat: add `title` getter on window, closes [#5023](https://www.github.com/tauri-apps/tauri/pull/5023) ([#5515](https://www.github.com/tauri-apps/tauri/pull/5515)) on 2022-12-13 +- Added `TrayHandle::set_tooltip` and `SystemTray::with_tooltip`. + - [2265e097](https://www.github.com/tauri-apps/tauri/commit/2265e09718f6ebfeb1d200f11e1e1e069075af6e) feat(windows): implement `with_tooltip` ([#5938](https://www.github.com/tauri-apps/tauri/pull/5938)) on 2023-01-01 +- Added window's `url()` getter. + - [d17027e1](https://www.github.com/tauri-apps/tauri/commit/d17027e1a0db3e8c5ae81fc4f472c5918fbce611) feat: expose url method ([#5914](https://www.github.com/tauri-apps/tauri/pull/5914)) on 2022-12-26 +- On Windows, change webview theme based on Window theme for more accurate `prefers-color-scheme` support. + - [7a8d570d](https://www.github.com/tauri-apps/tauri/commit/7a8d570db72667367eb24b75ddc5dd07a968f7c0) fix: sync webview theme with window theme on Windows, closes [#5802](https://www.github.com/tauri-apps/tauri/pull/5802) ([#5874](https://www.github.com/tauri-apps/tauri/pull/5874)) on 2022-12-27 +- On Windows, Fix missing `WindowEvent::Focused` in `App::run` callback. + - [ff4ea1ea](https://www.github.com/tauri-apps/tauri/commit/ff4ea1eabbf2874b113c6b4698002929bbac737a) fix: dispatch focus event to app.run on Windows, closes [#6460](https://www.github.com/tauri-apps/tauri/pull/6460) ([#6504](https://www.github.com/tauri-apps/tauri/pull/6504)) on 2023-03-31 +- Implement the webview navigation handler. + - [3f35b452](https://www.github.com/tauri-apps/tauri/commit/3f35b452637ef1c794a423f1eda62a15d2ddaf42) Expose wry navigation_handler via WindowBuilder closes [#4080](https://www.github.com/tauri-apps/tauri/pull/4080) ([#5686](https://www.github.com/tauri-apps/tauri/pull/5686)) on 2022-12-27 + +## \[0.12.3] + +- Block remote URLs from accessing the IPC. + - [9c0593c33](https://www.github.com/tauri-apps/tauri/commit/9c0593c33af52cd9e00ec784d15f63efebdf039c) feat(core): block remote URLs from accessing the IPC on 2023-04-12 + +## \[0.12.2] + +- Fix compatibility with older Linux distributions. + - [b490308c](https://www.github.com/tauri-apps/tauri/commit/b490308c8897b893292951754607c2253abbc6e1) fix(core): compilation error on older Linux versions, fixes [#5684](https://www.github.com/tauri-apps/tauri/pull/5684) ([#5697](https://www.github.com/tauri-apps/tauri/pull/5697)) on 2022-11-28 +- Update wry to 0.23. + - [fdcd7733](https://www.github.com/tauri-apps/tauri/commit/fdcd77338c1a3a7ef8a8ea1907351c5c350ea7ba) chore(deps): update wry to 0.23 on 2022-12-08 + +## \[0.12.1] + +- Fix `allowlist > app > show/hide` always disabled when `allowlist > app > all: false`. + - Bumped due to a bump in tauri-utils. + - [bb251087](https://www.github.com/tauri-apps/tauri/commit/bb2510876d0bdff736d36bf3a465cdbe4ad2b90c) fix(core): extend allowlist with `app`'s allowlist, closes [#5650](https://www.github.com/tauri-apps/tauri/pull/5650) ([#5652](https://www.github.com/tauri-apps/tauri/pull/5652)) on 2022-11-18 + +## \[0.12.0] + +- Add `accept_first_mouse` option for macOS windows. + - [95f467ad](https://www.github.com/tauri-apps/tauri/commit/95f467add51448319983c54e2f382c7c09fb72d6) feat(core): add window `accept_first_mouse` option, closes [#5347](https://www.github.com/tauri-apps/tauri/pull/5347) ([#5374](https://www.github.com/tauri-apps/tauri/pull/5374)) on 2022-10-17 +- Disable automatic window tabbing on macOS when the `tabbing_identifier` option is not defined, the window is transparent or does not have decorations. + - [4137ab44](https://www.github.com/tauri-apps/tauri/commit/4137ab44a81d739556cbc7583485887e78952bf1) feat(macos): add `tabbing_identifier` option, closes [#2804](https://www.github.com/tauri-apps/tauri/pull/2804), [#3912](https://www.github.com/tauri-apps/tauri/pull/3912) ([#5399](https://www.github.com/tauri-apps/tauri/pull/5399)) on 2022-10-19 +- Drop the WebContext when the WebView is dropped. + - [9d8b3774](https://www.github.com/tauri-apps/tauri/commit/9d8b377481abf975dc37f9050d2ac7b63ce353e9) feat(tauri-runtime-wry): drop the WebContext on WebView drop ([#5240](https://www.github.com/tauri-apps/tauri/pull/5240)) on 2022-10-19 +- Readd the option to create an unfocused window via the `focused` method. The `focus` function has been deprecated. + - [4036e15f](https://www.github.com/tauri-apps/tauri/commit/4036e15f5af933bdc0d0913508b5103958afc143) feat(core): reimplement window initial focus flag, closes [#5120](https://www.github.com/tauri-apps/tauri/pull/5120) ([#5338](https://www.github.com/tauri-apps/tauri/pull/5338)) on 2022-10-08 +- Add `hidden_title` option for macOS windows. + - [321f3fed](https://www.github.com/tauri-apps/tauri/commit/321f3fed19df40c1223099bce953332b7f00f7a9) feat(macos): `title_bar_style` and `hidden_title` window options, closes [#2663](https://www.github.com/tauri-apps/tauri/pull/2663) ([#3965](https://www.github.com/tauri-apps/tauri/pull/3965)) on 2022-09-30 +- Custom protocol headers are now implemented on Linux when running on webkit2gtk 2.36 or above. + - [357480f4](https://www.github.com/tauri-apps/tauri/commit/357480f4ae43aa8da99f7ba61ae2ee51b4552c60) feat(core): custom protocol headers on Linux, closes [#4496](https://www.github.com/tauri-apps/tauri/pull/4496) ([#5421](https://www.github.com/tauri-apps/tauri/pull/5421)) on 2022-10-17 +- Added `Runtime::show()`, `RuntimeHandle::show()`, `Runtime::hide()`, `RuntimeHandle::hide()` for hiding/showing the entire application on macOS. + - [39bf895b](https://www.github.com/tauri-apps/tauri/commit/39bf895b73ec6b53f5758815396ba85dda6b9c67) feat(macOS): Add application `show` and `hide` methods ([#3689](https://www.github.com/tauri-apps/tauri/pull/3689)) on 2022-10-03 +- Fix regression in `SystemTray::with_menu_on_left_click` + - [f8a3becb](https://www.github.com/tauri-apps/tauri/commit/f8a3becb287942db7f7b551b5db6aeb5a2e939ee) feat(core): add option to disable tray menu on left click, closes [#4584](https://www.github.com/tauri-apps/tauri/pull/4584) ([#4587](https://www.github.com/tauri-apps/tauri/pull/4587)) on 2022-07-05 + - [7bbf167c](https://www.github.com/tauri-apps/tauri/commit/7bbf167c1c84493ea6e2353f720edafd7daa47e4) Apply Version Updates From Current Changes ([#4560](https://www.github.com/tauri-apps/tauri/pull/4560)) on 2022-07-06 + - [63011ca8](https://www.github.com/tauri-apps/tauri/commit/63011ca84e7a22c8c0d8bd1c1be6592140f93ff2) fix(macos): fix regression in `with_menu_on_left_click`, closes [#5220](https://www.github.com/tauri-apps/tauri/pull/5220) ([#5235](https://www.github.com/tauri-apps/tauri/pull/5235)) on 2022-09-30 +- - [7d9aa398](https://www.github.com/tauri-apps/tauri/commit/7d9aa3987efce2d697179ffc33646d086c68030c) feat: bump MSRV to 1.59 ([#5296](https://www.github.com/tauri-apps/tauri/pull/5296)) on 2022-09-28 +- Added `tabbing_identifier` to the window builder on macOS. + - [4137ab44](https://www.github.com/tauri-apps/tauri/commit/4137ab44a81d739556cbc7583485887e78952bf1) feat(macos): add `tabbing_identifier` option, closes [#2804](https://www.github.com/tauri-apps/tauri/pull/2804), [#3912](https://www.github.com/tauri-apps/tauri/pull/3912) ([#5399](https://www.github.com/tauri-apps/tauri/pull/5399)) on 2022-10-19 +- Add `title_bar_style` option for macOS windows. + - [321f3fed](https://www.github.com/tauri-apps/tauri/commit/321f3fed19df40c1223099bce953332b7f00f7a9) feat(macos): `title_bar_style` and `hidden_title` window options, closes [#2663](https://www.github.com/tauri-apps/tauri/pull/2663) ([#3965](https://www.github.com/tauri-apps/tauri/pull/3965)) on 2022-09-30 +- Fix regression introduce in tauri@1.1 which prevented removing tray icon when the app exits on Windows. + - [f756cd5e](https://www.github.com/tauri-apps/tauri/commit/f756cd5e7ecc86f178f8d602eded1e1b6ecb51f3) fix(core): wait for tray cleanup before exiting app, closes [#5244](https://www.github.com/tauri-apps/tauri/pull/5244) ([#5245](https://www.github.com/tauri-apps/tauri/pull/5245)) on 2022-10-04 +- Added methods to set the system tray title on macOS. + - [8f1ace77](https://www.github.com/tauri-apps/tauri/commit/8f1ace77956ac3477826ceb059a191e55b3fff93) feat: expose `set_title` for MacOS tray ([#5182](https://www.github.com/tauri-apps/tauri/pull/5182)) on 2022-09-30 +- Added the `user_agent` option when creating a window. + - [a6c94119](https://www.github.com/tauri-apps/tauri/commit/a6c94119d8545d509723b147c273ca5edfe3729f) feat(core): expose user_agent to window config ([#5317](https://www.github.com/tauri-apps/tauri/pull/5317)) on 2022-10-02 + +## \[0.11.2] + +- Block remote URLs from accessing the IPC. + - [58ea0b452](https://www.github.com/tauri-apps/tauri/commit/58ea0b45268dbd46cbac0ebb0887353d057ca767) feat(core): block remote URLs from accessing the IPC on 2023-04-12 + +## \[0.11.1] + +- Add missing allowlist config for `set_cursor_grab`, `set_cursor_visible`, `set_cursor_icon` and `set_cursor_position` APIs. + - Bumped due to a bump in tauri-utils. + - [c764408d](https://www.github.com/tauri-apps/tauri/commit/c764408da7fae123edd41115bda42fa75a4731d2) fix: Add missing allowlist config for cursor apis, closes [#5207](https://www.github.com/tauri-apps/tauri/pull/5207) ([#5211](https://www.github.com/tauri-apps/tauri/pull/5211)) on 2022-09-16 + +## \[0.11.0] + +- Ignore window events with unknown IDs. + - [0668dd42](https://www.github.com/tauri-apps/tauri/commit/0668dd42204b163f11aaf31f45106c8551f15942) fix(tauri-runtime-wry): ignore events on unknown windows on 2022-08-29 +- Implement theme APIs for Linux. + - [f21cbecd](https://www.github.com/tauri-apps/tauri/commit/f21cbecdeb3571ac4ad971b9a865ff62a131a176) feat(core): implement theme APIs for Linux ([#4808](https://www.github.com/tauri-apps/tauri/pull/4808)) on 2022-08-02 +- Changed `windows` map to be stored in a `RefCell` instead of a `Mutex`. + - [64546cb9](https://www.github.com/tauri-apps/tauri/commit/64546cb9cca2fe56cf81cfc4aaf85c4e1d58877c) refactor: use RefCell instead of Mutex for windows map, closes [#4870](https://www.github.com/tauri-apps/tauri/pull/4870) ([#4909](https://www.github.com/tauri-apps/tauri/pull/4909)) on 2022-08-10 +- Added APIs to create a system tray at runtime. + - [4d063ae9](https://www.github.com/tauri-apps/tauri/commit/4d063ae9ee9538cd6fa5e01b80070c6edf8eaeb9) feat(core): create system tray at runtime, closes [#2278](https://www.github.com/tauri-apps/tauri/pull/2278) ([#4862](https://www.github.com/tauri-apps/tauri/pull/4862)) on 2022-08-09 +- Update windows to 0.39.0 and webview2-com to 0.19.1. + - [e6d9b670](https://www.github.com/tauri-apps/tauri/commit/e6d9b670b0b314ed667b0e164f2c8d27048e678f) refactor: remove unneeded focus code ([#5065](https://www.github.com/tauri-apps/tauri/pull/5065)) on 2022-09-03 + +## \[0.10.3] + +- Block remote URLs from accessing the IPC. + - [fa90214b0](https://www.github.com/tauri-apps/tauri/commit/fa90214b052b1a5d38d54fbf1ca422b4c37cfd1f) feat(core): block remote URLs from accessing the IPC on 2023-04-12 + +## \[0.10.2] + +- Disable drag-n-drop of tao based on `fileDropEnabled` value. + - [a1d569bb](https://www.github.com/tauri-apps/tauri/commit/a1d569bbc9cfdd58258916df594911e1c512a75e) fix(core): disable tao's drag-n-drop based on `fileDropEnabled`, closes [#4580](https://www.github.com/tauri-apps/tauri/pull/4580) ([#4592](https://www.github.com/tauri-apps/tauri/pull/4592)) on 2022-07-05 +- Added option to disable tray menu on left click on macOS. + - [f8a3becb](https://www.github.com/tauri-apps/tauri/commit/f8a3becb287942db7f7b551b5db6aeb5a2e939ee) feat(core): add option to disable tray menu on left click, closes [#4584](https://www.github.com/tauri-apps/tauri/pull/4584) ([#4587](https://www.github.com/tauri-apps/tauri/pull/4587)) on 2022-07-05 + +## \[0.10.1] + +- Fixes a deadlock on the file drop handler. + - [23a48007](https://www.github.com/tauri-apps/tauri/commit/23a48007c0df7346fa45c76dfaf9235a157f59ec) fix(tauri-runtime-wry): deadlock on file drop, closes [#4527](https://www.github.com/tauri-apps/tauri/pull/4527) ([#4535](https://www.github.com/tauri-apps/tauri/pull/4535)) on 2022-06-30 +- Send theme value only once on the getter function implementation on macOS. + - [63841c10](https://www.github.com/tauri-apps/tauri/commit/63841c10609c3d7337ba6cd68ae126b18987014d) fix(tauri-runtime-wry): do not send theme twice on macOS, closes [#4532](https://www.github.com/tauri-apps/tauri/pull/4532) ([#4540](https://www.github.com/tauri-apps/tauri/pull/4540)) on 2022-06-30 +- Fixes a deadlock when the window focus change on Windows. + - [185b0e31](https://www.github.com/tauri-apps/tauri/commit/185b0e314ece9563cd7c83a16466b2b8b9167eb3) fix(tauri-runtime-wry): deadlock when window focus change, closes [#4533](https://www.github.com/tauri-apps/tauri/pull/4533) ([#4539](https://www.github.com/tauri-apps/tauri/pull/4539)) on 2022-06-30 + +## \[0.10.0] + +- Implement `raw_window_handle::HasRawWindowHandle` on Linux. + - [3efbc67f](https://www.github.com/tauri-apps/tauri/commit/3efbc67f7469ce65a2d9ea4ff2b60b51d2a36aa5) feat: implement `raw_window_handle` on Linux ([#4469](https://www.github.com/tauri-apps/tauri/pull/4469)) on 2022-06-26 +- Moved the window and menu event listeners to the window struct. + - [46196fe9](https://www.github.com/tauri-apps/tauri/commit/46196fe922f4f1b38057155c6113236cfa4b3597) refactor(tauri-runtime-wry): move window and menu listeners to window ([#4485](https://www.github.com/tauri-apps/tauri/pull/4485)) on 2022-06-27 +- Refactored the `tauri-runtime-wry` plugin interface. + - [e39e2999](https://www.github.com/tauri-apps/tauri/commit/e39e2999e0ab1843a8195ba83aea3d6de705c3d8) refactor(tauri-runtime-wry): enhance plugin interface ([#4476](https://www.github.com/tauri-apps/tauri/pull/4476)) on 2022-06-27 +- Removed the `hwnd` and `ns_window` functions from `Dispatch` in favor of `raw_window_handle`. + - [3efbc67f](https://www.github.com/tauri-apps/tauri/commit/3efbc67f7469ce65a2d9ea4ff2b60b51d2a36aa5) feat: implement `raw_window_handle` on Linux ([#4469](https://www.github.com/tauri-apps/tauri/pull/4469)) on 2022-06-26 +- The theme API is now implemented on macOS 10.14+. + - [6d94ce42](https://www.github.com/tauri-apps/tauri/commit/6d94ce42353204a02fe9c82ed397d349439f75ef) feat(core): theme is now implemented on macOS ([#4380](https://www.github.com/tauri-apps/tauri/pull/4380)) on 2022-06-17 +- Suppress unused variable warning in release builds. + - [45981851](https://www.github.com/tauri-apps/tauri/commit/45981851e35119266c1a079e1ff27a39f1fdfaed) chore(lint): unused variable warnings for release builds ([#4411](https://www.github.com/tauri-apps/tauri/pull/4411)) on 2022-06-22 +- Update tao to 0.12 and wry to 0.19. + - [f6edc6df](https://www.github.com/tauri-apps/tauri/commit/f6edc6df29b1c45b483fa87c481a3b95730b131b) chore(deps): update tao to 0.12, wry to 0.19, closes [#3220](https://www.github.com/tauri-apps/tauri/pull/3220) ([#4502](https://www.github.com/tauri-apps/tauri/pull/4502)) on 2022-06-28 +- Fixes deadlocks when using window setters in the main thread. + - [123f6e69](https://www.github.com/tauri-apps/tauri/commit/123f6e69f60ca6d4b2fd738ca3ff5cf016d8e814) fix(tauri-runtime-wry): release windows lock immediately, closes [#4390](https://www.github.com/tauri-apps/tauri/pull/4390) ([#4392](https://www.github.com/tauri-apps/tauri/pull/4392)) on 2022-06-19 + +## \[0.9.0] + +- Upgrade to `stable`! + - Bumped due to a bump in tauri-utils. + - [f4bb30cc](https://www.github.com/tauri-apps/tauri/commit/f4bb30cc73d6ba9b9ef19ef004dc5e8e6bb901d3) feat(covector): prepare for v1 ([#4351](https://www.github.com/tauri-apps/tauri/pull/4351)) on 2022-06-15 + +## \[0.8.1] + +- Add `Menu::os_default` which will create a menu filled with default menu items and submenus. + - Bumped due to a bump in tauri-runtime. + - [4c4acc30](https://www.github.com/tauri-apps/tauri/commit/4c4acc3094218dd9cee0f1ad61810c979e0b41fa) feat: implement `Default` for `Menu`, closes [#2398](https://www.github.com/tauri-apps/tauri/pull/2398) ([#4291](https://www.github.com/tauri-apps/tauri/pull/4291)) on 2022-06-15 + +## \[0.8.0] + +- Removed `TrayIcon` and renamed `WindowIcon` to `Icon`, a shared type for both icons. + - [4ce8e228](https://www.github.com/tauri-apps/tauri/commit/4ce8e228134cd3f22973b74ef26ca0d165fbbbd9) refactor(core): use `Icon` for tray icons ([#4342](https://www.github.com/tauri-apps/tauri/pull/4342)) on 2022-06-14 + +## \[0.7.0] + +- **Breaking change**: Removed the `gtk-tray` and `ayatana-tray` Cargo features. + - [6216eb49](https://www.github.com/tauri-apps/tauri/commit/6216eb49e72863bfb6d4c9edb8827b21406ac393) refactor(core): drop `ayatana-tray` and `gtk-tray` Cargo features ([#4247](https://www.github.com/tauri-apps/tauri/pull/4247)) on 2022-06-02 + +## \[0.6.0] + +- Account the monitor position when centering a window. + - [a7a9fde1](https://www.github.com/tauri-apps/tauri/commit/a7a9fde16fb7c35d48d4f97e83ff95b8baf9e090) fix(core): account for monitor position when centering window ([#4166](https://www.github.com/tauri-apps/tauri/pull/4166)) on 2022-05-21 +- Update `windows-rs` to `0.37.0`, which requires Rust 1.61.0+. + - [2326be39](https://www.github.com/tauri-apps/tauri/commit/2326be39821890cdd4de76e7029a531424dcb26f) feat(core): update windows-rs to 0.37.0 ([#4199](https://www.github.com/tauri-apps/tauri/pull/4199)) on 2022-05-24 + +## \[0.5.2] + +- Use the event loop proxy to create a window so it doesn't deadlock on Windows. + - [61e37652](https://www.github.com/tauri-apps/tauri/commit/61e37652b931520424d6a93a134e67893703d992) fix(core): deadlock when creating window from IPC handler, closes [#4121](https://www.github.com/tauri-apps/tauri/pull/4121) ([#4123](https://www.github.com/tauri-apps/tauri/pull/4123)) on 2022-05-13 + +## \[0.5.1] + +- Added the `plugin` method to the `Wry` runtime, allowing extensions to the event loop. + - [c8e0e5b9](https://www.github.com/tauri-apps/tauri/commit/c8e0e5b97d542e549b37be08b545515c862af0e5) feat(tauri-runtime-wry): add plugin API ([#4094](https://www.github.com/tauri-apps/tauri/pull/4094)) on 2022-05-10 +- Update wry to 0.16.2 and webkit2gtk to 0.18.0. + - [71a553b7](https://www.github.com/tauri-apps/tauri/commit/71a553b715312e2bcceb963c83e42cffca7a63bc) chore(deps): update wry to 0.16.2, webkit2gtk to 0.18.0 ([#4099](https://www.github.com/tauri-apps/tauri/pull/4099)) on 2022-05-10 + +## \[0.5.0] + +- The file drop event payloads are now percent-decoded. + - [a0ecd81a](https://www.github.com/tauri-apps/tauri/commit/a0ecd81a934e1aa8935151a74cad686786054204) fix(core): percent decode file drop payloads, closes [#4034](https://www.github.com/tauri-apps/tauri/pull/4034) ([#4035](https://www.github.com/tauri-apps/tauri/pull/4035)) on 2022-05-03 +- Fixes a crash when using the menu with the inspector window focused on macOS. In this case the `window_id` will be the id of the first app window. + - [891eb748](https://www.github.com/tauri-apps/tauri/commit/891eb748cf590895dc3f1666f8dbd6082b21e04e) fix(tauri-runtime-wry): menu even panic on macOS inspector, closes [#3875](https://www.github.com/tauri-apps/tauri/pull/3875) ([#4027](https://www.github.com/tauri-apps/tauri/pull/4027)) on 2022-05-02 +- Fixes a freeze when calling `set_size` in the main thread on Windows. + - [8f259f4e](https://www.github.com/tauri-apps/tauri/commit/8f259f4ef89be3da11b57222c8b66af9487ab736) fix(core): use EventLoopProxy to prevent set_size freeze closes [#3990](https://www.github.com/tauri-apps/tauri/pull/3990) ([#4014](https://www.github.com/tauri-apps/tauri/pull/4014)) on 2022-04-30 +- Expose methods to access the underlying native handles of the webview. + - [c82b4761](https://www.github.com/tauri-apps/tauri/commit/c82b4761e1660592472dc55308ad69d9efc5855b) feat(core): expose `with_webview` API to access the platform webview ([#4058](https://www.github.com/tauri-apps/tauri/pull/4058)) on 2022-05-04 + +## \[0.4.0] + +- \**Breaking change::* Added the `clipboard` Cargo feature. + - [24e4ff20](https://www.github.com/tauri-apps/tauri/commit/24e4ff208ee0fe1a4cc5b10667ea0922ac63dfb5) refactor(core): add clipboard Cargo feature, enhancing binary size ([#3957](https://www.github.com/tauri-apps/tauri/pull/3957)) on 2022-04-24 +- Expose Window cursor APIs `set_cursor_grab`, `set_cursor_visible`, `set_cursor_icon` and `set_cursor_position`. + - [c54ddfe9](https://www.github.com/tauri-apps/tauri/commit/c54ddfe9338e7eb90b4d5b02dfde687d432d5bc1) feat: expose window cursor APIs, closes [#3888](https://www.github.com/tauri-apps/tauri/pull/3888) [#3890](https://www.github.com/tauri-apps/tauri/pull/3890) ([#3935](https://www.github.com/tauri-apps/tauri/pull/3935)) on 2022-04-21 +- Fixes a panic when using the `create_tao_window` API. + - [320329a9](https://www.github.com/tauri-apps/tauri/commit/320329a9a7d8a249c0fc9dee6db5669057ca8b39) fix(core): insert to webview_id_map on tao window creation, closes [#3883](https://www.github.com/tauri-apps/tauri/pull/3883) ([#3932](https://www.github.com/tauri-apps/tauri/pull/3932)) on 2022-04-21 +- Fixes a panic when a menu event is triggered when all windows are minimized on macOS. + - [70ff55c1](https://www.github.com/tauri-apps/tauri/commit/70ff55c1aa69ed59cd2a78d865e1cb398ef2a4ba) fix(core): panic on menu event with minimized windows, closes [#3902](https://www.github.com/tauri-apps/tauri/pull/3902) ([#3918](https://www.github.com/tauri-apps/tauri/pull/3918)) on 2022-04-20 +- Fixes a rendering issue when resizing the window with the devtools open. + - [80b714af](https://www.github.com/tauri-apps/tauri/commit/80b714af6b31365b9026bc92f8631b1721950447) fix: rendering issue when resizing with devtools open closes [#3914](https://www.github.com/tauri-apps/tauri/pull/3914) [#3814](https://www.github.com/tauri-apps/tauri/pull/3814) ([#3915](https://www.github.com/tauri-apps/tauri/pull/3915)) on 2022-04-19 +- \**Breaking change::* Added the `global-shortcut` Cargo feature. + - [e11878bc](https://www.github.com/tauri-apps/tauri/commit/e11878bcf7174b261a1fa146fc7d564d12e6312a) refactor(core): add global-shortcut Cargo feature, enhancing binary size ([#3956](https://www.github.com/tauri-apps/tauri/pull/3956)) on 2022-04-24 +- Added `WindowEvent::ThemeChanged(theme)`. + - [4cebcf6d](https://www.github.com/tauri-apps/tauri/commit/4cebcf6da7cad1953e0f01b426afac3b5ef1f81e) feat: expose theme APIs, closes [#3903](https://www.github.com/tauri-apps/tauri/pull/3903) ([#3937](https://www.github.com/tauri-apps/tauri/pull/3937)) on 2022-04-21 +- Added `theme` getter on `Window`. + - [4cebcf6d](https://www.github.com/tauri-apps/tauri/commit/4cebcf6da7cad1953e0f01b426afac3b5ef1f81e) feat: expose theme APIs, closes [#3903](https://www.github.com/tauri-apps/tauri/pull/3903) ([#3937](https://www.github.com/tauri-apps/tauri/pull/3937)) on 2022-04-21 +- Added `theme` setter to the WindowBuilder. + - [4cebcf6d](https://www.github.com/tauri-apps/tauri/commit/4cebcf6da7cad1953e0f01b426afac3b5ef1f81e) feat: expose theme APIs, closes [#3903](https://www.github.com/tauri-apps/tauri/pull/3903) ([#3937](https://www.github.com/tauri-apps/tauri/pull/3937)) on 2022-04-21 +- Create webview immediately when executed in the main thread. + - [fa2baba7](https://www.github.com/tauri-apps/tauri/commit/fa2baba76c8f59c81f2a2f7139033a09d14d89da) feat(core): create webview immediately when running in main thread ([#3891](https://www.github.com/tauri-apps/tauri/pull/3891)) on 2022-04-12 + +## \[0.3.5] + +- Fixes `WindowEvent::Destroyed` not firing. + - [169b5035](https://www.github.com/tauri-apps/tauri/commit/169b5035a93e3f33a420d4b2b0f8943e6404e07f) fix(core): actually fire `WindowEvent::Destroyed` ([#3797](https://www.github.com/tauri-apps/tauri/pull/3797)) on 2022-03-28 + +## \[0.3.4] + +- Added `close_devtools` and `is_devtools_open` APIs to the `Dispatch` trait. + - [e05d718a](https://www.github.com/tauri-apps/tauri/commit/e05d718a7b46476d1fe4817c169008080e84f959) feat(core): add hotkey to toggle devtools, closes [#3776](https://www.github.com/tauri-apps/tauri/pull/3776) ([#3791](https://www.github.com/tauri-apps/tauri/pull/3791)) on 2022-03-28 +- Emit `RunEvent::Exit` on `tao::event::Event::LoopDestroyed` instead of after `RunEvent::ExitRequested`. + - [3c4ee7c9](https://www.github.com/tauri-apps/tauri/commit/3c4ee7c997fa3ff696bcfd5b8c82fecaca16bf49) refactor(wry): emit `RunEvent::Exit` on `Event::LoopDestroyed` ([#3785](https://www.github.com/tauri-apps/tauri/pull/3785)) on 2022-03-27 +- **Breaking change:** The `MenuItem::About` variant is now associated with a tuple value `(String, AboutMetadata)`. + - [5fb74332](https://www.github.com/tauri-apps/tauri/commit/5fb74332ab9210ac062d96b0e9afd1c942ee2911) chore(deps): update wry to 0.14, tao to 0.7 ([#3790](https://www.github.com/tauri-apps/tauri/pull/3790)) on 2022-03-28 +- Support window parenting on macOS + - [4e807a53](https://www.github.com/tauri-apps/tauri/commit/4e807a53e2d6d3f3cd5293d90013d5cdded5454e) Support window parenting on macOS, closes [#3751](https://www.github.com/tauri-apps/tauri/pull/3751) ([#3754](https://www.github.com/tauri-apps/tauri/pull/3754)) on 2022-03-23 +- The file drop event is now part of the `WindowEvent` enum instead of a having a dedicated handler. + - [07d1584c](https://www.github.com/tauri-apps/tauri/commit/07d1584cf06ea326aa45d8044bee1b77ecba5006) feat(core): add `WindowEvent::FileDrop`, closes [#3664](https://www.github.com/tauri-apps/tauri/pull/3664) ([#3686](https://www.github.com/tauri-apps/tauri/pull/3686)) on 2022-03-13 +- **Breaking change:** Use the dedicated `WindowEvent` enum on `RunEvent`. + - [edad9f4f](https://www.github.com/tauri-apps/tauri/commit/edad9f4f55dcc69a06cd9d6d5a5068c94ecb77dd) refactor(core): add `RunEvent::WindowEvent` ([#3793](https://www.github.com/tauri-apps/tauri/pull/3793)) on 2022-03-28 +- Added `create_proxy` to the `Runtime` and `RuntimeHandle` traits. + - [5d538ec2](https://www.github.com/tauri-apps/tauri/commit/5d538ec27c246274df4ff5b8057ff78b6364a43f) refactor(core): use the event loop proxy to send updater events ([#3687](https://www.github.com/tauri-apps/tauri/pull/3687)) on 2022-03-15 +- Allow specifying a user event type for the event loop message. + - [5d538ec2](https://www.github.com/tauri-apps/tauri/commit/5d538ec27c246274df4ff5b8057ff78b6364a43f) refactor(core): use the event loop proxy to send updater events ([#3687](https://www.github.com/tauri-apps/tauri/pull/3687)) on 2022-03-15 +- Use a random window id instead of `tao::window::WindowId` to not block the thread waiting for the event loop to process the window creation. + - [7cd39c70](https://www.github.com/tauri-apps/tauri/commit/7cd39c70c9ecd62cc9b60d0ab93f10ce0a6dd8b4) refactor(core): use random window id to simplify window creation, closes [#3645](https://www.github.com/tauri-apps/tauri/pull/3645) [#3597](https://www.github.com/tauri-apps/tauri/pull/3597) ([#3684](https://www.github.com/tauri-apps/tauri/pull/3684)) on 2022-03-15 +- Update `wry` to `0.14` and `tao` to `0.7`. + - [f2d24ef2](https://www.github.com/tauri-apps/tauri/commit/f2d24ef2fbd95ec7d3433ba651964f4aa3b7f48c) chore(deps): update wry ([#1482](https://www.github.com/tauri-apps/tauri/pull/1482)) on 2021-04-14 + - [e267ebf1](https://www.github.com/tauri-apps/tauri/commit/e267ebf1f1009b99829e0a7d71519925f5792f9f) Apply Version Updates From Current Changes ([#1486](https://www.github.com/tauri-apps/tauri/pull/1486)) on 2021-04-14 + - [5fb74332](https://www.github.com/tauri-apps/tauri/commit/5fb74332ab9210ac062d96b0e9afd1c942ee2911) chore(deps): update wry to 0.14, tao to 0.7 ([#3790](https://www.github.com/tauri-apps/tauri/pull/3790)) on 2022-03-28 +- Added the `WindowEvent::FileDrop` variant. + - [07d1584c](https://www.github.com/tauri-apps/tauri/commit/07d1584cf06ea326aa45d8044bee1b77ecba5006) feat(core): add `WindowEvent::FileDrop`, closes [#3664](https://www.github.com/tauri-apps/tauri/pull/3664) ([#3686](https://www.github.com/tauri-apps/tauri/pull/3686)) on 2022-03-13 + +## \[0.3.3] + +- Fixes a deadlock on the `Focused` event when the window is not visible. + - [c08cc6d5](https://www.github.com/tauri-apps/tauri/commit/c08cc6d50041ec887d3070c41bb2c793dbac5155) fix(core): deadlock on focus events with invisible window,[#3534](https://www.github.com/tauri-apps/tauri/pull/3534) ([#3622](https://www.github.com/tauri-apps/tauri/pull/3622)) on 2022-03-06 +- **Breaking change:** Move `ico` and `png` parsing behind `icon-ico` and `icon-png` Cargo features. + - [8c935872](https://www.github.com/tauri-apps/tauri/commit/8c9358725a17dcc2acaf4d10c3f654afdff586b0) refactor(core): move `png` and `ico` behind Cargo features ([#3588](https://www.github.com/tauri-apps/tauri/pull/3588)) on 2022-03-05 +- Print a warning to stderr if the window transparency has been set to true but `macos-private-api` is not enabled. + - [080755b5](https://www.github.com/tauri-apps/tauri/commit/080755b5377a3c0a17adf1d03e63555350422f0a) feat(core): warn if private APIs are not enabled, closes [#3481](https://www.github.com/tauri-apps/tauri/pull/3481) ([#3511](https://www.github.com/tauri-apps/tauri/pull/3511)) on 2022-02-19 + +## \[0.3.2] + +- Fix requirements for `RuntimeHandle`, `ClipboardManager`, `GlobalShortcutHandle` and `TrayHandle`. + - Bumped due to a bump in tauri-runtime. + - [84895a9c](https://www.github.com/tauri-apps/tauri/commit/84895a9cd270fc743e236d0f4d4cd6210b24a30f) fix(runtime): trait requirements ([#3489](https://www.github.com/tauri-apps/tauri/pull/3489)) on 2022-02-17 + +## \[0.3.1] + +- Change default value for the `freezePrototype` configuration to `false`. + - Bumped due to a bump in tauri-utils. + - [3a4c0160](https://www.github.com/tauri-apps/tauri/commit/3a4c01606184be762adee055ddac803de0d28527) fix(core): change default `freezePrototype` to false, closes [#3416](https://www.github.com/tauri-apps/tauri/pull/3416) [#3406](https://www.github.com/tauri-apps/tauri/pull/3406) ([#3423](https://www.github.com/tauri-apps/tauri/pull/3423)) on 2022-02-12 + +## \[0.3.0] + +- Fix `window.center` panic when window size is bigger than screen size. + - [76ce9f61](https://www.github.com/tauri-apps/tauri/commit/76ce9f61dd3c5bdd589c7557543894e1f770dd16) fix(core): fix `window.center` panic when window size > screen, closes [#2978](https://www.github.com/tauri-apps/tauri/pull/2978) ([#3002](https://www.github.com/tauri-apps/tauri/pull/3002)) on 2021-12-09 +- Enable non-session cookie persistence on Linux. + - [d7c02a30](https://www.github.com/tauri-apps/tauri/commit/d7c02a30a56de79100804969138b379e703f0e07) feat(core): persist non-session cookies on Linux ([#3052](https://www.github.com/tauri-apps/tauri/pull/3052)) on 2021-12-09 +- Fixes a deadlock when creating a window from a menu event handler. + - [9c82006b](https://www.github.com/tauri-apps/tauri/commit/9c82006b2fe166d20510183e36cee099bf96e8d9) fix(core): deadlock when creating window from menu handler, closes [#3110](https://www.github.com/tauri-apps/tauri/pull/3110) ([#3126](https://www.github.com/tauri-apps/tauri/pull/3126)) on 2021-12-28 +- Fixes `WindowEvent::Focus` and `WindowEvent::Blur` events not firing. + - [3b33d67a](https://www.github.com/tauri-apps/tauri/commit/3b33d67aa4f48dcf4e32b3b8a5f45e83808efc2d) fix: re-adding focus/blur events for linux and macos (fix [#2485](https://www.github.com/tauri-apps/tauri/pull/2485)) ([#2489](https://www.github.com/tauri-apps/tauri/pull/2489)) on 2021-08-24 +- Use webview's inner_size instead of window's value to get the correct size on macOS. + - [4c0c780e](https://www.github.com/tauri-apps/tauri/commit/4c0c780e00d8851be38cb1c22f636d9e4ed34a23) fix(core): window's inner_size usage, closes [#2187](https://www.github.com/tauri-apps/tauri/pull/2187) ([#2690](https://www.github.com/tauri-apps/tauri/pull/2690)) on 2021-09-29 +- Reimplement `remove_system_tray` on Windows to drop the `SystemTray` to run its cleanup code. + - [a03b8554](https://www.github.com/tauri-apps/tauri/commit/a03b85545a4b0b61a598a43eabe96e03565dcaf0) fix(core): tray not closing on Windows ([#3351](https://www.github.com/tauri-apps/tauri/pull/3351)) on 2022-02-07 +- Replace `WindowBuilder`'s `has_menu` with `get_menu`. + - [ac37b56e](https://www.github.com/tauri-apps/tauri/commit/ac37b56ef43c9e97039967a5fd99f0d2dccb5b5a) fix(core): menu id map not reflecting the current window menu ([#2726](https://www.github.com/tauri-apps/tauri/pull/2726)) on 2021-10-08 +- Fix empty header from CORS on Linux. + - [b48487e6](https://www.github.com/tauri-apps/tauri/commit/b48487e6a7b33f5a352e542fae21a2efd53ce295) Fix empty header from CORS on Linux, closes [#2327](https://www.github.com/tauri-apps/tauri/pull/2327) ([#2762](https://www.github.com/tauri-apps/tauri/pull/2762)) on 2021-10-18 +- The `run_return` API is now available on Linux. + - [8483fde9](https://www.github.com/tauri-apps/tauri/commit/8483fde975aac8833d2ce426e42fb40aeaeecba9) feat(core): expose `run_return` on Linux ([#3352](https://www.github.com/tauri-apps/tauri/pull/3352)) on 2022-02-07 +- Allow window, global shortcut and clipboard APIs to be called on the main thread. + - [2812c446](https://www.github.com/tauri-apps/tauri/commit/2812c4464b93a365ab955935d05b5cea8cb03aab) feat(core): window, shortcut and clipboard API calls on main thread ([#2659](https://www.github.com/tauri-apps/tauri/pull/2659)) on 2021-09-26 + - [d24fd8d1](https://www.github.com/tauri-apps/tauri/commit/d24fd8d10242da3da143a971d976b42ec4de6079) feat(tauri-runtime-wry): allow window creation and closing on the main thread ([#2668](https://www.github.com/tauri-apps/tauri/pull/2668)) on 2021-09-27 +- Change event loop callbacks definition to allow callers to move in mutable values. + - [bdbf905e](https://www.github.com/tauri-apps/tauri/commit/bdbf905e5d802b58693d2bd27582ce4269faf79c) Transformed event-loop callback to FnMut to allow mutable values ([#2667](https://www.github.com/tauri-apps/tauri/pull/2667)) on 2021-09-27 +- **Breaking change:** Add `macos-private-api` feature flag, enabled via `tauri.conf.json > tauri > macOSPrivateApi`. + - [6ac21b3c](https://www.github.com/tauri-apps/tauri/commit/6ac21b3cef7f14358df38cc69ea3d277011accaf) feat: add private api feature flag ([#7](https://www.github.com/tauri-apps/tauri/pull/7)) on 2022-01-09 +- Refactor `create_tao_window` API to return `Weak` instead of `Arc`. + - [c1494b35](https://www.github.com/tauri-apps/tauri/commit/c1494b353233c6a9552d7ace962fdf8d5b1f199a) refactor: return Weak on create_tao_window on 2021-08-31 +- Added `any_thread` constructor on the `Runtime` trait (only possible on Linux and Windows). + - [af44bf81](https://www.github.com/tauri-apps/tauri/commit/af44bf8168310cf77fbe102a53e7c433f11641a3) feat(core): allow app run on any thread on Linux & Windows, closes [#3172](https://www.github.com/tauri-apps/tauri/pull/3172) ([#3353](https://www.github.com/tauri-apps/tauri/pull/3353)) on 2022-02-07 +- Added `run_on_main_thread` API on `RuntimeHandle`. + - [53fdfe52](https://www.github.com/tauri-apps/tauri/commit/53fdfe52bb30d52653c72ca9f42506c3863dcf4a) feat(core): expose `run_on_main_thread` API ([#2711](https://www.github.com/tauri-apps/tauri/pull/2711)) on 2021-10-04 +- **Breaking change:** Renamed the `RPC` interface to `IPC`. + - [3420aa50](https://www.github.com/tauri-apps/tauri/commit/3420aa5031b3274a95c6c5fa0f8683ca13213396) refactor: IPC handler \[TRI-019] ([#9](https://www.github.com/tauri-apps/tauri/pull/9)) on 2022-01-09 +- Added `open_devtools` to the `Dispatcher` trait. + - [55aa22de](https://www.github.com/tauri-apps/tauri/commit/55aa22de80c3de873e29bcffcb5b2fe236a637a6) feat(core): add `Window#open_devtools` API, closes [#1213](https://www.github.com/tauri-apps/tauri/pull/1213) ([#3350](https://www.github.com/tauri-apps/tauri/pull/3350)) on 2022-02-07 +- The minimum Rust version is now `1.56`. + - [a9dfc015](https://www.github.com/tauri-apps/tauri/commit/a9dfc015505afe91281c2027954ffcc588b1a59c) feat: update to edition 2021 and set minimum rust to 1.56 ([#2789](https://www.github.com/tauri-apps/tauri/pull/2789)) on 2021-10-22 +- Replace all of the `winapi` crate references with the `windows` crate, and replace `webview2` and `webview2-sys` with `webview2-com` and `webview2-com-sys` built with the `windows` crate. This goes along with updates to the TAO and WRY `next` branches. + - [bb00d5bd](https://www.github.com/tauri-apps/tauri/commit/bb00d5bd6c9dfcb6bdd0d308dadb70e6c6aafe5c) Replace winapi with windows crate and use webview2-com instead of webview2 ([#2615](https://www.github.com/tauri-apps/tauri/pull/2615)) on 2021-09-24 +- Update the `windows` crate to 0.25.0, which comes with pre-built libraries. WRY and Tao can both reference the same types directly from the `windows` crate instead of sharing bindings in `webview2-com-sys`. + - [34be6cf3](https://www.github.com/tauri-apps/tauri/commit/34be6cf37a98ee7cbd66623ebddae08e5a6520fd) Update webview2-com and windows crates ([#2875](https://www.github.com/tauri-apps/tauri/pull/2875)) on 2021-11-11 +- This is a temporary fix of null pointer crash on `get_content` of web resource request. + We will switch it back once upstream is updated. + - [84f6e3e8](https://www.github.com/tauri-apps/tauri/commit/84f6e3e84a34b01b7fa04f5c4719acb921ef4263) Switch to next branch of wry ([#2574](https://www.github.com/tauri-apps/tauri/pull/2574)) on 2021-09-10 +- Update wry to 0.13. + - [343ea3e2](https://www.github.com/tauri-apps/tauri/commit/343ea3e2e8d51bac63ab651289295c26fcc841d8) Update wry to 0.13 ([#3336](https://www.github.com/tauri-apps/tauri/pull/3336)) on 2022-02-06 + +## \[0.2.1] + +- Migrate to latest custom protocol allowing `Partial content` streaming and Header parsing. + - [539e4489](https://www.github.com/tauri-apps/tauri/commit/539e4489e0bac7029d86917e9982ea49e02fe489) refactor: custom protocol ([#2503](https://www.github.com/tauri-apps/tauri/pull/2503)) on 2021-08-23 + +## \[0.2.0] + +- Fix blur/focus events being incorrect on Windows. + - [d832d575](https://www.github.com/tauri-apps/tauri/commit/d832d575d9b03a0ff78accabe4631cc638c08c3b) fix(windows): use webview events on windows ([#2277](https://www.github.com/tauri-apps/tauri/pull/2277)) on 2021-07-23 + +- Add `ExitRequested` event that allows preventing the app from exiting when all windows are closed, and an `AppHandle.exit()` function to exit the app manually. + - [892c63a0](https://www.github.com/tauri-apps/tauri/commit/892c63a0538f8d62680dce5848657128ad6b7af3) feat([#2287](https://www.github.com/tauri-apps/tauri/pull/2287)): Add `ExitRequested` event to let users prevent app from exiting ([#2293](https://www.github.com/tauri-apps/tauri/pull/2293)) on 2021-08-09 + +- Update gtk and its related libraries to v0.14. This also remove requirements of `clang` as build dependency. + - [63ad3039](https://www.github.com/tauri-apps/tauri/commit/63ad303903bbee7c9a7382413b342e2a05d3ea75) chore(linux): bump gtk to v0.14 ([#2361](https://www.github.com/tauri-apps/tauri/pull/2361)) on 2021-08-07 + +- Implement `Debug` on public API structs and enums. + - [fa9341ba](https://www.github.com/tauri-apps/tauri/commit/fa9341ba18ba227735341530900714dba0f27291) feat(core): implement `Debug` on public API structs/enums, closes [#2292](https://www.github.com/tauri-apps/tauri/pull/2292) ([#2387](https://www.github.com/tauri-apps/tauri/pull/2387)) on 2021-08-11 + +- Fix the error "cannot find type MenuHash in this scope" + - [226414d1](https://www.github.com/tauri-apps/tauri/commit/226414d1a588c8bc2b540a71fcd84c318319d6af) "cannot find type `MenuHash` in this scope" ([#2240](https://www.github.com/tauri-apps/tauri/pull/2240)) on 2021-07-20 + +- Panic when a dispatcher getter method (`Window`, `GlobalShortcutHandle`, `ClipboardManager` and `MenuHandle` APIs) is called on the main thread. + - [50ffdc06](https://www.github.com/tauri-apps/tauri/commit/50ffdc06fbde56aba32b4291fd130104935d1408) feat(core): panic when a dispatcher getter is used on the main thread ([#2455](https://www.github.com/tauri-apps/tauri/pull/2455)) on 2021-08-16 + +- Remove menu feature flag since there's no package dependency need to be installed on any platform anymore. + - [f81ebddf](https://www.github.com/tauri-apps/tauri/commit/f81ebddfcc1aea0d4989706aef43538e8ea98bea) feat: remove menu feature flag ([#2415](https://www.github.com/tauri-apps/tauri/pull/2415)) on 2021-08-13 + +- Adds `Resumed` and `MainEventsCleared` variants to the `RunEvent` enum. + - [6be3f433](https://www.github.com/tauri-apps/tauri/commit/6be3f4339168651fe4e003b09f7d181fd12cd5a8) feat(core): add `Resumed` and `MainEventsCleared` events, closes [#2127](https://www.github.com/tauri-apps/tauri/pull/2127) ([#2439](https://www.github.com/tauri-apps/tauri/pull/2439)) on 2021-08-15 + +- Adds `set_activation_policy` API to the `Runtime` trait (macOS only). + - [4a031add](https://www.github.com/tauri-apps/tauri/commit/4a031add69014a1f3823f4ea19b172a2557f6794) feat(core): expose `set_activation_policy`, closes [#2258](https://www.github.com/tauri-apps/tauri/pull/2258) ([#2420](https://www.github.com/tauri-apps/tauri/pull/2420)) on 2021-08-13 + +- Allow creation of empty Window with `create_tao_window()` and management with `send_tao_window_event()` on the AppHandler. + - [88080855](https://www.github.com/tauri-apps/tauri/commit/8808085541a629b8e22b612a06cef01cf9b3722e) feat(window): Allow creation of Window without `wry` ([#2321](https://www.github.com/tauri-apps/tauri/pull/2321)) on 2021-07-29 + - [15566cfd](https://www.github.com/tauri-apps/tauri/commit/15566cfd64f5072fa4980a6ce5b33259958e9021) feat(core): add API to send wry window message to the event loop ([#2339](https://www.github.com/tauri-apps/tauri/pull/2339)) on 2021-08-02 + +- - Support [macOS tray icon template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) to adjust automatically based on taskbar color. + +- Images you mark as template images should consist of only black and clear colors. You can use the alpha channel in the image to adjust the opacity of black content, however. + +- [426a6b49](https://www.github.com/tauri-apps/tauri/commit/426a6b49962de8faf061db2e820ac10fcbb300d6) feat(macOS): Implement tray icon template ([#2322](https://www.github.com/tauri-apps/tauri/pull/2322)) on 2021-07-29 + +- Add `Event::Ready` on the `run()` callback. Triggered once when the event loop is ready. + - [28c6b7ad](https://www.github.com/tauri-apps/tauri/commit/28c6b7adfe98e701b158e936eafb7541ddc700e0) feat: add `Event::Ready` ([#2433](https://www.github.com/tauri-apps/tauri/pull/2433)) on 2021-08-15 + +- Add webdriver support to Tauri. + - [be76fb1d](https://www.github.com/tauri-apps/tauri/commit/be76fb1dfe73a1605cc2ad246418579f4c2e1999) WebDriver support ([#1972](https://www.github.com/tauri-apps/tauri/pull/1972)) on 2021-06-23 + - [b4426eda](https://www.github.com/tauri-apps/tauri/commit/b4426eda9e64fcdd25a2d72e548b8b0fbfa09619) Revert "WebDriver support ([#1972](https://www.github.com/tauri-apps/tauri/pull/1972))" on 2021-06-23 + - [4b2aa356](https://www.github.com/tauri-apps/tauri/commit/4b2aa35684632ed2afd7dec4ad848df5704868e4) Add back WebDriver support ([#2324](https://www.github.com/tauri-apps/tauri/pull/2324)) on 2021-08-01 + +## \[0.1.4] + +- Allow preventing window close when the user requests it. + - [8157a68a](https://www.github.com/tauri-apps/tauri/commit/8157a68af1d94de1b90a14aa44139bb123b3436b) feat(core): allow listening to event loop events & prevent window close ([#2131](https://www.github.com/tauri-apps/tauri/pull/2131)) on 2021-07-06 +- Fixes SVG loading on custom protocol. + - [e663bdd5](https://www.github.com/tauri-apps/tauri/commit/e663bdd5938830ab4eba961e69c3985191b499dd) fix(core): svg mime type ([#2129](https://www.github.com/tauri-apps/tauri/pull/2129)) on 2021-06-30 +- Fixes `center` and `focus` not being allowed in `tauri.conf.json > tauri > windows` and ignored in `WindowBuilderWrapper`. + - [bc2c331d](https://www.github.com/tauri-apps/tauri/commit/bc2c331dec3dec44c79e659b082b5fb6b65cc5ea) fix: center and focus not being allowed in config ([#2199](https://www.github.com/tauri-apps/tauri/pull/2199)) on 2021-07-12 +- Expose `gtk_window` getter. + - [e0a8e09c](https://www.github.com/tauri-apps/tauri/commit/e0a8e09cab6799eeb9ec524b5f7780d1e5a84299) feat(core): expose `gtk_window`, closes [#2083](https://www.github.com/tauri-apps/tauri/pull/2083) ([#2141](https://www.github.com/tauri-apps/tauri/pull/2141)) on 2021-07-02 +- Remove a few locks requirement in tauri-runtime-wry + - [6569c2bf](https://www.github.com/tauri-apps/tauri/commit/6569c2bf5caf24b009cad1e2cffba25418d6bb68) refactor(wry): remove a few locks requirements ([#2137](https://www.github.com/tauri-apps/tauri/pull/2137)) on 2021-07-02 +- Fix macOS high CPU usage. + - [a280ee90](https://www.github.com/tauri-apps/tauri/commit/a280ee90af0749ce18d6d0b00939b06473717bc9) Fix high cpu usage on mac, fix [#2074](https://www.github.com/tauri-apps/tauri/pull/2074) ([#2125](https://www.github.com/tauri-apps/tauri/pull/2125)) on 2021-06-30 +- Bump `wry` 0.11 and fix focus integration to make it compatible with tao 0.4. + - [f0a8db62](https://www.github.com/tauri-apps/tauri/commit/f0a8db62e445dbbc5770e7addf0390ce3844c1ea) core(deps): bump `wry` to `0.11` ([#2210](https://www.github.com/tauri-apps/tauri/pull/2210)) on 2021-07-15 +- `Params` has been removed, along with all the associated types on it. Functions that previously accepted those + associated types now accept strings instead. Type that used a generic parameter `Params` now use `Runtime` instead. If + you use the `wry` feature, then types with a `Runtime` generic parameter should default to `Wry`, letting you omit the + explicit type and let the compiler infer it instead. + +`tauri`: + +- See `Params` note +- If you were using `Params` inside a function parameter or definition, all references to it have been replaced with a + simple runtime that defaults to `Wry`. If you are not using a custom runtime, just remove `Params` from the definition + of functions/items that previously took it. If you are using a custom runtime, you *may* need to pass the runtime type + to these functions. +- If you were using custom types for `Params` (uncommon and if you don't understand you probably were not using it), all + methods that were previously taking the custom type now takes an `Into` or a `&str`. The types were already + required to be string-able, so just make sure to convert it into a string before passing it in if this breaking change + affects you. + +`tauri-macros`: + +- (internal) Added private `default_runtime` proc macro to allow us to give item definitions a custom runtime only when + the specified feature is enabled. + +`tauri-runtime`: + +- See `Params` note +- Removed `Params`, `MenuId`, `Tag`, `TagRef`. +- Added `menu::{MenuHash, MenuId, MenuIdRef}` as type aliases for the internal type that menu types now use. + - All previous menu items that had a `MenuId` generic now use the underlying `MenuId` type without a generic. +- `Runtime`, `RuntimeHandle`, and `Dispatch` have no more generic parameter on `create_window(...)` and instead use the + `Runtime` type directly +- `Runtime::system_tray` has no more `MenuId` generic and uses the string based `SystemTray` type directly. +- (internal) `CustomMenuItem::id_value()` is now hashed on creation and exposed as the `id` field with type `MenuHash`. + +`tauri-runtime-wry`: + +- See `Params` note +- update menu and runtime related types to the ones changed in `tauri-runtime`. + +`tauri-utils`: + +- `Assets::get` signature has changed to take a `&AssetKey` instead of `impl Into` to become trait object + safe. +- [fd8fab50](https://www.github.com/tauri-apps/tauri/commit/fd8fab507c8fa1b113b841af14c6693eb3955f6b) refactor(core): remove `Params` and replace with strings ([#2191](https://www.github.com/tauri-apps/tauri/pull/2191)) on 2021-07-15 + +## \[0.1.3] + +- `Window` is now `Send + Sync` on Windows. + - [fe32afcc](https://www.github.com/tauri-apps/tauri/commit/fe32afcc933920d6282ae1d63b041b182278a031) fix(core): `Window` must be `Send + Sync` on Windows, closes [#2078](https://www.github.com/tauri-apps/tauri/pull/2078) ([#2093](https://www.github.com/tauri-apps/tauri/pull/2093)) on 2021-06-27 + +## \[0.1.2] + +- Adds `clipboard` APIs (write and read text). + - [285bf64b](https://www.github.com/tauri-apps/tauri/commit/285bf64bf9569efb2df904c69c6df405ff0d62e2) feat(core): add clipboard writeText and readText APIs ([#2035](https://www.github.com/tauri-apps/tauri/pull/2035)) on 2021-06-21 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Fixes window event being emitted to all windows listeners. + - [fca97640](https://www.github.com/tauri-apps/tauri/commit/fca976404e6bec373a81332572458c4c44f7bb3a) fix(wry): window event listeners being emitted to all windows ([#2056](https://www.github.com/tauri-apps/tauri/pull/2056)) on 2021-06-23 +- Panic on window getters usage on the main thread when the event loop is not running and document it. + - [ab3eb44b](https://www.github.com/tauri-apps/tauri/commit/ab3eb44bac7a3bf73a4985df38ccc2b87a913be7) fix(core): deadlock on window getters, fixes [#1893](https://www.github.com/tauri-apps/tauri/pull/1893) ([#1998](https://www.github.com/tauri-apps/tauri/pull/1998)) on 2021-06-16 +- Adds `focus` API to the WindowBuilder. + - [5f351622](https://www.github.com/tauri-apps/tauri/commit/5f351622c7812ad1bb56ddb37364ccaa4124c24b) feat(core): add focus API to the WindowBuilder and WindowOptions, [#1737](https://www.github.com/tauri-apps/tauri/pull/1737) on 2021-05-30 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds support to PNG icons. + - [40b717ed](https://www.github.com/tauri-apps/tauri/commit/40b717edc57288a1393fad0529390e101ab903c1) feat(core): set window icon on Linux, closes [#1922](https://www.github.com/tauri-apps/tauri/pull/1922) ([#1937](https://www.github.com/tauri-apps/tauri/pull/1937)) on 2021-06-01 +- Adds `is_decorated` getter on Window. + - [f58a2114](https://www.github.com/tauri-apps/tauri/commit/f58a2114fbfd5307c349f05c88f2e08fd8baa8aa) feat(core): add `is_decorated` Window getter on 2021-05-30 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `is_resizable` getter on Window. + - [1e8af280](https://www.github.com/tauri-apps/tauri/commit/1e8af280c27f381828d6209722b10e889082fa00) feat(core): add `is_resizable` Window getter on 2021-05-30 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `is_visible` getter on Window. + - [36506c96](https://www.github.com/tauri-apps/tauri/commit/36506c967de82bc7ff453d11e6104ecf66d7a588) feat(core): add `is_visible` API on 2021-05-30 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Removes `image` dependency. For now only `.ico` icons on Windows are supported, and we'll implement other types on demand to optimize bundle size. + - [1be37a3f](https://www.github.com/tauri-apps/tauri/commit/1be37a3f30ff789d9396ec9009f9c0dd0bb928a7) refactor(core): remove `image` dependency ([#1859](https://www.github.com/tauri-apps/tauri/pull/1859)) on 2021-05-18 +- The `run_on_main_thread` API now uses WRY's UserEvent, so it wakes the event loop. + - [9bf82f0d](https://www.github.com/tauri-apps/tauri/commit/9bf82f0d9261808f58bdb5b5dbd6a255e5dcd333) fix(core): `run_on_main_thread` now wakes the event loop ([#1949](https://www.github.com/tauri-apps/tauri/pull/1949)) on 2021-06-04 +- Adds global shortcut interfaces. + - [3280c4aa](https://www.github.com/tauri-apps/tauri/commit/3280c4aa91e50a8ccdd561a8b48a12a4a13ea8d5) refactor(core): global shortcut is now provided by `tao` ([#2031](https://www.github.com/tauri-apps/tauri/pull/2031)) on 2021-06-21 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `request_user_attention` API to the `Dispatcher` trait. + - [7dcca6e9](https://www.github.com/tauri-apps/tauri/commit/7dcca6e9281182b11ad3d4a79871f09b30b9b419) feat(core): add `request_user_attention` API, closes [#2023](https://www.github.com/tauri-apps/tauri/pull/2023) ([#2026](https://www.github.com/tauri-apps/tauri/pull/2026)) on 2021-06-20 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `fn run_iteration` (macOS and Windows only) to the Runtime trait. + - [8c0d0739](https://www.github.com/tauri-apps/tauri/commit/8c0d0739eebf7286b64a5380e922746411eb52c6) feat(core): add `run_iteration`, `parent_window` and `owner_window` APIs, closes [#1872](https://www.github.com/tauri-apps/tauri/pull/1872) ([#1874](https://www.github.com/tauri-apps/tauri/pull/1874)) on 2021-05-21 +- Adds `show_menu`, `hide_menu` and `is_menu_visible` APIs to the `Dispatcher` trait. + - [954460c5](https://www.github.com/tauri-apps/tauri/commit/954460c5205d57444ef4b1412051fbedf3e38676) feat(core): MenuHandle `show`, `hide`, `is_visible` and `toggle` APIs ([#1958](https://www.github.com/tauri-apps/tauri/pull/1958)) on 2021-06-15 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `set_focus` API on Window. + - [bb6992f8](https://www.github.com/tauri-apps/tauri/commit/bb6992f888196ca7c87bb2fe74ad2bd8bf393e05) feat(core): add `set_focus` window API, fixes [#1737](https://www.github.com/tauri-apps/tauri/pull/1737) on 2021-05-30 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `set_skip_taskbar` API on Window. + - [e06aa277](https://www.github.com/tauri-apps/tauri/commit/e06aa277384450cfef617c0e57b0d5d403bb1e7f) feat(core): add `set_skip_taskbar` API on 2021-05-30 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Update `wry` to v0.10.0 and replace the removed `dispatch_script` and `evaluate_script` methods with the new `evaluate_script` method in `handle_event_loop`. + - [cca8115d](https://www.github.com/tauri-apps/tauri/commit/cca8115d9c813d13efb30a38445d5bda009a7f97) refactor: update wry, simplify script eval ([#1965](https://www.github.com/tauri-apps/tauri/pull/1965)) on 2021-06-16 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `skip_taskbar` API to the WindowBuilder. + - [5525b03a](https://www.github.com/tauri-apps/tauri/commit/5525b03a78a2232c650043fbd9894ce1553cad41) feat(core): add `skip_taskbar` API to the WindowBuilder/WindowOptions on 2021-05-30 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `Window#center` and `WindowBuilder#center` APIs. + - [5cba6eb4](https://www.github.com/tauri-apps/tauri/commit/5cba6eb4d28d53f06855d60d4d0eae6b95233ccf) feat(core): add window `center` API, closes [#1822](https://www.github.com/tauri-apps/tauri/pull/1822) ([#1954](https://www.github.com/tauri-apps/tauri/pull/1954)) on 2021-06-05 +- Adds `parent_window` and `owner_window` setters to the `WindowBuilder` (Windows only). + - [8c0d0739](https://www.github.com/tauri-apps/tauri/commit/8c0d0739eebf7286b64a5380e922746411eb52c6) feat(core): add `run_iteration`, `parent_window` and `owner_window` APIs, closes [#1872](https://www.github.com/tauri-apps/tauri/pull/1872) ([#1874](https://www.github.com/tauri-apps/tauri/pull/1874)) on 2021-05-21 +- Adds window native handle getter (HWND on Windows). + - [abf78c58](https://www.github.com/tauri-apps/tauri/commit/abf78c5860cdc52fbfd2bc5dbca29a864e2da8f9) fix(core): set parent window handle on dialogs, closes [#1876](https://www.github.com/tauri-apps/tauri/pull/1876) ([#1889](https://www.github.com/tauri-apps/tauri/pull/1889)) on 2021-05-21 + +## \[0.1.1] + +- Fixes `system-tray` feature usage. + - [1ab8dd9](https://www.github.com/tauri-apps/tauri/commit/1ab8dd93e670d2a2d070c7a6ec48308a0ab32f1a) fix(core): `system-tray` cargo feature usage, fixes [#1798](https://www.github.com/tauri-apps/tauri/pull/1798) ([#1801](https://www.github.com/tauri-apps/tauri/pull/1801)) on 2021-05-12 +- Fixes webview transparency. + - [f5a480f](https://www.github.com/tauri-apps/tauri/commit/f5a480fea34ab3a75751f1ca760a38b3e53da2cc) fix(core): window transparency ([#1800](https://www.github.com/tauri-apps/tauri/pull/1800)) on 2021-05-12 + +## \[0.1.0] + +- **Breaking:** `Context` fields are now private, and is expected to be created through `Context::new(...)`. + All fields previously available through `Context` are now public methods. + - [5542359](https://www.github.com/tauri-apps/tauri/commit/55423590ddbf560684dab6a0214acf95aadfa8d2) refactor(core): Context fields now private, Icon used on all platforms ([#1774](https://www.github.com/tauri-apps/tauri/pull/1774)) on 2021-05-11 +- `tauri-runtime-wry` initial release. + - [45a7a11](https://www.github.com/tauri-apps/tauri/commit/45a7a111e0cf9d9956d713cc9a99fa7a5313eec7) feat(core): add `tauri-wry` crate ([#1756](https://www.github.com/tauri-apps/tauri/pull/1756)) on 2021-05-09 diff --git a/crates/tauri-runtime-wry/Cargo.toml b/crates/tauri-runtime-wry/Cargo.toml new file mode 100644 index 0000000..10ab655 --- /dev/null +++ b/crates/tauri-runtime-wry/Cargo.toml @@ -0,0 +1,77 @@ +[package] +name = "tauri-runtime-wry" +version = "2.11.4" +description = "Wry bindings to the Tauri runtime" +exclude = ["CHANGELOG.md", "/target"] +authors.workspace = true +homepage.workspace = true +repository.workspace = true +categories.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +wry = { version = "0.57.0", default-features = false, features = [ + "os-webview", + "linux-body", +] } +tao = { version = "0.37.0", default-features = false, features = ["rwh_06"] } +tauri-runtime = { workspace = true } +tauri-utils = { workspace = true } +raw-window-handle = "0.6" +http = "1" +url = "2" +tracing = { version = "0.1", optional = true } +log = "0.4.21" + +[target."cfg(windows)".dependencies] +webview2-com = "0.39" +softbuffer = { version = "0.4", default-features = false } +once_cell = "1.20" + +[target."cfg(windows)".dependencies.windows] +version = "0.62" +features = [ + "Win32_Foundation", + "Win32_Graphics_Dwm", + "Win32_UI_WindowsAndMessaging", +] + +[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] +gtk = { version = "0.18", features = ["v3_24"] } +webkit2gtk = { version = "=2.0", features = ["v2_40"] } +percent-encoding = "2" + +[target.'cfg(target_vendor = "apple")'.dependencies] +objc2 = "0.6" + +[target.'cfg(target_os = "macos")'.dependencies] +objc2-app-kit = { version = "0.3.2", default-features = false, features = [ + "block2", + "NSApplication", + "NSResponder", + "NSView", + "NSWindow", + "NSScreen", + "NSGraphics", +] } + +[target."cfg(target_os = \"android\")".dependencies] +jni = "0.21" + +[features] +default = ["x11", "dbus"] +devtools = ["wry/devtools", "tauri-runtime/devtools"] +x11 = ["tao/x11", "wry/x11"] +macos-private-api = ["tauri-runtime/macos-private-api"] +# TODO: Remove in v3 - wry does not have this feature anymore +objc-exception = [] +tracing = ["dep:tracing", "wry/tracing"] +macos-proxy = ["wry/mac-proxy"] +unstable = [] +common-controls-v6 = ["windows/Win32_UI_Controls", "windows/Win32_UI_Shell"] +dbus = ["tao/dbus"] + +[lints] +workspace = true diff --git a/crates/tauri-runtime-wry/LICENSE-APACHE-2.0 b/crates/tauri-runtime-wry/LICENSE-APACHE-2.0 new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/crates/tauri-runtime-wry/LICENSE-APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/tauri-runtime-wry/LICENSE-MIT b/crates/tauri-runtime-wry/LICENSE-MIT new file mode 100644 index 0000000..b08530d --- /dev/null +++ b/crates/tauri-runtime-wry/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/tauri-runtime-wry/README.md b/crates/tauri-runtime-wry/README.md new file mode 100644 index 0000000..a452527 --- /dev/null +++ b/crates/tauri-runtime-wry/README.md @@ -0,0 +1,44 @@ +# tauri-runtime-wry + + + +[![status](https://img.shields.io/badge/Status-Beta-green.svg)](https://github.com/tauri-apps/tauri) +[![Chat Server](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/SpmNs4S) + +[![test core](https://img.shields.io/github/actions/workflow/status/tauri-apps/tauri/test-core.yml?label=test%20core&logo=github)](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml) +[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app) + +[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation) +[![support](https://img.shields.io/badge/sponsor-Opencollective-blue.svg)](https://opencollective.com/tauri) + +| Component | Version | +| ----------------- | ---------------------------------------------------------------------------------------------------------------------- | +| tauri-runtime-wry | [![](https://img.shields.io/crates/v/tauri-runtime-wry?style=flat-square)](https://crates.io/crates/tauri-runtime-wry) | + +## About Tauri + +Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily. + +Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task. + +## This module + +This crate opens up direct systems-level interactions specifically for WRY, such as printing, monitor detection, and other windowing related tasks. `tauri-runtime` implementation for WRY. +None of the exposed API of this crate is stable, and it may break semver +compatibility in the future. The major version only signifies the intended Tauri version. + +To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document. + +## Semver + +**tauri** is following [Semantic Versioning 2.0](https://semver.org/). + +## Licenses + +Code: (c) 2021 - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. + +Logo: CC-BY-NC-ND + +- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum) diff --git a/crates/tauri-runtime-wry/build.rs b/crates/tauri-runtime-wry/build.rs new file mode 100644 index 0000000..9974c50 --- /dev/null +++ b/crates/tauri-runtime-wry/build.rs @@ -0,0 +1,19 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// creates a cfg alias if `has_feature` is true. +// `alias` must be a snake case string. +fn alias(alias: &str, has_feature: bool) { + println!("cargo:rustc-check-cfg=cfg({alias})"); + if has_feature { + println!("cargo:rustc-cfg={alias}"); + } +} + +fn main() { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let mobile = target_os == "ios" || target_os == "android"; + alias("desktop", !mobile); + alias("mobile", mobile); +} diff --git a/crates/tauri-runtime-wry/src/dialog/mod.rs b/crates/tauri-runtime-wry/src/dialog/mod.rs new file mode 100644 index 0000000..83d8922 --- /dev/null +++ b/crates/tauri-runtime-wry/src/dialog/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(windows)] +mod windows; + +// Takes a `&'static str` here since we convert clickable hyperlinks, +// DO NOT pass in untrusted input +#[cfg_attr(not(windows), allow(unused))] +pub fn error(err: &'static str) { + #[cfg(windows)] + windows::error(err); + + #[cfg(not(windows))] + { + unimplemented!("Error dialog is not implemented for this platform"); + } +} diff --git a/crates/tauri-runtime-wry/src/dialog/windows.rs b/crates/tauri-runtime-wry/src/dialog/windows.rs new file mode 100644 index 0000000..26850eb --- /dev/null +++ b/crates/tauri-runtime-wry/src/dialog/windows.rs @@ -0,0 +1,124 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use windows::core::{HSTRING, w}; + +enum Level { + Error, + #[allow(unused)] + Warning, + #[allow(unused)] + Info, +} + +pub fn error(err: &'static str) { + dialog_inner(err, Level::Error); +} + +fn dialog_inner(err: &str, level: Level) { + let title = match level { + Level::Warning => w!("Warning"), + Level::Error => w!("Error"), + Level::Info => w!("Info"), + }; + + #[cfg(not(feature = "common-controls-v6"))] + { + use windows::Win32::UI::WindowsAndMessaging::*; + + let err = remove_hyperlink(err); + let err = HSTRING::from(err); + + unsafe { + MessageBoxW( + None, + &err, + title, + match level { + Level::Warning => MB_ICONWARNING, + Level::Error => MB_ICONERROR, + Level::Info => MB_ICONINFORMATION, + }, + ) + }; + } + + #[cfg(feature = "common-controls-v6")] + { + use windows::Win32::Foundation::*; + use windows::Win32::UI::Controls::*; + use windows::Win32::UI::Shell::*; + use windows::Win32::UI::WindowsAndMessaging::*; + use windows::core::{HRESULT, PCWSTR}; + + extern "system" fn task_dialog_callback( + _hwnd: HWND, + msg: TASKDIALOG_NOTIFICATIONS, + _wparam: WPARAM, + lparam: LPARAM, + _data: isize, + ) -> HRESULT { + if msg == TDN_HYPERLINK_CLICKED { + let link = PCWSTR(lparam.0 as _); + let _ = unsafe { ShellExecuteW(None, None, link, None, None, SW_SHOWNORMAL) }; + } + + S_OK + } + + let err = HSTRING::from(err); + let err = PCWSTR(err.as_ptr()); + + let task_dialog_config = TASKDIALOGCONFIG { + cbSize: std::mem::size_of::() as u32, + dwFlags: TDF_ALLOW_DIALOG_CANCELLATION | TDF_ENABLE_HYPERLINKS, + pszWindowTitle: title, + pszContent: err, + Anonymous1: TASKDIALOGCONFIG_0 { + pszMainIcon: match level { + Level::Warning => TD_WARNING_ICON, + Level::Error => TD_ERROR_ICON, + Level::Info => TD_INFORMATION_ICON, + }, + }, + dwCommonButtons: TDCBF_OK_BUTTON, + pfCallback: Some(task_dialog_callback), + ..Default::default() + }; + + let _ = unsafe { TaskDialogIndirect(&task_dialog_config, None, None, None) }; + } +} + +#[cfg(not(feature = "common-controls-v6"))] +fn remove_hyperlink(str: &str) -> String { + let mut result = String::new(); + let mut in_hyperlink = false; + + for c in str.chars() { + if c == '<' { + in_hyperlink = true; + } else if c == '>' { + in_hyperlink = false; + } else if !in_hyperlink { + result.push(c); + } + } + + result +} + +#[cfg(test)] +#[cfg(not(feature = "common-controls-v6"))] +mod tests { + use super::*; + + #[test] + fn test_remove_hyperlink() { + let input = "This is a test string."; + let expected = "This is a test string."; + let result = remove_hyperlink(input); + assert_eq!(result, expected); + } +} diff --git a/crates/tauri-runtime-wry/src/monitor/linux.rs b/crates/tauri-runtime-wry/src/monitor/linux.rs new file mode 100644 index 0000000..62f87eb --- /dev/null +++ b/crates/tauri-runtime-wry/src/monitor/linux.rs @@ -0,0 +1,18 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use gtk::prelude::MonitorExt; +use tao::platform::unix::MonitorHandleExtUnix; +use tauri_runtime::dpi::{LogicalPosition, LogicalSize, PhysicalRect}; + +impl super::MonitorExt for tao::monitor::MonitorHandle { + fn work_area(&self) -> PhysicalRect { + let rect = self.gdk_monitor().workarea(); + let scale_factor = self.scale_factor(); + PhysicalRect { + size: LogicalSize::new(rect.width() as u32, rect.height() as u32).to_physical(scale_factor), + position: LogicalPosition::new(rect.x(), rect.y()).to_physical(scale_factor), + } + } +} diff --git a/crates/tauri-runtime-wry/src/monitor/macos.rs b/crates/tauri-runtime-wry/src/monitor/macos.rs new file mode 100644 index 0000000..aca8e33 --- /dev/null +++ b/crates/tauri-runtime-wry/src/monitor/macos.rs @@ -0,0 +1,36 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri_runtime::dpi::{LogicalSize, PhysicalRect}; + +impl super::MonitorExt for tao::monitor::MonitorHandle { + fn work_area(&self) -> PhysicalRect { + use objc2_app_kit::NSScreen; + use tao::platform::macos::MonitorHandleExtMacOS; + if let Some(ns_screen) = self.ns_screen() { + let ns_screen: &NSScreen = unsafe { &*ns_screen.cast() }; + let screen_frame = ns_screen.frame(); + let visible_frame = ns_screen.visibleFrame(); + + let scale_factor = self.scale_factor(); + + let mut position = self.position().to_logical::(scale_factor); + + 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), + position: position.to_physical(scale_factor), + } + } else { + PhysicalRect { + size: self.size(), + position: self.position(), + } + } + } +} diff --git a/crates/tauri-runtime-wry/src/monitor/mod.rs b/crates/tauri-runtime-wry/src/monitor/mod.rs new file mode 100644 index 0000000..4322c3c --- /dev/null +++ b/crates/tauri-runtime-wry/src/monitor/mod.rs @@ -0,0 +1,37 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri_runtime::dpi::PhysicalRect; + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +mod linux; +#[cfg(target_os = "macos")] +mod macos; +#[cfg(windows)] +mod windows; + +pub trait MonitorExt { + /// Get the work area of this monitor + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Unsupported. + fn work_area(&self) -> PhysicalRect; +} + +#[cfg(mobile)] +impl MonitorExt for tao::monitor::MonitorHandle { + fn work_area(&self) -> PhysicalRect { + PhysicalRect { + size: self.size(), + position: self.position(), + } + } +} diff --git a/crates/tauri-runtime-wry/src/monitor/windows.rs b/crates/tauri-runtime-wry/src/monitor/windows.rs new file mode 100644 index 0000000..8547ac9 --- /dev/null +++ b/crates/tauri-runtime-wry/src/monitor/windows.rs @@ -0,0 +1,32 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tao::dpi::{PhysicalPosition, PhysicalSize}; +use tauri_runtime::dpi::PhysicalRect; + +impl super::MonitorExt for tao::monitor::MonitorHandle { + fn work_area(&self) -> PhysicalRect { + use tao::platform::windows::MonitorHandleExtWindows; + use windows::Win32::Graphics::Gdi::{GetMonitorInfoW, HMONITOR, MONITORINFO}; + let mut monitor_info = MONITORINFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + let status = unsafe { GetMonitorInfoW(HMONITOR(self.hmonitor() as _), &mut monitor_info) }; + if status.as_bool() { + PhysicalRect { + size: PhysicalSize::new( + (monitor_info.rcWork.right - monitor_info.rcWork.left) as u32, + (monitor_info.rcWork.bottom - monitor_info.rcWork.top) as u32, + ), + position: PhysicalPosition::new(monitor_info.rcWork.left, monitor_info.rcWork.top), + } + } else { + PhysicalRect { + size: self.size(), + position: self.position(), + } + } + } +} diff --git a/crates/tauri-runtime-wry/src/undecorated_resizing.rs b/crates/tauri-runtime-wry/src/undecorated_resizing.rs new file mode 100644 index 0000000..d8db89a --- /dev/null +++ b/crates/tauri-runtime-wry/src/undecorated_resizing.rs @@ -0,0 +1,654 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] + +const CLIENT: isize = 0b0000; +const LEFT: isize = 0b0001; +const RIGHT: isize = 0b0010; +const TOP: isize = 0b0100; +const BOTTOM: isize = 0b1000; +const TOPLEFT: isize = TOP | LEFT; +const TOPRIGHT: isize = TOP | RIGHT; +const BOTTOMLEFT: isize = BOTTOM | LEFT; +const BOTTOMRIGHT: isize = BOTTOM | RIGHT; + +#[cfg(not(windows))] +pub use self::gtk::*; +#[cfg(windows)] +pub use self::windows::*; + +#[cfg(windows)] +type WindowPositions = i32; +#[cfg(not(windows))] +type WindowPositions = f64; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum HitTestResult { + Client, + Left, + Right, + Top, + Bottom, + TopLeft, + TopRight, + BottomLeft, + BottomRight, + NoWhere, +} + +#[allow(clippy::too_many_arguments)] +fn hit_test( + left: WindowPositions, + top: WindowPositions, + right: WindowPositions, + bottom: WindowPositions, + cx: WindowPositions, + cy: WindowPositions, + border_x: WindowPositions, + border_y: WindowPositions, +) -> HitTestResult { + #[rustfmt::skip] + let result = (LEFT * (cx < left + border_x) as isize) + | (RIGHT * (cx >= right - border_x) as isize) + | (TOP * (cy < top + border_y) as isize) + | (BOTTOM * (cy >= bottom - border_y) as isize); + + match result { + CLIENT => HitTestResult::Client, + LEFT => HitTestResult::Left, + RIGHT => HitTestResult::Right, + TOP => HitTestResult::Top, + BOTTOM => HitTestResult::Bottom, + TOPLEFT => HitTestResult::TopLeft, + TOPRIGHT => HitTestResult::TopRight, + BOTTOMLEFT => HitTestResult::BottomLeft, + BOTTOMRIGHT => HitTestResult::BottomRight, + _ => HitTestResult::NoWhere, + } +} + +#[cfg(windows)] +mod windows { + use crate::util; + + use super::{HitTestResult, hit_test}; + + use windows::Win32::System::LibraryLoader::*; + use windows::Win32::UI::WindowsAndMessaging::*; + use windows::Win32::{Foundation::*, UI::Shell::SetWindowSubclass}; + use windows::Win32::{Graphics::Gdi::*, UI::Shell::DefSubclassProc}; + use windows::core::*; + + impl HitTestResult { + fn to_win32(self) -> i32 { + match self { + HitTestResult::Left => HTLEFT as _, + HitTestResult::Right => HTRIGHT as _, + HitTestResult::Top => HTTOP as _, + HitTestResult::Bottom => HTBOTTOM as _, + HitTestResult::TopLeft => HTTOPLEFT as _, + HitTestResult::TopRight => HTTOPRIGHT as _, + HitTestResult::BottomLeft => HTBOTTOMLEFT as _, + HitTestResult::BottomRight => HTBOTTOMRIGHT as _, + _ => HTTRANSPARENT, + } + } + } + + const CLASS_NAME: PCWSTR = w!("TAURI_DRAG_RESIZE_BORDERS"); + const WINDOW_NAME: PCWSTR = w!("TAURI_DRAG_RESIZE_WINDOW"); + + const WM_UPDATE_UNDECORATED_SHADOWS: u32 = WM_USER + 100; + + struct UndecoratedResizingData { + child: HWND, + has_undecorated_shadows: bool, + } + + pub fn attach_resize_handler(hwnd: isize, has_undecorated_shadows: bool) { + let parent = HWND(hwnd as _); + + // return early if we already attached + if unsafe { FindWindowExW(Some(parent), None, CLASS_NAME, WINDOW_NAME) }.is_ok() { + return; + } + + let class = WNDCLASSEXW { + cbSize: std::mem::size_of::() as u32, + style: WNDCLASS_STYLES::default(), + lpfnWndProc: Some(drag_resize_window_proc), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: unsafe { HINSTANCE(GetModuleHandleW(PCWSTR::null()).unwrap_or_default().0) }, + hIcon: HICON::default(), + hCursor: HCURSOR::default(), + hbrBackground: HBRUSH::default(), + lpszMenuName: PCWSTR::null(), + lpszClassName: CLASS_NAME, + hIconSm: HICON::default(), + }; + + unsafe { RegisterClassExW(&class) }; + + let mut rect = RECT::default(); + unsafe { GetClientRect(parent, &mut rect).unwrap() }; + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + + let data = UndecoratedResizingData { + child: HWND::default(), + has_undecorated_shadows, + }; + + let Ok(drag_window) = (unsafe { + CreateWindowExW( + WINDOW_EX_STYLE::default(), + CLASS_NAME, + WINDOW_NAME, + WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS, + 0, + 0, + width, + height, + Some(parent), + None, + GetModuleHandleW(None).ok().map(|h| HINSTANCE(h.0)), + Some(Box::into_raw(Box::new(data)) as _), + ) + }) else { + return; + }; + + unsafe { + set_drag_hwnd_rgn(drag_window, width, height, has_undecorated_shadows); + + let _ = SetWindowPos( + drag_window, + Some(HWND_TOP), + 0, + 0, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOSIZE, + ); + + let data = UndecoratedResizingData { + child: drag_window, + has_undecorated_shadows, + }; + + let _ = SetWindowSubclass( + parent, + Some(subclass_parent), + (WM_USER + 1) as _, + Box::into_raw(Box::new(data)) as _, + ); + } + } + + unsafe extern "system" fn subclass_parent( + parent: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + _: usize, + data: usize, + ) -> LRESULT { + unsafe { + match msg { + WM_SIZE => { + let data = data as *mut UndecoratedResizingData; + let data = &*data; + let child = data.child; + let has_undecorated_shadows = data.has_undecorated_shadows; + + // when parent is maximized, remove the undecorated window drag resize region + if is_maximized(parent).unwrap_or(false) { + let _ = SetWindowPos( + child, + Some(HWND_TOP), + 0, + 0, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOMOVE, + ); + } else { + // otherwise update the cutout region + let mut rect = RECT::default(); + if GetClientRect(parent, &mut rect).is_ok() { + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + + let _ = SetWindowPos( + child, + Some(HWND_TOP), + 0, + 0, + width, + height, + SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOMOVE, + ); + + set_drag_hwnd_rgn(child, width, height, has_undecorated_shadows); + } + } + } + + WM_UPDATE_UNDECORATED_SHADOWS => { + let data = data as *mut UndecoratedResizingData; + let data = &mut *data; + data.has_undecorated_shadows = wparam.0 != 0; + } + + WM_NCDESTROY => { + let data = data as *mut UndecoratedResizingData; + drop(Box::from_raw(data)); + } + + _ => {} + } + + DefSubclassProc(parent, msg, wparam, lparam) + } + } + + unsafe extern "system" fn drag_resize_window_proc( + child: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + unsafe { + match msg { + WM_CREATE => { + let data = lparam.0 as *mut CREATESTRUCTW; + let data = (*data).lpCreateParams as *mut UndecoratedResizingData; + (*data).child = child; + SetWindowLongPtrW(child, GWLP_USERDATA, data as _); + } + + WM_NCHITTEST => { + let data = GetWindowLongPtrW(child, GWLP_USERDATA); + let data = &*(data as *mut UndecoratedResizingData); + + let Ok(parent) = GetParent(child) else { + return DefWindowProcW(child, msg, wparam, lparam); + }; + let style = GetWindowLongPtrW(parent, GWL_STYLE); + let style = WINDOW_STYLE(style as u32); + + let is_resizable = (style & WS_SIZEBOX).0 != 0; + if !is_resizable { + return DefWindowProcW(child, msg, wparam, lparam); + } + + // if the window has undecorated shadows, + // it should always be the top border, + // ensured by the cutout drag window + if data.has_undecorated_shadows { + return LRESULT(HTTOP as _); + } + + let mut rect = RECT::default(); + if GetWindowRect(child, &mut rect).is_err() { + return DefWindowProcW(child, msg, wparam, lparam); + } + + let (cx, cy) = (GET_X_LPARAM(lparam) as i32, GET_Y_LPARAM(lparam) as i32); + + let dpi = util::hwnd_dpi(child); + let border_x = util::get_system_metrics_for_dpi(SM_CXFRAME, dpi); + let border_y = util::get_system_metrics_for_dpi(SM_CYFRAME, dpi); + + let res = hit_test( + rect.left, + rect.top, + rect.right, + rect.bottom, + cx, + cy, + border_x, + border_y, + ); + + return LRESULT(res.to_win32() as _); + } + + WM_NCLBUTTONDOWN => { + let data = GetWindowLongPtrW(child, GWLP_USERDATA); + let data = &*(data as *mut UndecoratedResizingData); + + let Ok(parent) = GetParent(child) else { + return DefWindowProcW(child, msg, wparam, lparam); + }; + let style = GetWindowLongPtrW(parent, GWL_STYLE); + let style = WINDOW_STYLE(style as u32); + + let is_resizable = (style & WS_SIZEBOX).0 != 0; + if !is_resizable { + return DefWindowProcW(child, msg, wparam, lparam); + } + + let (cx, cy) = (GET_X_LPARAM(lparam) as i32, GET_Y_LPARAM(lparam) as i32); + + // if the window has undecorated shadows, + // it should always be the top border, + // ensured by the cutout drag window + let res = if data.has_undecorated_shadows { + HitTestResult::Top + } else { + let mut rect = RECT::default(); + if GetWindowRect(child, &mut rect).is_err() { + return DefWindowProcW(child, msg, wparam, lparam); + } + + let dpi = util::hwnd_dpi(child); + let border_x = util::get_system_metrics_for_dpi(SM_CXFRAME, dpi); + let border_y = util::get_system_metrics_for_dpi(SM_CYFRAME, dpi); + + hit_test( + rect.left, + rect.top, + rect.right, + rect.bottom, + cx, + cy, + border_x, + border_y, + ) + }; + + if res != HitTestResult::NoWhere { + let points = POINTS { + x: cx as i16, + y: cy as i16, + }; + + let _ = PostMessageW( + Some(parent), + WM_NCLBUTTONDOWN, + WPARAM(res.to_win32() as _), + LPARAM(&points as *const _ as _), + ); + } + + return LRESULT(0); + } + + WM_UPDATE_UNDECORATED_SHADOWS => { + let data = GetWindowLongPtrW(child, GWLP_USERDATA); + let data = &mut *(data as *mut UndecoratedResizingData); + data.has_undecorated_shadows = wparam.0 != 0; + } + + WM_NCDESTROY => { + let data = GetWindowLongPtrW(child, GWLP_USERDATA); + let data = data as *mut UndecoratedResizingData; + drop(Box::from_raw(data)); + } + + _ => {} + } + + DefWindowProcW(child, msg, wparam, lparam) + } + } + + pub fn detach_resize_handler(hwnd: isize) { + let hwnd = HWND(hwnd as _); + + let Ok(child) = (unsafe { FindWindowExW(Some(hwnd), None, CLASS_NAME, WINDOW_NAME) }) else { + return; + }; + + let _ = unsafe { DestroyWindow(child) }; + } + + unsafe fn set_drag_hwnd_rgn(hwnd: HWND, width: i32, height: i32, only_top: bool) { + unsafe { + // The window used for drag resizing an undecorated window + // is a child HWND that covers the whole window + // and so we need create a cut out in the middle for the parent and other child + // windows like the webview can receive mouse events. + + let dpi = util::hwnd_dpi(hwnd); + let border_x = util::get_system_metrics_for_dpi(SM_CXFRAME, dpi); + let border_y = util::get_system_metrics_for_dpi(SM_CYFRAME, dpi); + + // hrgn1 must be mutable to call .free() later + let mut hrgn1 = CreateRectRgn(0, 0, width, height); + + let x1 = if only_top { 0 } else { border_x }; + let y1 = border_y; + let x2 = if only_top { width } else { width - border_x }; + let y2 = if only_top { height } else { height - border_y }; + + // Wrap hrgn2 in Owned so it is automatically freed when going out of scope + let hrgn2 = Owned::new(CreateRectRgn(x1, y1, x2, y2)); + + CombineRgn(Some(hrgn1), Some(hrgn1), Some(*hrgn2), RGN_DIFF); + + // Try to set the window region + if SetWindowRgn(hwnd, Some(hrgn1), true) == 0 { + // If it fails, we must free hrgn1 manually + hrgn1.free(); + } + } + } + + pub fn update_drag_hwnd_rgn_for_undecorated(hwnd: isize, has_undecorated_shadows: bool) { + let hwnd = HWND(hwnd as _); + + let mut rect = RECT::default(); + let Ok(_) = (unsafe { GetClientRect(hwnd, &mut rect) }) else { + return; + }; + + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + + let Ok(child) = (unsafe { FindWindowExW(Some(hwnd), None, CLASS_NAME, WINDOW_NAME) }) else { + return; + }; + + unsafe { set_drag_hwnd_rgn(child, width, height, has_undecorated_shadows) }; + + unsafe { + SendMessageW( + hwnd, + WM_UPDATE_UNDECORATED_SHADOWS, + None, + Some(LPARAM(has_undecorated_shadows as _)), + ) + }; + unsafe { + SendMessageW( + child, + WM_UPDATE_UNDECORATED_SHADOWS, + None, + Some(LPARAM(has_undecorated_shadows as _)), + ) + }; + } + + fn is_maximized(window: HWND) -> windows::core::Result { + let mut placement = WINDOWPLACEMENT { + length: std::mem::size_of::() as u32, + ..WINDOWPLACEMENT::default() + }; + unsafe { GetWindowPlacement(window, &mut placement)? }; + Ok(placement.showCmd == SW_MAXIMIZE.0 as u32) + } + + /// Implementation of the `GET_X_LPARAM` macro. + #[allow(non_snake_case)] + #[inline] + fn GET_X_LPARAM(lparam: LPARAM) -> i16 { + ((lparam.0 as usize) & 0xFFFF) as u16 as i16 + } + + /// Implementation of the `GET_Y_LPARAM` macro. + #[allow(non_snake_case)] + #[inline] + fn GET_Y_LPARAM(lparam: LPARAM) -> i16 { + (((lparam.0 as usize) & 0xFFFF_0000) >> 16) as u16 as i16 + } +} + +#[cfg(not(windows))] +mod gtk { + use super::{HitTestResult, hit_test}; + use gtk::prelude::*; + + const BORDERLESS_RESIZE_INSET: i32 = 5; + + impl HitTestResult { + fn to_gtk_edge(self) -> gtk::gdk::WindowEdge { + match self { + HitTestResult::Client | HitTestResult::NoWhere => gtk::gdk::WindowEdge::__Unknown(0), + HitTestResult::Left => gtk::gdk::WindowEdge::West, + HitTestResult::Right => gtk::gdk::WindowEdge::East, + HitTestResult::Top => gtk::gdk::WindowEdge::North, + HitTestResult::Bottom => gtk::gdk::WindowEdge::South, + HitTestResult::TopLeft => gtk::gdk::WindowEdge::NorthWest, + HitTestResult::TopRight => gtk::gdk::WindowEdge::NorthEast, + HitTestResult::BottomLeft => gtk::gdk::WindowEdge::SouthWest, + HitTestResult::BottomRight => gtk::gdk::WindowEdge::SouthEast, + } + } + + fn to_cursor(self, display: >k::gdk::Display) -> Option { + use gtk::gdk::CursorType; + + let cursor_type = match self { + HitTestResult::Left => CursorType::LeftSide, + HitTestResult::Right => CursorType::RightSide, + HitTestResult::Top => CursorType::TopSide, + HitTestResult::Bottom => CursorType::BottomSide, + HitTestResult::TopLeft => CursorType::TopLeftCorner, + HitTestResult::TopRight => CursorType::TopRightCorner, + HitTestResult::BottomLeft => CursorType::BottomLeftCorner, + HitTestResult::BottomRight => CursorType::BottomRightCorner, + HitTestResult::Client | HitTestResult::NoWhere => return None, + }; + + gtk::gdk::Cursor::for_display(display, cursor_type) + } + } + + fn resizable_window(webview: &webkit2gtk::WebView) -> Option { + let window = webview.parent()?.parent()?.downcast::().ok()?; + (!window.is_decorated() && window.is_resizable() && !window.is_maximized()).then_some(window) + } + + fn hit_test_window(window: >k::gdk::Window, x: f64, y: f64) -> HitTestResult { + let border = window.scale_factor() * BORDERLESS_RESIZE_INSET; + hit_test( + 0.0, + 0.0, + window.width() as f64, + window.height() as f64, + x, + y, + border as _, + border as _, + ) + } + + fn hit_test_window_root(window: >k::gdk::Window, root_x: f64, root_y: f64) -> HitTestResult { + let (window_x, window_y) = window.position(); + hit_test_window(window, root_x - window_x as f64, root_y - window_y as f64) + } + + pub fn attach_resize_handler(webview: &wry::WebView) { + use gtk::{gdk::WindowEdge, glib::Propagation}; + use wry::WebViewExtUnix; + + let webview = webview.webview(); + + webview.add_events( + gtk::gdk::EventMask::POINTER_MOTION_MASK + | gtk::gdk::EventMask::BUTTON1_MOTION_MASK + | gtk::gdk::EventMask::BUTTON_PRESS_MASK + | gtk::gdk::EventMask::TOUCH_MASK, + ); + + webview.connect_motion_notify_event( + move |webview: &webkit2gtk::WebView, event: >k::gdk::EventMotion| { + let Some(window) = resizable_window(webview).and_then(|_| webview.window()) else { + return Propagation::Proceed; + }; + + let (x, y) = event.position(); + let result = hit_test_window(&window, x, y); + if matches!(result, HitTestResult::Client | HitTestResult::NoWhere) { + return Propagation::Proceed; + } + + let cursor = result.to_cursor(&window.display()); + window.set_cursor(cursor.as_ref()); + Propagation::Stop + }, + ); + + webview.connect_button_press_event( + move |webview: &webkit2gtk::WebView, event: >k::gdk::EventButton| { + if event.button() != 1 { + return Propagation::Proceed; + } + let Some(window) = resizable_window(webview).and_then(|w| w.window()) else { + return Propagation::Proceed; + }; + + let (root_x, root_y) = event.root(); + let edge = hit_test_window_root(&window, root_x, root_y).to_gtk_edge(); + if matches!(&edge, WindowEdge::__Unknown(_)) { + return Propagation::Proceed; + } + + window.begin_resize_drag(edge, 1, root_x as i32, root_y as i32, event.time()); + // Prevent the webview from handling an event claimed by the resize inset + Propagation::Stop + }, + ); + + webview.connect_touch_event( + move |webview: &webkit2gtk::WebView, event: >k::gdk::Event| { + let Some(window) = resizable_window(webview).and_then(|w| w.window()) else { + return Propagation::Proceed; + }; + let Some((root_x, root_y)) = event.root_coords() else { + return Propagation::Proceed; + }; + let Some(device) = event.device() else { + return Propagation::Proceed; + }; + + let edge = hit_test_window_root(&window, root_x, root_y).to_gtk_edge(); + if matches!(&edge, WindowEdge::__Unknown(_)) { + return Propagation::Proceed; + } + + window.begin_resize_drag_for_device( + edge, + &device, + 0, + root_x as i32, + root_y as i32, + event.time(), + ); + Propagation::Proceed + }, + ); + } +} diff --git a/crates/tauri-runtime-wry/src/util.rs b/crates/tauri-runtime-wry/src/util.rs new file mode 100644 index 0000000..ede110f --- /dev/null +++ b/crates/tauri-runtime-wry/src/util.rs @@ -0,0 +1,124 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg_attr(not(windows), allow(unused_imports))] +pub use imp::*; + +#[cfg(not(windows))] +mod imp {} + +#[cfg(windows)] +mod imp { + use std::{iter::once, os::windows::ffi::OsStrExt}; + + use once_cell::sync::Lazy; + use windows::{ + Win32::{ + Foundation::*, + Graphics::Gdi::*, + System::LibraryLoader::{GetProcAddress, LoadLibraryW}, + UI::{HiDpi::*, WindowsAndMessaging::*}, + }, + core::{HRESULT, PCSTR, PCWSTR}, + }; + + pub fn encode_wide(string: impl AsRef) -> Vec { + string.as_ref().encode_wide().chain(once(0)).collect() + } + + // Helper function to dynamically load function pointer. + // `library` and `function` must be zero-terminated. + pub(super) fn get_function_impl(library: &str, function: &str) -> FARPROC { + let library = encode_wide(library); + assert_eq!(function.chars().last(), Some('\0')); + + // Library names we will use are ASCII so we can use the A version to avoid string conversion. + let module = unsafe { LoadLibraryW(PCWSTR::from_raw(library.as_ptr())) }.unwrap_or_default(); + if module.is_invalid() { + return None; + } + + unsafe { GetProcAddress(module, PCSTR::from_raw(function.as_ptr())) } + } + + macro_rules! get_function { + ($lib:expr, $func:ident) => { + $crate::util::get_function_impl($lib, concat!(stringify!($func), '\0')) + .map(|f| unsafe { std::mem::transmute::<_, $func>(f) }) + }; + } + + type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32; + type GetDpiForMonitor = unsafe extern "system" fn( + hmonitor: HMONITOR, + dpi_type: MONITOR_DPI_TYPE, + dpi_x: *mut u32, + dpi_y: *mut u32, + ) -> HRESULT; + type GetSystemMetricsForDpi = + unsafe extern "system" fn(nindex: SYSTEM_METRICS_INDEX, dpi: u32) -> i32; + + static GET_DPI_FOR_WINDOW: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetDpiForWindow)); + static GET_DPI_FOR_MONITOR: Lazy> = + Lazy::new(|| get_function!("shcore.dll", GetDpiForMonitor)); + static GET_SYSTEM_METRICS_FOR_DPI: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetSystemMetricsForDpi)); + + #[allow(non_snake_case)] + pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 { + unsafe { + if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW { + // We are on Windows 10 Anniversary Update (1607) or later. + match GetDpiForWindow(hwnd) { + 0 => USER_DEFAULT_SCREEN_DPI, // 0 is returned if hwnd is invalid + dpi => dpi, + } + } else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { + // We are on Windows 8.1 or later. + let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + if monitor.is_invalid() { + return USER_DEFAULT_SCREEN_DPI; + } + + let mut dpi_x = 0; + let mut dpi_y = 0; + if GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y).is_ok() { + dpi_x + } else { + USER_DEFAULT_SCREEN_DPI + } + } else { + // We are on Vista or later. + if IsProcessDPIAware().as_bool() { + let hdc = GetDC(Some(hwnd)); + if hdc.is_invalid() { + return USER_DEFAULT_SCREEN_DPI; + } + // If the process is DPI aware, then scaling must be handled by the application using + // this DPI value. + let dpi = GetDeviceCaps(Some(hdc), LOGPIXELSX) as u32; + ReleaseDC(Some(hwnd), hdc); + dpi + } else { + // If the process is DPI unaware, then scaling is performed by the OS; we thus return + // 96 (scale factor 1.0) to prevent the window from being re-scaled by both the + // application and the WM. + USER_DEFAULT_SCREEN_DPI + } + } + } + } + + #[allow(non_snake_case)] + pub unsafe fn get_system_metrics_for_dpi(nindex: SYSTEM_METRICS_INDEX, dpi: u32) -> i32 { + unsafe { + if let Some(GetSystemMetricsForDpi) = *GET_SYSTEM_METRICS_FOR_DPI { + GetSystemMetricsForDpi(nindex, dpi) + } else { + GetSystemMetrics(nindex) + } + } + } +} diff --git a/crates/tauri-runtime-wry/src/webview.rs b/crates/tauri-runtime-wry/src/webview.rs new file mode 100644 index 0000000..69a7863 --- /dev/null +++ b/crates/tauri-runtime-wry/src/webview.rs @@ -0,0 +1,49 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +mod imp { + pub type Webview = webkit2gtk::WebView; +} + +#[cfg(target_vendor = "apple")] +mod imp { + use std::ffi::c_void; + + // These pointers are borrowed from ObjC `Retained` handles owned elsewhere and must + // not be mutated through. TODO: change these to `*const c_void` in v3 (breaking change). + pub struct Webview { + pub webview: *mut c_void, + pub manager: *mut c_void, + #[cfg(target_os = "macos")] + pub ns_window: *mut c_void, + #[cfg(target_os = "ios")] + pub view_controller: *mut c_void, + } +} + +#[cfg(windows)] +mod imp { + use webview2_com::Microsoft::Web::WebView2::Win32::{ + ICoreWebView2Controller, ICoreWebView2Environment, + }; + pub struct Webview { + pub controller: ICoreWebView2Controller, + pub environment: ICoreWebView2Environment, + } +} + +#[cfg(target_os = "android")] +mod imp { + use wry::JniHandle; + pub type Webview = JniHandle; +} + +pub use imp::*; diff --git a/crates/tauri-runtime-wry/src/webview_permissions.rs b/crates/tauri-runtime-wry/src/webview_permissions.rs new file mode 100644 index 0000000..98e1f57 --- /dev/null +++ b/crates/tauri-runtime-wry/src/webview_permissions.rs @@ -0,0 +1,35 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri_runtime::webview::{PermissionKind, PermissionResponse}; + +pub(super) fn from_wry_permission_kind(kind: wry::PermissionKind) -> PermissionKind { + match kind { + wry::PermissionKind::Microphone => PermissionKind::Microphone, + wry::PermissionKind::Camera => PermissionKind::Camera, + wry::PermissionKind::Geolocation => PermissionKind::Geolocation, + wry::PermissionKind::Notifications => PermissionKind::Notifications, + wry::PermissionKind::ClipboardRead => PermissionKind::ClipboardRead, + wry::PermissionKind::DisplayCapture => PermissionKind::DisplayCapture, + wry::PermissionKind::Midi => PermissionKind::Midi, + wry::PermissionKind::Sensors => PermissionKind::Sensors, + wry::PermissionKind::MediaKeySystemAccess => PermissionKind::MediaKeySystemAccess, + wry::PermissionKind::LocalFonts => PermissionKind::LocalFonts, + wry::PermissionKind::WindowManagement => PermissionKind::WindowManagement, + wry::PermissionKind::PointerLock => PermissionKind::PointerLock, + wry::PermissionKind::AutomaticDownloads => PermissionKind::AutomaticDownloads, + wry::PermissionKind::FileSystemAccess => PermissionKind::FileSystemAccess, + wry::PermissionKind::Autoplay => PermissionKind::Autoplay, + wry::PermissionKind::Other => PermissionKind::Other, + _ => PermissionKind::Other, + } +} + +pub(super) fn to_wry_permission_response(response: PermissionResponse) -> wry::PermissionResponse { + match response { + PermissionResponse::Allow => wry::PermissionResponse::Allow, + PermissionResponse::Deny => wry::PermissionResponse::Deny, + PermissionResponse::Default => wry::PermissionResponse::Default, + } +} diff --git a/crates/tauri-runtime-wry/src/window/linux.rs b/crates/tauri-runtime-wry/src/window/linux.rs new file mode 100644 index 0000000..e7ee927 --- /dev/null +++ b/crates/tauri-runtime-wry/src/window/linux.rs @@ -0,0 +1,31 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use gtk::prelude::*; +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +use tao::platform::unix::WindowExtUnix; + +impl super::WindowExt for tao::window::Window { + fn set_enabled(&self, enabled: bool) { + self.gtk_window().set_sensitive(enabled); + } + + fn is_enabled(&self) -> bool { + self.gtk_window().is_sensitive() + } + + fn center(&self) { + if let Some(monitor) = self.current_monitor() { + let window_size = self.outer_size(); + let new_pos = super::calculate_window_center_position(window_size, monitor); + self.set_outer_position(new_pos); + } + } +} diff --git a/crates/tauri-runtime-wry/src/window/macos.rs b/crates/tauri-runtime-wry/src/window/macos.rs new file mode 100644 index 0000000..4a1d9a3 --- /dev/null +++ b/crates/tauri-runtime-wry/src/window/macos.rs @@ -0,0 +1,43 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use objc2::MainThreadMarker; +use objc2_app_kit::{NSBackingStoreType, NSWindow, NSWindowStyleMask}; +use tao::platform::macos::WindowExtMacOS; + +impl super::WindowExt for tao::window::Window { + // based on electron implementation + // https://github.com/electron/electron/blob/15db63e26df3e3d59ce6281f030624f746518511/shell/browser/native_window_mac.mm#L474 + fn set_enabled(&self, enabled: bool) { + let ns_window: &NSWindow = unsafe { &*self.ns_window().cast() }; + if !enabled { + let frame = ns_window.frame(); + let mtm = MainThreadMarker::new() + .expect("`Window::set_enabled` can only be called on the main thread"); + let sheet = unsafe { + NSWindow::initWithContentRect_styleMask_backing_defer( + mtm.alloc(), + frame, + NSWindowStyleMask::Titled, + NSBackingStoreType::Buffered, + false, + ) + }; + sheet.setAlphaValue(0.5); + ns_window.beginSheet_completionHandler(&sheet, None); + } else if let Some(attached) = ns_window.attachedSheet() { + ns_window.endSheet(&attached); + } + } + + fn is_enabled(&self) -> bool { + let ns_window: &NSWindow = unsafe { &*self.ns_window().cast() }; + ns_window.attachedSheet().is_none() + } + + fn center(&self) { + let ns_window: &NSWindow = unsafe { &*self.ns_window().cast() }; + ns_window.center(); + } +} diff --git a/crates/tauri-runtime-wry/src/window/mod.rs b/crates/tauri-runtime-wry/src/window/mod.rs new file mode 100644 index 0000000..56384f4 --- /dev/null +++ b/crates/tauri-runtime-wry/src/window/mod.rs @@ -0,0 +1,74 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +mod linux; +#[cfg(target_os = "macos")] +mod macos; +#[cfg(windows)] +mod windows; + +#[cfg(desktop)] +use crate::monitor::MonitorExt; + +pub trait WindowExt { + /// Enable or disable the window + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Unsupported. + fn set_enabled(&self, enabled: bool); + + /// Whether the window is enabled or disabled. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Unsupported, always returns `true`. + fn is_enabled(&self) -> bool; + + /// Center the window + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Unsupported. + fn center(&self) {} + + /// Clears the window surface. i.e make it transparent. + #[cfg(windows)] + fn draw_surface( + &self, + surface: &mut softbuffer::Surface< + std::sync::Arc, + std::sync::Arc, + >, + background_color: Option, + ); +} + +#[cfg(mobile)] +impl WindowExt for tao::window::Window { + fn set_enabled(&self, _: bool) {} + fn is_enabled(&self) -> bool { + true + } +} + +#[cfg(desktop)] +pub fn calculate_window_center_position( + window_size: tao::dpi::PhysicalSize, + target_monitor: tao::monitor::MonitorHandle, +) -> tao::dpi::PhysicalPosition { + let work_area = target_monitor.work_area(); + + tao::dpi::PhysicalPosition::new( + (work_area.size.width as i32 - window_size.width as i32) / 2 + work_area.position.x, + (work_area.size.height as i32 - window_size.height as i32) / 2 + work_area.position.y, + ) +} diff --git a/crates/tauri-runtime-wry/src/window/windows.rs b/crates/tauri-runtime-wry/src/window/windows.rs new file mode 100644 index 0000000..1bfff48 --- /dev/null +++ b/crates/tauri-runtime-wry/src/window/windows.rs @@ -0,0 +1,68 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use windows::Win32::{ + Foundation::{HWND, RECT}, + Graphics::Dwm::{DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute}, + UI::Input::KeyboardAndMouse::{EnableWindow, IsWindowEnabled}, +}; + +use tao::platform::windows::WindowExtWindows; + +impl super::WindowExt for tao::window::Window { + fn set_enabled(&self, enabled: bool) { + let _ = unsafe { EnableWindow(HWND(self.hwnd() as _), enabled) }; + } + + fn is_enabled(&self) -> bool { + unsafe { IsWindowEnabled(HWND(self.hwnd() as _)) }.as_bool() + } + + fn center(&self) { + if let Some(monitor) = self.current_monitor() { + let mut window_size = self.outer_size(); + + if self.is_decorated() { + let mut rect = RECT::default(); + let result = unsafe { + DwmGetWindowAttribute( + HWND(self.hwnd() as _), + DWMWA_EXTENDED_FRAME_BOUNDS, + &mut rect as *mut _ as *mut _, + std::mem::size_of::() as u32, + ) + }; + if result.is_ok() { + window_size.height = (rect.bottom - rect.top) as u32; + } + } + + let new_pos = super::calculate_window_center_position(window_size, monitor); + self.set_outer_position(new_pos); + } + } + + fn draw_surface( + &self, + surface: &mut softbuffer::Surface< + std::sync::Arc, + std::sync::Arc, + >, + background_color: Option, + ) { + let size = self.inner_size(); + if let (Some(width), Some(height)) = ( + std::num::NonZeroU32::new(size.width), + std::num::NonZeroU32::new(size.height), + ) { + surface.resize(width, height).unwrap(); + let mut buffer = surface.buffer_mut().unwrap(); + let color = background_color + .map(|(r, g, b, _)| (b as u32) | ((g as u32) << 8) | ((r as u32) << 16)) + .unwrap_or(0); + buffer.fill(color); + let _ = buffer.present(); + } + } +} diff --git a/crates/tauri-runtime/CHANGELOG.md b/crates/tauri-runtime/CHANGELOG.md new file mode 100644 index 0000000..b719f44 --- /dev/null +++ b/crates/tauri-runtime/CHANGELOG.md @@ -0,0 +1,1101 @@ +# Changelog + +## \[2.11.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.3` + +## \[2.11.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.9.2` + +## \[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 + +- Upgraded to `tauri-utils@2.8.3` + +## \[2.10.0] + +### Bug Fixes + +- [`9b242e40c`](https://www.github.com/tauri-apps/tauri/commit/9b242e40c844189c877a91e513ae6196202d5ae9) ([#14700](https://www.github.com/tauri-apps/tauri/pull/14700) by [@mewi99](https://www.github.com/tauri-apps/tauri/../../mewi99)) Fix compilation errors when targeting BSD. + +### Dependencies + +- Upgraded to `tauri-utils@2.8.2` +- [`75057c7c0`](https://www.github.com/tauri-apps/tauri/commit/75057c7c08f0d4d3dd8d10cea4e2217e5d61fe1a) ([#14778](https://www.github.com/tauri-apps/tauri/pull/14778) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) **Breaking Change** for `with_webview` users: Updated webkit2gtk-rs crates to `v2.0.2`. + +## \[2.9.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.8.1` + +## \[2.9.1] + +### Bug Fixes + +- [`4b6b8690a`](https://www.github.com/tauri-apps/tauri/commit/4b6b8690ab886ebdf1307951cffbe03e31280baa) ([#14347](https://www.github.com/tauri-apps/tauri/pull/14347) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Fixed an issue that caused docs.rs builds to fail. No user facing changes. + +## \[2.9.0] + +### New Features + +- [`f5851ee00`](https://www.github.com/tauri-apps/tauri/commit/f5851ee00d6d1f4d560a220ca5a728fedd525092) ([#14089](https://www.github.com/tauri-apps/tauri/pull/14089)) Adds the `scroll_bar_style` option to the Webview and WebviewWindow builders. + The possible values for this option are gated behind conditional compilation + flags, and will need to be applied using conditional compilation if customised. + +### Dependencies + +- Upgraded to `tauri-utils@2.8.0` + +## \[2.8.0] + +### New Features + +- [`68874c68c`](https://www.github.com/tauri-apps/tauri/commit/68874c68c566638b4c21a3aa67844d1bdaeb6dab) ([#13564](https://www.github.com/tauri-apps/tauri/pull/13564) by [@robertrpf](https://www.github.com/tauri-apps/tauri/../../robertrpf)) Add window focusable attribute and set_focusable API. +- [`22d6bcacb`](https://www.github.com/tauri-apps/tauri/commit/22d6bcacbb2001eb292ebd8c5d021447700f9512) ([#14008](https://www.github.com/tauri-apps/tauri/pull/14008) by [@WSH032](https://www.github.com/tauri-apps/tauri/../../WSH032)) Implement `App::set_device_event_filter` for `AppHandle` also. +- [`33d0b3f0c`](https://www.github.com/tauri-apps/tauri/commit/33d0b3f0c133edebb1c716e2f5942d70509ae347) ([#13876](https://www.github.com/tauri-apps/tauri/pull/13876) by [@thlstsul](https://www.github.com/tauri-apps/tauri/../../thlstsul)) Added `WebviewBuilder::on_document_title_changed` and `WebviewWindowBuilder::on_document_title_changed`. +- [`33d0b3f0c`](https://www.github.com/tauri-apps/tauri/commit/33d0b3f0c133edebb1c716e2f5942d70509ae347) ([#13876](https://www.github.com/tauri-apps/tauri/pull/13876) by [@thlstsul](https://www.github.com/tauri-apps/tauri/../../thlstsul)) Added `WebviewBuilder::on_new_window` and `WebviewWindowBuilder::on_new_window`. +- [`dfadcb764`](https://www.github.com/tauri-apps/tauri/commit/dfadcb764bdf84089a5487005a7b4f3b7cf09494) ([#13661](https://www.github.com/tauri-apps/tauri/pull/13661) by [@WSH032](https://www.github.com/tauri-apps/tauri/../../WSH032)) Added `WebviewDispatch::set_cookie()` and `WebviewDispatch::delete_cookie()`. +- [`5110a762e`](https://www.github.com/tauri-apps/tauri/commit/5110a762e9db978a28a15400bf76e3c864da2a86) ([#13830](https://www.github.com/tauri-apps/tauri/pull/13830) by [@Sky-walkerX](https://www.github.com/tauri-apps/tauri/../../Sky-walkerX)) Added `Window::set_simple_fullscreen`. + +### Dependencies + +- Upgraded to `tauri-utils@2.7.0` + +## \[2.7.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.6.0` + +## \[2.7.0] + +### Dependencies + +- Upgraded to `tauri-utils@2.5.0` +- [`9c16eefa3`](https://www.github.com/tauri-apps/tauri/commit/9c16eefa319b4697bac1d1019bbb5f93eca63173) ([#13629](https://www.github.com/tauri-apps/tauri/pull/13629)) Update html5ever to 0.29 and kuchikiki to version 0.8.8-speedreader. + +## \[2.6.0] + +### New Features + +- [`dd4f13ce4`](https://www.github.com/tauri-apps/tauri/commit/dd4f13ce4b3cd89cde2fa3f18a063c272f215621) ([#13185](https://www.github.com/tauri-apps/tauri/pull/13185)) MacOS: Add `set_dock_visibility` method to support setting the visibility of the application in the dock. +- [`8cf662e34`](https://www.github.com/tauri-apps/tauri/commit/8cf662e34bf738a0d16bb7b9aeb35667e2e4984b) ([#13076](https://www.github.com/tauri-apps/tauri/pull/13076)) - add API to run initialization scripts on all frames + \- `WebviewBuilder::initialization_script_on_all_frames` + \- `WebviewWindowBuilder::initialization_script_on_all_frames` + \- `WebviewAttributes::initialization_script_on_all_frames` +- [`ea36294cb`](https://www.github.com/tauri-apps/tauri/commit/ea36294cbca98f7725c91d1464fd92e77c89698a) ([#13208](https://www.github.com/tauri-apps/tauri/pull/13208)) Added `WebviewAttributes::input_accessory_view_builder` on iOS. +- [`c1cd0a2dd`](https://www.github.com/tauri-apps/tauri/commit/c1cd0a2ddb5bc3e99451cbe399b5fc9f0035f571) ([#13090](https://www.github.com/tauri-apps/tauri/pull/13090)) macOS/iOS: add option to disable or enable link previews when building a webview (the webkit api has it enabled by default) + + - `WebViewBuilder.allow_link_preview(allow_link_preview: bool)` + - `WebviewWindowBuilder.allow_link_preview(allow_link_preview: bool)` +- [`b072e2b29`](https://www.github.com/tauri-apps/tauri/commit/b072e2b2967640ae4fa1af466ae878c156551edd) ([#9687](https://www.github.com/tauri-apps/tauri/pull/9687)) Add `preventOverflow` config option to prevent the window from overflowing the monitor size on creation + +### Dependencies + +- Upgraded to `tauri-utils@2.4.0` +- [`bb5faa21f`](https://www.github.com/tauri-apps/tauri/commit/bb5faa21f418dd765ce81b495b56e9c519251b6d) ([#13163](https://www.github.com/tauri-apps/tauri/pull/13163)) Update windows to 0.61. + +## \[2.5.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.3.1` + +## \[2.5.0] + +### New Features + +- [`be2e6b85f`](https://www.github.com/tauri-apps/tauri/commit/be2e6b85fed226732b4a98f68cc5d72b4f8f5a13) ([#12944](https://www.github.com/tauri-apps/tauri/pull/12944) by [@Simon-Laux](https://www.github.com/tauri-apps/tauri/../../Simon-Laux)) add `Window.is_always_on_top()` and `WebviewWindow.is_always_on_top()` +- [`658e5f5d1`](https://www.github.com/tauri-apps/tauri/commit/658e5f5d1dc1bd970ae572a42447448d064a7fee) ([#12668](https://www.github.com/tauri-apps/tauri/pull/12668) by [@thomaseizinger](https://www.github.com/tauri-apps/tauri/../../thomaseizinger)) Add `App::run_return` function. Contrary to `App::run`, this will **not** exit the process but instead return the requested exit-code. This allows the host app to perform further cleanup after Tauri has exited. `App::run_return` is not available on iOS and fallbacks to the regular `App::run` functionality. + + The `App::run_iteration` function is deprecated as part of this because calling it in a loop - as suggested by the name - will cause a busy-loop. +- [`30f5a1553`](https://www.github.com/tauri-apps/tauri/commit/30f5a1553d3c0ce460c9006764200a9210915a44) ([#12366](https://www.github.com/tauri-apps/tauri/pull/12366) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Added `trafficLightPosition` window configuration to set the traffic light buttons position on macOS. +- [`30f5a1553`](https://www.github.com/tauri-apps/tauri/commit/30f5a1553d3c0ce460c9006764200a9210915a44) ([#12366](https://www.github.com/tauri-apps/tauri/pull/12366) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Added `traffic_light_position` window builder method to set the traffic light buttons position on macOS. +- [`cedb24d49`](https://www.github.com/tauri-apps/tauri/commit/cedb24d494b84111daa3206c05196c8b89f1e994) ([#12665](https://www.github.com/tauri-apps/tauri/pull/12665) by [@charrondev](https://www.github.com/tauri-apps/tauri/../../charrondev)) Added `WebviewDispatch::cookies()` and `WebviewDispatch::cookies_for_url()`. + +### Dependencies + +- Upgraded to `tauri-utils@2.3.0` + +## \[2.4.0] + +### Enhancements + +- [`a2d36b8c3`](https://www.github.com/tauri-apps/tauri/commit/a2d36b8c34a8dcfc6736797ca5cd4665faf75e7e) ([#12181](https://www.github.com/tauri-apps/tauri/pull/12181) by [@bastiankistner](https://www.github.com/tauri-apps/tauri/../../bastiankistner)) Add an option to change the default background throttling policy (currently for WebKit only). +- [`d6520a21c`](https://www.github.com/tauri-apps/tauri/commit/d6520a21ce02c3e2be2955999946c2cb7bdb07aa) ([#12541](https://www.github.com/tauri-apps/tauri/pull/12541) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Updated `wry` to 0.50, `windows` to 0.60, `webview2-com` to 0.36, and `objc2` to 0.6. This can be a **breaking change** if you use the `with_webview` API! + +### Dependencies + +- Upgraded to `tauri-utils@2.2.0` + +## \[2.3.0] + +### New Features + +- [`18bd639f6`](https://www.github.com/tauri-apps/tauri/commit/18bd639f6e22c0188aa219739f367b5bf5ab0398) ([#11798](https://www.github.com/tauri-apps/tauri/pull/11798) by [@lars-berger](https://www.github.com/tauri-apps/tauri/../../lars-berger)) Add `WebviewWindowBuilder/WebviewBuilder::data_store_identifier` on macOS. +- [`dc4d79477`](https://www.github.com/tauri-apps/tauri/commit/dc4d79477665bc3bfefb4048772414cf5d78e3df) ([#11628](https://www.github.com/tauri-apps/tauri/pull/11628) by [@SpikeHD](https://www.github.com/tauri-apps/tauri/../../SpikeHD)) Add `WebviewWindowBuilder/WebviewBuilder::extensions_path` on Linux and Windows. +- [`020ea0556`](https://www.github.com/tauri-apps/tauri/commit/020ea05561348dcd6d2a7df358f8a5190f661ba2) ([#11661](https://www.github.com/tauri-apps/tauri/pull/11661) by [@ahqsoftwares](https://www.github.com/tauri-apps/tauri/../../ahqsoftwares)) Add badging APIs: + + - `Window/WebviewWindow::set_badge_count` for Linux, macOS and IOS. + - `Window/WebviewWindow::set_overlay_icon` for Windows Only. + - `Window/WebviewWindow::set_badge_label`for macOS Only. + +### Dependencies + +- Upgraded to `tauri-utils@2.1.1` + +## \[2.2.0] + +### New Features + +- [`4d545ab3c`](https://www.github.com/tauri-apps/tauri/commit/4d545ab3ca228c8a21b966b709f84a0da2864479) ([#11486](https://www.github.com/tauri-apps/tauri/pull/11486) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Added `Window::set_background_color` and `WindowBuilder::background_color`. +- [`f37e97d41`](https://www.github.com/tauri-apps/tauri/commit/f37e97d410c4a219e99f97692da05ca9d8e0ba3a) ([#11477](https://www.github.com/tauri-apps/tauri/pull/11477) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `WebviewWindowBuilder/WebviewBuilder::use_https_scheme` to choose whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android +- [`cbc095ec5`](https://www.github.com/tauri-apps/tauri/commit/cbc095ec5fe7de29b5c9265576d4e071ec159c1c) ([#11451](https://www.github.com/tauri-apps/tauri/pull/11451) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `WebviewWindowBuilder::devtools` and `WebviewBuilder::devtools` to enable or disable devtools for a specific webview. +- [`2a75c64b5`](https://www.github.com/tauri-apps/tauri/commit/2a75c64b5431284e7340e8743d4ea56a62c75466) ([#11469](https://www.github.com/tauri-apps/tauri/pull/11469) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Added `WindowBuilder/WebviewWindowBuilder::window_classname` method to specify the name of the window class on Windows. + +### Bug Fixes + +- [`129414faa`](https://www.github.com/tauri-apps/tauri/commit/129414faa4e027c9035d56614682cacc0335a6a0) ([#11569](https://www.github.com/tauri-apps/tauri/pull/11569) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Fix webview not focused by default. + +### Dependencies + +- Upgraded to `tauri-utils@2.1.0` + +## \[2.1.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.2` + +## \[2.1.0] + +### Bug Fixes + +- [`2d087ee4b`](https://www.github.com/tauri-apps/tauri/commit/2d087ee4b7d3e8849933f81284e4f5ed1aaa6455) ([#11268](https://www.github.com/tauri-apps/tauri/pull/11268) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) On Linux, fix commands, that use `Webview` or `WebviewWindow` as an argument, receiving an incorrect webview when using multi webviews. +- [`2d087ee4b`](https://www.github.com/tauri-apps/tauri/commit/2d087ee4b7d3e8849933f81284e4f5ed1aaa6455) ([#11268](https://www.github.com/tauri-apps/tauri/pull/11268) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) On Linux, fix events only emitted to first webview only when using multi webviews. +- [`2d087ee4b`](https://www.github.com/tauri-apps/tauri/commit/2d087ee4b7d3e8849933f81284e4f5ed1aaa6455) ([#11268](https://www.github.com/tauri-apps/tauri/pull/11268) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) On Linux, fix custom protocols receiving an incorrect webview label when using multi webviews + +## \[2.0.1] + +### What's Changed + +- [`0ab2b3306`](https://www.github.com/tauri-apps/tauri/commit/0ab2b330644b6419f6cee1d5377bfb5cdda2ccf9) ([#11205](https://www.github.com/tauri-apps/tauri/pull/11205) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.1` + +## \[2.0.0] + +### What's Changed + +- [`382ed482b`](https://www.github.com/tauri-apps/tauri/commit/382ed482bd08157c39e62f9a0aaad8802f1092cb) Bump MSRV to 1.78. +- [`637285790`](https://www.github.com/tauri-apps/tauri/commit/6372857905ae9c0aedb7f482ddf6cf9f9836c9f2) Promote to v2 stable! + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0` + +## \[2.0.0-rc.13] + +### New Features + +- [`a247170e1`](https://www.github.com/tauri-apps/tauri/commit/a247170e1f620a9b012274b11cfe51e90327d6e9) ([#11056](https://www.github.com/tauri-apps/tauri/pull/11056) by [@SpikeHD](https://www.github.com/tauri-apps/tauri/../../SpikeHD)) Expose the ability to enabled browser extensions in WebView2 on Windows. +- [`9014a3f17`](https://www.github.com/tauri-apps/tauri/commit/9014a3f1765ca406ea5c3e5224267a79c52cd53d) ([#11066](https://www.github.com/tauri-apps/tauri/pull/11066) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `WebviewWindow::clear_all_browsing_data` and `Webview::clear_all_browsing_data` to clear the webview browsing data. +- [`95df53a2e`](https://www.github.com/tauri-apps/tauri/commit/95df53a2ed96873cd35a4b14a5e312d07e4e3004) ([#11143](https://www.github.com/tauri-apps/tauri/pull/11143) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Add the ability to set theme dynamically using `Window::set_theme`, `App::set_theme` +- [`d9d2502b4`](https://www.github.com/tauri-apps/tauri/commit/d9d2502b41e39efde679e30c8955006e2ba9ea64) ([#11140](https://www.github.com/tauri-apps/tauri/pull/11140) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `WebviewDispatch::hide` and `WebviewDispatch::show` methods. +- [`de7414aab`](https://www.github.com/tauri-apps/tauri/commit/de7414aab935e45540594ea930eb60bae4dbc979) ([#11154](https://www.github.com/tauri-apps/tauri/pull/11154) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `Window::set_enabled` and `Window::is_enabled` methods + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.13` + +## \[2.0.0-rc.12] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.12` + +## \[2.0.0-rc.11] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.11` + +## \[2.0.0-rc.10] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.10` + +## \[2.0.0-rc.9] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.9` + +## \[2.0.0-rc.8] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.8` + +## \[2.0.0-rc.7] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.7` + +## \[2.0.0-rc.6] + +### What's Changed + +- [`f4d5241b3`](https://www.github.com/tauri-apps/tauri/commit/f4d5241b377d0f7a1b58100ee19f7843384634ac) ([#10731](https://www.github.com/tauri-apps/tauri/pull/10731) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Update documentation icon path. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.6` + +## \[2.0.0-rc.5] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.5` + +## \[2.0.0-rc.4] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.4` + +## \[2.0.0-rc.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.3` +- [`d39c392b7`](https://www.github.com/tauri-apps/tauri/commit/d39c392b7cec746da423211f9c74632abe4b6af5) ([#10655](https://www.github.com/tauri-apps/tauri/pull/10655) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Update `tao` to 0.29 and `wry` to 0.42. + +## \[2.0.0-rc.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.2` + +## \[2.0.0-rc.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.1` + +## \[2.0.0-rc.0] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-rc.0` + +## \[2.0.0-beta.21] + +### What's Changed + +- [`da25f7353`](https://www.github.com/tauri-apps/tauri/commit/da25f7353070477ba969851e974379d7666d6806) ([#10242](https://www.github.com/tauri-apps/tauri/pull/10242) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `inner_size_constraints` method on `WindowBuilder` trait and `set_size_constraints` method on `WindowDispatch` trait. + +## \[2.0.0-beta.20] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.19` + +## \[2.0.0-beta.19] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.18` +- [`d4c908cfb`](https://www.github.com/tauri-apps/tauri/commit/d4c908cfb8c567abdaf99b85f65f482ea81967e5) ([#10048](https://www.github.com/tauri-apps/tauri/pull/10048)) Update `windows` crate to version `0.57` and `webview2-com` crate to version `0.31` + +## \[2.0.0-beta.18] + +### Enhancements + +- [`276c4b143`](https://www.github.com/tauri-apps/tauri/commit/276c4b14385e17cff15a2e5b57fd2a7cddef9f08)([#9832](https://www.github.com/tauri-apps/tauri/pull/9832)) Added `WindowBuilder::get_theme`. + +### What's Changed + +- [`9ac930380`](https://www.github.com/tauri-apps/tauri/commit/9ac930380a5df3fe700e68e75df8684d261ca292)([#9850](https://www.github.com/tauri-apps/tauri/pull/9850)) Emit `cargo:rustc-check-cfg` instruction so Cargo validates custom cfg attributes on Rust 1.80 (or nightly-2024-05-05). + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.17` + +## \[2.0.0-beta.17] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.16` + +## \[2.0.0-beta.16] + +### New Features + +- [`78839b6d2`](https://www.github.com/tauri-apps/tauri/commit/78839b6d2f1005a5e6e1a54b0305136bae0c3a7c)([#4865](https://www.github.com/tauri-apps/tauri/pull/4865)) Add `RunEvent::Reopen` for handle click on dock icon on macOS. + +### What's Changed + +- [`783ef0f2d`](https://www.github.com/tauri-apps/tauri/commit/783ef0f2d331f520fa827c3112f36c0b519b9292)([#9647](https://www.github.com/tauri-apps/tauri/pull/9647)) Changed `WebviewDispatch::url` getter to return a result. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.15` + +## \[2.0.0-beta.15] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.14` + +## \[2.0.0-beta.14] + +### New Features + +- [`477bb8cd4`](https://www.github.com/tauri-apps/tauri/commit/477bb8cd4ea88ade3f6c1f268ad1701a68150161)([#9297](https://www.github.com/tauri-apps/tauri/pull/9297)) Add `App/AppHandle/Window/Webview/WebviewWindow::cursor_position` getter to get the current cursor position. + +## \[2.0.0-beta.13] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.13` + +## \[2.0.0-beta.12] + +### New Features + +- [`58a7a552d`](https://www.github.com/tauri-apps/tauri/commit/58a7a552d739b77b71d61af11c53f7f2dc7a6e7e)([#9378](https://www.github.com/tauri-apps/tauri/pull/9378)) Added the `set_zoom` function to the webview API. +- [`58a7a552d`](https://www.github.com/tauri-apps/tauri/commit/58a7a552d739b77b71d61af11c53f7f2dc7a6e7e)([#9378](https://www.github.com/tauri-apps/tauri/pull/9378)) Add `zoom_hotkeys_enabled` to enable browser native zoom controls on creating webviews. +- [`4973d73a2`](https://www.github.com/tauri-apps/tauri/commit/4973d73a237dc5c60618c1011e202278e7a29b5c)([#9386](https://www.github.com/tauri-apps/tauri/pull/9386)) Provide a basic zoom hotkey polyfill for non-Windows platforms + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.12` + +## \[2.0.0-beta.11] + +### What's Changed + +- [`06833f4fa`](https://www.github.com/tauri-apps/tauri/commit/06833f4fa8e63ecc55fe3fc874a9e397e77a5709)([#9100](https://www.github.com/tauri-apps/tauri/pull/9100)) Updated `http` crate to `1.1` + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.11` + +### Breaking Changes + +- [`06833f4fa`](https://www.github.com/tauri-apps/tauri/commit/06833f4fa8e63ecc55fe3fc874a9e397e77a5709)([#9100](https://www.github.com/tauri-apps/tauri/pull/9100)) The IPC handler closure now receives a `http::Request` instead of a String representing the request body. +- [`06833f4fa`](https://www.github.com/tauri-apps/tauri/commit/06833f4fa8e63ecc55fe3fc874a9e397e77a5709)([#9100](https://www.github.com/tauri-apps/tauri/pull/9100)) Rename `FileDrop` to `DragDrop` on structs, enums and enum variants. Also renamed `file_drop` to `drag_drop` on fields and function names. +- [`06833f4fa`](https://www.github.com/tauri-apps/tauri/commit/06833f4fa8e63ecc55fe3fc874a9e397e77a5709)([#9100](https://www.github.com/tauri-apps/tauri/pull/9100)) Moved `window::dpi` module to the root of the crate. + +## \[2.0.0-beta.10] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.10` + +## \[2.0.0-beta.9] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.9` + +## \[2.0.0-beta.8] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.8` + +## \[2.0.0-beta.7] + +### New Features + +- [`46de49aaa`](https://www.github.com/tauri-apps/tauri/commit/46de49aaad4a148fafc31d591be0e2ed12256507)([#9059](https://www.github.com/tauri-apps/tauri/pull/9059)) Added `set_auto_resize` method for the webview. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.7` + +### Breaking Changes + +- [`d1e77acd8`](https://www.github.com/tauri-apps/tauri/commit/d1e77acd8dfdf554b90b542513a58a2de1ef2360)([#9011](https://www.github.com/tauri-apps/tauri/pull/9011)) Add a lifetime parameter for `Icon` type. Also changed `rgba` field to be `Cow<'a, [u8]>` + +## \[2.0.0-beta.6] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.6` + +## \[2.0.0-beta.5] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.5` + +## \[2.0.0-beta.4] + +### New Features + +- [`fdcaf935`](https://www.github.com/tauri-apps/tauri/commit/fdcaf935fa75ecfa2806939c4faad4fe9e880386)([#8939](https://www.github.com/tauri-apps/tauri/pull/8939)) Added the `reparent` function to the webview API. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.4` + +## \[2.0.0-beta.3] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.3` + +## \[2.0.0-beta.2] + +### What's Changed + +- [`16e550ec`](https://www.github.com/tauri-apps/tauri/commit/16e550ec1503765158cdc3bb2a20e70ec710e981)([#8844](https://www.github.com/tauri-apps/tauri/pull/8844)) Add `WebviewEvent`, `RunEvent::WebviewEvent` and `WebviewDispatch::on_webview_event`. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.2` + +### Breaking Changes + +- [`2f55bfec`](https://www.github.com/tauri-apps/tauri/commit/2f55bfecbf0244f3b5aa1ad7622182fca3fcdcbb)([#8795](https://www.github.com/tauri-apps/tauri/pull/8795)) Update raw-window-handle to 0.6. + +## \[2.0.0-beta.1] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.1` + +## \[2.0.0-beta.0] + +### New Features + +- [`af610232`](https://www.github.com/tauri-apps/tauri/commit/af6102327376884364b2075b468bdf08ee0d02aa)([#8710](https://www.github.com/tauri-apps/tauri/pull/8710)) Added `Window::destroy` to force close a window. +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) Add multiwebview support behind the `unstable` feature flag. See `WindowBuilder` and `WebviewBuilder` for more information. +- [`00e15675`](https://www.github.com/tauri-apps/tauri/commit/00e1567584721644797b587205187f9cbe4e5cd1)([#8708](https://www.github.com/tauri-apps/tauri/pull/8708)) Added `RuntimeHandle::request_exit` function. + +### Bug Fixes + +- [`95da1a27`](https://www.github.com/tauri-apps/tauri/commit/95da1a27476e01e06f6ce0335df8535b662dd9c4)([#8713](https://www.github.com/tauri-apps/tauri/pull/8713)) Fix calling `set_activation_policy` when the event loop is running. + +### What's Changed + +- [`9eaeb5a8`](https://www.github.com/tauri-apps/tauri/commit/9eaeb5a8cd95ae24b5e66205bdc2763cb7f965ce)([#8622](https://www.github.com/tauri-apps/tauri/pull/8622)) Added `WindowBuilder::transient_for` and Renamed `WindowBuilder::owner_window` to `WindowBuilder::owner` and `WindowBuilder::parent_window` to `WindowBuilder::parent`. +- [`7f033f6d`](https://www.github.com/tauri-apps/tauri/commit/7f033f6dcd54c69a4193765a5c1584755ba92c61)([#8537](https://www.github.com/tauri-apps/tauri/pull/8537)) Add `Window::start_resize_dragging` and `ResizeDirection` enum. +- [`6639a579`](https://www.github.com/tauri-apps/tauri/commit/6639a579c76d45210f33a72d37e21d4c5a9d334b)([#8441](https://www.github.com/tauri-apps/tauri/pull/8441)) Added the `WindowConfig::proxy_url` `WebviewBuilder::proxy_url() / WebviewWindowBuilder::proxy_url()` options when creating a webview. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-beta.0` + +### Breaking Changes + +- [`9eaeb5a8`](https://www.github.com/tauri-apps/tauri/commit/9eaeb5a8cd95ae24b5e66205bdc2763cb7f965ce)([#8622](https://www.github.com/tauri-apps/tauri/pull/8622)) Changed `WindowBuilder::with_config` to take a reference to a `WindowConfig` instead of an owned value. + +## \[1.0.0-alpha.8] + +### New Features + +- [`29ced5ce`](https://www.github.com/tauri-apps/tauri/commit/29ced5ceec40b2934094ade2db9a8855f294e1d1)([#8159](https://www.github.com/tauri-apps/tauri/pull/8159)) Added download event closure via `PendingWindow::download_handler`. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.13` + +## \[1.0.0-alpha.7] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.12` + +## \[1.0.0-alpha.6] + +### Dependencies + +- [\`\`](https://www.github.com/tauri-apps/tauri/commit/undefined) Update dependencies. + +## \[1.0.0-alpha.5] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.11` + +## \[1.0.0-alpha.4] + +### New Features + +- [`74d2464d`](https://www.github.com/tauri-apps/tauri/commit/74d2464d0e490fae341ad73bdf2964cf215fe6c5)([#8116](https://www.github.com/tauri-apps/tauri/pull/8116)) Added `on_page_load` hook for `PendingWindow`. + +### Enhancements + +- [`c6c59cf2`](https://www.github.com/tauri-apps/tauri/commit/c6c59cf2373258b626b00a26f4de4331765dd487) Pull changes from Tauri 1.5 release. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.10` +- [`9580df1d`](https://www.github.com/tauri-apps/tauri/commit/9580df1d7b027befb9e5f025ea2cbaf2dcc82c8e)([#8084](https://www.github.com/tauri-apps/tauri/pull/8084)) Upgrade `gtk` to 0.18. +- [`c7c2507d`](https://www.github.com/tauri-apps/tauri/commit/c7c2507da16a9beb71bf06745fe7ac1325ab7c2a)([#8035](https://www.github.com/tauri-apps/tauri/pull/8035)) Update `windows` to version `0.51` and `webview2-com` to version `0.27` + +## \[1.0.0-alpha.3] + +### New Features + +- [`c085adda`](https://www.github.com/tauri-apps/tauri/commit/c085addab58ba851398373c6fd13f9cb026d71e8)([#8009](https://www.github.com/tauri-apps/tauri/pull/8009)) Added `set_progress_bar` to `Window`. +- [`c1ec0f15`](https://www.github.com/tauri-apps/tauri/commit/c1ec0f155118527361dd5645d920becbc8afd569)([#7933](https://www.github.com/tauri-apps/tauri/pull/7933)) Added `Window::set_always_on_bottom` and the `always_on_bottom` option when creating a window. +- [`880266a7`](https://www.github.com/tauri-apps/tauri/commit/880266a7f697e1fe58d685de3bb6836ce5251e92)([#8031](https://www.github.com/tauri-apps/tauri/pull/8031)) Bump the MSRV to 1.70. + +### Enhancements + +- [`46dcb941`](https://www.github.com/tauri-apps/tauri/commit/46dcb94110ac16d0d4328fa149bb86975b658f59)([#8006](https://www.github.com/tauri-apps/tauri/pull/8006)) Include mobile on docs.rs targets. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.9` + +### Breaking Changes + +- [`2558fab8`](https://www.github.com/tauri-apps/tauri/commit/2558fab861006936296e8511e43ccd69a38f61b0)([#7939](https://www.github.com/tauri-apps/tauri/pull/7939)) Added `WindowEventId` type and Changed `Dispatch::on_window_event` return type from `Uuid` to `WindowEventId` + +## \[1.0.0-alpha.2] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.8` + +## \[1.0.0-alpha.1] + +### Enhancements + +- [`0d63732b`](https://www.github.com/tauri-apps/tauri/commit/0d63732b962e71b98430f8d7b34ea5b59a2e8bb4)([#7754](https://www.github.com/tauri-apps/tauri/pull/7754)) Changed custom protocol closure type to enable asynchronous usage. + +### What's Changed + +- [`6177150b`](https://www.github.com/tauri-apps/tauri/commit/6177150b6f83b52ca359d6e20f7e540f7554e4eb)([#7601](https://www.github.com/tauri-apps/tauri/pull/7601)) Changed `FileDropEvent` to include drop and hover position. + +### Breaking Changes + +- [`0d63732b`](https://www.github.com/tauri-apps/tauri/commit/0d63732b962e71b98430f8d7b34ea5b59a2e8bb4)([#7754](https://www.github.com/tauri-apps/tauri/pull/7754)) `tauri-runtime` no longer implements its own HTTP types and relies on the `http` crate instead. + +## \[1.0.0-alpha.0] + +### New Features + +- [`4db363a0`](https://www.github.com/tauri-apps/tauri/commit/4db363a03c182349f8491f46ced258d84723b11f)([#6589](https://www.github.com/tauri-apps/tauri/pull/6589)) Added `visible_on_all_workspaces` configuration option to `WindowBuilder`, `Window`, and `WindowConfig`. +- [`84c41597`](https://www.github.com/tauri-apps/tauri/commit/84c4159754b2e59244211ed9e1fc702d851a0562)([#6394](https://www.github.com/tauri-apps/tauri/pull/6394)) Added `primary_monitor` and `available_monitors` to `Runtime` and `RuntimeHandle`. +- [`2a000e15`](https://www.github.com/tauri-apps/tauri/commit/2a000e150d02dff28c8b20ad097b29e209160045)([#7235](https://www.github.com/tauri-apps/tauri/pull/7235)) Added `navigate` function to `Dispatch` trait. +- [`3b98141a`](https://www.github.com/tauri-apps/tauri/commit/3b98141aa26f74c641a4090874247b97079bd58a)([#3736](https://www.github.com/tauri-apps/tauri/pull/3736)) Added the `Opened` variant to `RunEvent`. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.7` + +### Breaking Changes + +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) `Dispatch::create_window`, `Runtime::create_window` and `RuntimeHandle::create_window` has been changed to accept a 3rd parameter which is a closure that takes `RawWindow` and to be executed right after the window is created and before the webview is added to the window. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) System tray and menu related APIs and structs have all been removed and are now implemented in tauri outside of the runtime-space. +- [`3a2c3e74`](https://www.github.com/tauri-apps/tauri/commit/3a2c3e74710bef9a14932dce74c351cca6215429)([#7306](https://www.github.com/tauri-apps/tauri/pull/7306)) The `PendingWindow#navigation_handler` closure now receives a `&Url` argument instead of `Url`. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) `Runtime::new` and `Runtime::new_any_thread` now accept a `RuntimeInitArgs`. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) Removed `system-tray` feature flag + +## \[0.13.0-alpha.6] + +### New Features + +- [`e0f0dce2`](https://www.github.com/tauri-apps/tauri/commit/e0f0dce220730e2822fc202463aedf0166145de7)([#6442](https://www.github.com/tauri-apps/tauri/pull/6442)) Added the `window_effects` option when creating a window and `Window::set_effects` to change it at runtime. + +## \[0.13.0-alpha.5] + +- [`39f1b04f`](https://www.github.com/tauri-apps/tauri/commit/39f1b04f7be4966488484829cd54c8ce72a04200)([#6943](https://www.github.com/tauri-apps/tauri/pull/6943)) Moved the `event` JS APIs to a plugin. +- [`3188f376`](https://www.github.com/tauri-apps/tauri/commit/3188f3764978c6d1452ee31d5a91469691e95094)([#6883](https://www.github.com/tauri-apps/tauri/pull/6883)) Bump the MSRV to 1.65. +- [`cebd7526`](https://www.github.com/tauri-apps/tauri/commit/cebd75261ac71b98976314a450cb292eeeec1515)([#6728](https://www.github.com/tauri-apps/tauri/pull/6728)) Moved the `clipboard` feature to its own plugin in the plugins-workspace repository. +- [`3f17ee82`](https://www.github.com/tauri-apps/tauri/commit/3f17ee82f6ff21108806edb7b00500b8512b8dc7)([#6737](https://www.github.com/tauri-apps/tauri/pull/6737)) Moved the `global-shortcut` feature to its own plugin in the plugins-workspace repository. + +## \[0.13.0-alpha.4] + +- Added `android` configuration object under `tauri > bundle`. + - Bumped due to a bump in tauri-utils. + - [db4c9dc6](https://www.github.com/tauri-apps/tauri/commit/db4c9dc655e07ee2184fe04571f500f7910890cd) feat(core): add option to configure Android's minimum SDK version ([#6651](https://www.github.com/tauri-apps/tauri/pull/6651)) on 2023-04-07 + +## \[0.13.0-alpha.3] + +- Pull changes from Tauri 1.3 release. + - [](https://www.github.com/tauri-apps/tauri/commit/undefined) on undefined + +## \[0.13.0-alpha.2] + +- Add `find_class`, `run_on_android_context` on `RuntimeHandle`. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 +- Added the `shadow` option when creating a window and `Window::set_shadow`. + - [a81750d7](https://www.github.com/tauri-apps/tauri/commit/a81750d779bc72f0fdb7de90b7fbddfd8049b328) feat(core): add shadow APIs ([#6206](https://www.github.com/tauri-apps/tauri/pull/6206)) on 2023-02-08 +- Implemented `with_webview` on Android and iOS. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 + +## \[0.13.0-alpha.1] + +- Update gtk to 0.16. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Bump the MSRV to 1.64. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 + +## \[0.13.0-alpha.0] + +- Parse `android` and `ios` Tauri configuration files. + - Bumped due to a bump in tauri-utils. + - [b3a3afc7](https://www.github.com/tauri-apps/tauri/commit/b3a3afc7de8de4021d73559288f5192732a706cf) feat(core): detect android and ios platform configuration files ([#4997](https://www.github.com/tauri-apps/tauri/pull/4997)) on 2022-08-22 +- First mobile alpha release! + - Bumped due to a bump in tauri-utils. + - [fa3a1098](https://www.github.com/tauri-apps/tauri/commit/fa3a10988a03aed1b66fb17d893b1a9adb90f7cd) feat(ci): prepare 2.0.0-alpha.0 ([#5786](https://www.github.com/tauri-apps/tauri/pull/5786)) on 2022-12-08 + +## \[0.14.2] + +### Dependencies + +- Upgraded to `tauri-utils@1.5.2` + +## \[0.14.1] + +### Enhancements + +- [`9aa34ada`](https://www.github.com/tauri-apps/tauri/commit/9aa34ada5769dbefa7dfe5f7a6288b3d20b294e4)([#7645](https://www.github.com/tauri-apps/tauri/pull/7645)) Add setting to switch to `http://.localhost/` for custom protocols on Windows. + +### Dependencies + +- Upgraded to `tauri-utils@1.5.0` + +## \[0.14.0] + +### New Features + +- [`c4d6fb4b`](https://www.github.com/tauri-apps/tauri/commit/c4d6fb4b1ea8acf02707a9fe5dcab47c1c5bae7b)([#2353](https://www.github.com/tauri-apps/tauri/pull/2353)) Added the `maximizable`, `minimizable` and `closable` methods to `WindowBuilder`. +- [`c4d6fb4b`](https://www.github.com/tauri-apps/tauri/commit/c4d6fb4b1ea8acf02707a9fe5dcab47c1c5bae7b)([#2353](https://www.github.com/tauri-apps/tauri/pull/2353)) Added `set_maximizable`, `set_minimizable`, `set_closable`, `is_maximizable`, `is_minimizable` and `is_closable` methods to the `Dispatch` trait. +- [`000104bc`](https://www.github.com/tauri-apps/tauri/commit/000104bc3bc0c9ff3d20558ab9cf2080f126e9e0)([#6472](https://www.github.com/tauri-apps/tauri/pull/6472)) Add `Window::is_focused` getter. + +### Enhancements + +- [`d2710e9d`](https://www.github.com/tauri-apps/tauri/commit/d2710e9d2e8fd93975ef6494512370faa8cb3b7e)([#6944](https://www.github.com/tauri-apps/tauri/pull/6944)) Unpin `time`, `ignore`, and `winnow` crate versions. Developers now have to pin crates if needed themselves. A list of crates that need pinning to adhere to Tauri's MSRV will be visible in Tauri's GitHub workflow: https://github.com/tauri-apps/tauri/blob/dev/.github/workflows/test-core.yml#L85. + +### Bug Fixes + +- [`2b487c94`](https://www.github.com/tauri-apps/tauri/commit/2b487c946737352187d7e042dd6142873e62a4ca)([#7012](https://www.github.com/tauri-apps/tauri/pull/7012)) Fixes typo in `CursorIcon` deserialization of the `ZoomIn` variant. + +### What's Changed + +- [`076e1a81`](https://www.github.com/tauri-apps/tauri/commit/076e1a81a50468e3dfb34ae9ca7e77c5e1758daa)([#7119](https://www.github.com/tauri-apps/tauri/pull/7119)) Use `u32` instead of `u64` for js event listener ids +- [`ff5e4dbb`](https://www.github.com/tauri-apps/tauri/commit/ff5e4dbbb01bf3fc9c5143df732c75eef6fd98cb)([#6794](https://www.github.com/tauri-apps/tauri/pull/6794)) impl `From<&WindowConfig>` for `WebviewAttributes`. + +## \[0.13.0] + +- Added the `additional_browser_args` option when creating a window. + - [3dc38b15](https://www.github.com/tauri-apps/tauri/commit/3dc38b150ea8c59c8ba67fd586f921016928f47c) feat(core): expose additional_browser_args to window config (fix: [#5757](https://www.github.com/tauri-apps/tauri/pull/5757)) ([#5799](https://www.github.com/tauri-apps/tauri/pull/5799)) on 2022-12-14 +- Added the `content_protected` option when creating a window and `Window::set_content_protected` to change it at runtime. + - [4ab5545b](https://www.github.com/tauri-apps/tauri/commit/4ab5545b7a831c549f3c65e74de487ede3ab7ce5) feat: add content protection api, closes [#5132](https://www.github.com/tauri-apps/tauri/pull/5132) ([#5513](https://www.github.com/tauri-apps/tauri/pull/5513)) on 2022-12-13 +- Added `Builder::device_event_filter` and `App::set_device_event_filter` methods. + - [73fd60ee](https://www.github.com/tauri-apps/tauri/commit/73fd60eef2b60f5dc84525ef9c315f4d80c4414f) expose set_device_event_filter in tauri ([#5562](https://www.github.com/tauri-apps/tauri/pull/5562)) on 2022-12-13 +- Add `is_minimized()` window method. + - [62144ef3](https://www.github.com/tauri-apps/tauri/commit/62144ef3be63b237869e511826edfb938e2c7174) feat: add is_minimized (fix [#3878](https://www.github.com/tauri-apps/tauri/pull/3878)) ([#5618](https://www.github.com/tauri-apps/tauri/pull/5618)) on 2022-12-13 +- Bump minimum supported Rust version to 1.60. + - [5fdc616d](https://www.github.com/tauri-apps/tauri/commit/5fdc616df9bea633810dcb814ac615911d77222c) feat: Use the zbus-backed of notify-rust ([#6332](https://www.github.com/tauri-apps/tauri/pull/6332)) on 2023-03-31 +- Pin raw-window-handle to 0.5.0 to keep MSRV. + - [c46c09f3](https://www.github.com/tauri-apps/tauri/commit/c46c09f31d9f5169ca8a7e62406a9ea170e3a5c5) fix(deps): pin raw-window-handle to 0.5.0 ([#6480](https://www.github.com/tauri-apps/tauri/pull/6480)) on 2023-03-17 +- Added `navigation_handler` field on `PendingWindow`. + - [3f35b452](https://www.github.com/tauri-apps/tauri/commit/3f35b452637ef1c794a423f1eda62a15d2ddaf42) Expose wry navigation_handler via WindowBuilder closes [#4080](https://www.github.com/tauri-apps/tauri/pull/4080) ([#5686](https://www.github.com/tauri-apps/tauri/pull/5686)) on 2022-12-27 +- Add `title` getter on window. + - [233e43b0](https://www.github.com/tauri-apps/tauri/commit/233e43b0c34fada1ca025378533a0b76931a6540) feat: add `title` getter on window, closes [#5023](https://www.github.com/tauri-apps/tauri/pull/5023) ([#5515](https://www.github.com/tauri-apps/tauri/pull/5515)) on 2022-12-13 +- Added `TrayHandle::set_tooltip` and `SystemTray::with_tooltip`. + - [2265e097](https://www.github.com/tauri-apps/tauri/commit/2265e09718f6ebfeb1d200f11e1e1e069075af6e) feat(windows): implement `with_tooltip` ([#5938](https://www.github.com/tauri-apps/tauri/pull/5938)) on 2023-01-01 +- Added window's `url()` getter. + - [d17027e1](https://www.github.com/tauri-apps/tauri/commit/d17027e1a0db3e8c5ae81fc4f472c5918fbce611) feat: expose url method ([#5914](https://www.github.com/tauri-apps/tauri/pull/5914)) on 2022-12-26 +- On Windows, change webview theme based on Window theme for more accurate `prefers-color-scheme` support. + - [7a8d570d](https://www.github.com/tauri-apps/tauri/commit/7a8d570db72667367eb24b75ddc5dd07a968f7c0) fix: sync webview theme with window theme on Windows, closes [#5802](https://www.github.com/tauri-apps/tauri/pull/5802) ([#5874](https://www.github.com/tauri-apps/tauri/pull/5874)) on 2022-12-27 + +## \[0.12.2] + +- Block remote URLs from accessing the IPC. + - [9c0593c33](https://www.github.com/tauri-apps/tauri/commit/9c0593c33af52cd9e00ec784d15f63efebdf039c) feat(core): block remote URLs from accessing the IPC on 2023-04-12 + +## \[0.12.1] + +- Fix `allowlist > app > show/hide` always disabled when `allowlist > app > all: false`. + - Bumped due to a bump in tauri-utils. + - [bb251087](https://www.github.com/tauri-apps/tauri/commit/bb2510876d0bdff736d36bf3a465cdbe4ad2b90c) fix(core): extend allowlist with `app`'s allowlist, closes [#5650](https://www.github.com/tauri-apps/tauri/pull/5650) ([#5652](https://www.github.com/tauri-apps/tauri/pull/5652)) on 2022-11-18 + +## \[0.12.0] + +- Readd the option to create an unfocused window via the `focused` method. The `focus` function has been deprecated. + - [4036e15f](https://www.github.com/tauri-apps/tauri/commit/4036e15f5af933bdc0d0913508b5103958afc143) feat(core): reimplement window initial focus flag, closes [#5120](https://www.github.com/tauri-apps/tauri/pull/5120) ([#5338](https://www.github.com/tauri-apps/tauri/pull/5338)) on 2022-10-08 +- Added `Runtime::show()`, `RuntimeHandle::show()`, `Runtime::hide()`, `RuntimeHandle::hide()` for hiding/showing the entire application on macOS. + - [39bf895b](https://www.github.com/tauri-apps/tauri/commit/39bf895b73ec6b53f5758815396ba85dda6b9c67) feat(macOS): Add application `show` and `hide` methods ([#3689](https://www.github.com/tauri-apps/tauri/pull/3689)) on 2022-10-03 +- - [7d9aa398](https://www.github.com/tauri-apps/tauri/commit/7d9aa3987efce2d697179ffc33646d086c68030c) feat: bump MSRV to 1.59 ([#5296](https://www.github.com/tauri-apps/tauri/pull/5296)) on 2022-09-28 +- Added `tabbing_identifier` to the window builder on macOS. + - [4137ab44](https://www.github.com/tauri-apps/tauri/commit/4137ab44a81d739556cbc7583485887e78952bf1) feat(macos): add `tabbing_identifier` option, closes [#2804](https://www.github.com/tauri-apps/tauri/pull/2804), [#3912](https://www.github.com/tauri-apps/tauri/pull/3912) ([#5399](https://www.github.com/tauri-apps/tauri/pull/5399)) on 2022-10-19 +- Added methods to set the system tray title on macOS. + - [8f1ace77](https://www.github.com/tauri-apps/tauri/commit/8f1ace77956ac3477826ceb059a191e55b3fff93) feat: expose `set_title` for MacOS tray ([#5182](https://www.github.com/tauri-apps/tauri/pull/5182)) on 2022-09-30 +- Added the `user_agent` option when creating a window. + - [a6c94119](https://www.github.com/tauri-apps/tauri/commit/a6c94119d8545d509723b147c273ca5edfe3729f) feat(core): expose user_agent to window config ([#5317](https://www.github.com/tauri-apps/tauri/pull/5317)) on 2022-10-02 + +## \[0.11.2] + +- Block remote URLs from accessing the IPC. + - [58ea0b452](https://www.github.com/tauri-apps/tauri/commit/58ea0b45268dbd46cbac0ebb0887353d057ca767) feat(core): block remote URLs from accessing the IPC on 2023-04-12 + +## \[0.11.1] + +- Add missing allowlist config for `set_cursor_grab`, `set_cursor_visible`, `set_cursor_icon` and `set_cursor_position` APIs. + - Bumped due to a bump in tauri-utils. + - [c764408d](https://www.github.com/tauri-apps/tauri/commit/c764408da7fae123edd41115bda42fa75a4731d2) fix: Add missing allowlist config for cursor apis, closes [#5207](https://www.github.com/tauri-apps/tauri/pull/5207) ([#5211](https://www.github.com/tauri-apps/tauri/pull/5211)) on 2022-09-16 + +## \[0.11.0] + +- Added APIs to create a system tray at runtime. + - [4d063ae9](https://www.github.com/tauri-apps/tauri/commit/4d063ae9ee9538cd6fa5e01b80070c6edf8eaeb9) feat(core): create system tray at runtime, closes [#2278](https://www.github.com/tauri-apps/tauri/pull/2278) ([#4862](https://www.github.com/tauri-apps/tauri/pull/4862)) on 2022-08-09 +- Update windows to 0.39.0 and webview2-com to 0.19.1. + - [e6d9b670](https://www.github.com/tauri-apps/tauri/commit/e6d9b670b0b314ed667b0e164f2c8d27048e678f) refactor: remove unneeded focus code ([#5065](https://www.github.com/tauri-apps/tauri/pull/5065)) on 2022-09-03 + +## \[0.10.3] + +- Block remote URLs from accessing the IPC. + - [fa90214b0](https://www.github.com/tauri-apps/tauri/commit/fa90214b052b1a5d38d54fbf1ca422b4c37cfd1f) feat(core): block remote URLs from accessing the IPC on 2023-04-12 + +## \[0.10.2] + +- Added option to disable tray menu on left click on macOS. + - [f8a3becb](https://www.github.com/tauri-apps/tauri/commit/f8a3becb287942db7f7b551b5db6aeb5a2e939ee) feat(core): add option to disable tray menu on left click, closes [#4584](https://www.github.com/tauri-apps/tauri/pull/4584) ([#4587](https://www.github.com/tauri-apps/tauri/pull/4587)) on 2022-07-05 + +## \[0.10.1] + +- Expose `platform::windows_version` function. + - Bumped due to a bump in tauri-utils. + - [bf764e83](https://www.github.com/tauri-apps/tauri/commit/bf764e83e01e7443e6cc54572001e1c98c357465) feat(utils): expose `windows_version` function ([#4534](https://www.github.com/tauri-apps/tauri/pull/4534)) on 2022-06-30 + +## \[0.10.0] + +- Added `fn new` constructors for `PhysicalSize`, `LogicalSize`, `PhysicalPosition` and `LogicalPosition` and missing conversion methods. + - [c7d13a1c](https://www.github.com/tauri-apps/tauri/commit/c7d13a1c60cdbe0c42834ea059321d7a3a7f01a0) feat(core): add missing methods to the dpi module ([#4393](https://www.github.com/tauri-apps/tauri/pull/4393)) on 2022-06-19 +- Implement `raw_window_handle::HasRawWindowHandle` on Linux. + - [3efbc67f](https://www.github.com/tauri-apps/tauri/commit/3efbc67f7469ce65a2d9ea4ff2b60b51d2a36aa5) feat: implement `raw_window_handle` on Linux ([#4469](https://www.github.com/tauri-apps/tauri/pull/4469)) on 2022-06-26 +- Removed the `hwnd` and `ns_window` functions from `Dispatch` in favor of `raw_window_handle`. + - [3efbc67f](https://www.github.com/tauri-apps/tauri/commit/3efbc67f7469ce65a2d9ea4ff2b60b51d2a36aa5) feat: implement `raw_window_handle` on Linux ([#4469](https://www.github.com/tauri-apps/tauri/pull/4469)) on 2022-06-26 +- The theme API is now implemented on macOS 10.14+. + - [6d94ce42](https://www.github.com/tauri-apps/tauri/commit/6d94ce42353204a02fe9c82ed397d349439f75ef) feat(core): theme is now implemented on macOS ([#4380](https://www.github.com/tauri-apps/tauri/pull/4380)) on 2022-06-17 + +## \[0.9.0] + +- Upgrade to `stable`! + - Bumped due to a bump in tauri-utils. + - [f4bb30cc](https://www.github.com/tauri-apps/tauri/commit/f4bb30cc73d6ba9b9ef19ef004dc5e8e6bb901d3) feat(covector): prepare for v1 ([#4351](https://www.github.com/tauri-apps/tauri/pull/4351)) on 2022-06-15 + +## \[0.8.1] + +- Add `Menu::os_default` which will create a menu filled with default menu items and submenus. + - [4c4acc30](https://www.github.com/tauri-apps/tauri/commit/4c4acc3094218dd9cee0f1ad61810c979e0b41fa) feat: implement `Default` for `Menu`, closes [#2398](https://www.github.com/tauri-apps/tauri/pull/2398) ([#4291](https://www.github.com/tauri-apps/tauri/pull/4291)) on 2022-06-15 + +## \[0.8.0] + +- Removed `TrayIcon` and renamed `WindowIcon` to `Icon`, a shared type for both icons. + - [4ce8e228](https://www.github.com/tauri-apps/tauri/commit/4ce8e228134cd3f22973b74ef26ca0d165fbbbd9) refactor(core): use `Icon` for tray icons ([#4342](https://www.github.com/tauri-apps/tauri/pull/4342)) on 2022-06-14 + +## \[0.7.0] + +- Added a config flag to bundle the media framework used by webkit2gtk `tauri.conf.json > tauri > bundle > appimage > bundleMediaFramework`. + - Bumped due to a bump in tauri-utils. + - [d335fae9](https://www.github.com/tauri-apps/tauri/commit/d335fae92cdcbb0ee18aad4e54558914afa3e778) feat(bundler): bundle additional gstreamer files, closes [#4092](https://www.github.com/tauri-apps/tauri/pull/4092) ([#4271](https://www.github.com/tauri-apps/tauri/pull/4271)) on 2022-06-10 + +## \[0.6.0] + +- Update `windows-rs` to `0.37.0`, which requires Rust 1.61.0+. + - [2326be39](https://www.github.com/tauri-apps/tauri/commit/2326be39821890cdd4de76e7029a531424dcb26f) feat(core): update windows-rs to 0.37.0 ([#4199](https://www.github.com/tauri-apps/tauri/pull/4199)) on 2022-05-24 + +## \[0.5.1] + +- Fix `.mjs` not being recognised as a file extension for JavaScript files (`text/javascript`). + - [45c45253](https://www.github.com/tauri-apps/tauri/commit/45c45253866ce0de317a6a547af3ea0434d4bcac) fix: add mjs mime type (fix: [#4098](https://www.github.com/tauri-apps/tauri/pull/4098)) ([#4108](https://www.github.com/tauri-apps/tauri/pull/4108)) on 2022-05-13 + +## \[0.5.0] + +- Expose methods to access the underlying native handles of the webview. + - [c82b4761](https://www.github.com/tauri-apps/tauri/commit/c82b4761e1660592472dc55308ad69d9efc5855b) feat(core): expose `with_webview` API to access the platform webview ([#4058](https://www.github.com/tauri-apps/tauri/pull/4058)) on 2022-05-04 + +## \[0.4.0] + +- The `AboutMetadata` string setters now take `impl Into`. + - [b14aa896](https://www.github.com/tauri-apps/tauri/commit/b14aa89673c3563522e5c04baf9630fa1c4739b0) feat(core): improve `AboutMetadata` setters on 2022-03-29 +- \**Breaking change::* Added the `clipboard` Cargo feature. + - [24e4ff20](https://www.github.com/tauri-apps/tauri/commit/24e4ff208ee0fe1a4cc5b10667ea0922ac63dfb5) refactor(core): add clipboard Cargo feature, enhancing binary size ([#3957](https://www.github.com/tauri-apps/tauri/pull/3957)) on 2022-04-24 +- Expose Window cursor APIs `set_cursor_grab`, `set_cursor_visible`, `set_cursor_icon` and `set_cursor_position`. + - [c54ddfe9](https://www.github.com/tauri-apps/tauri/commit/c54ddfe9338e7eb90b4d5b02dfde687d432d5bc1) feat: expose window cursor APIs, closes [#3888](https://www.github.com/tauri-apps/tauri/pull/3888) [#3890](https://www.github.com/tauri-apps/tauri/pull/3890) ([#3935](https://www.github.com/tauri-apps/tauri/pull/3935)) on 2022-04-21 +- \**Breaking change::* Added the `global-shortcut` Cargo feature. + - [e11878bc](https://www.github.com/tauri-apps/tauri/commit/e11878bcf7174b261a1fa146fc7d564d12e6312a) refactor(core): add global-shortcut Cargo feature, enhancing binary size ([#3956](https://www.github.com/tauri-apps/tauri/pull/3956)) on 2022-04-24 +- Added `WindowEvent::ThemeChanged(theme)`. + - [4cebcf6d](https://www.github.com/tauri-apps/tauri/commit/4cebcf6da7cad1953e0f01b426afac3b5ef1f81e) feat: expose theme APIs, closes [#3903](https://www.github.com/tauri-apps/tauri/pull/3903) ([#3937](https://www.github.com/tauri-apps/tauri/pull/3937)) on 2022-04-21 +- Added `theme` getter on `Window`. + - [4cebcf6d](https://www.github.com/tauri-apps/tauri/commit/4cebcf6da7cad1953e0f01b426afac3b5ef1f81e) feat: expose theme APIs, closes [#3903](https://www.github.com/tauri-apps/tauri/pull/3903) ([#3937](https://www.github.com/tauri-apps/tauri/pull/3937)) on 2022-04-21 +- Added `theme` setter to the WindowBuilder. + - [4cebcf6d](https://www.github.com/tauri-apps/tauri/commit/4cebcf6da7cad1953e0f01b426afac3b5ef1f81e) feat: expose theme APIs, closes [#3903](https://www.github.com/tauri-apps/tauri/pull/3903) ([#3937](https://www.github.com/tauri-apps/tauri/pull/3937)) on 2022-04-21 + +## \[0.3.4] + +- Added `close_devtools` and `is_devtools_open` APIs to the `Dispatch` trait. + - [e05d718a](https://www.github.com/tauri-apps/tauri/commit/e05d718a7b46476d1fe4817c169008080e84f959) feat(core): add hotkey to toggle devtools, closes [#3776](https://www.github.com/tauri-apps/tauri/pull/3776) ([#3791](https://www.github.com/tauri-apps/tauri/pull/3791)) on 2022-03-28 +- **Breaking change:** The `MenuItem::About` variant is now associated with a tuple value `(String, AboutMetadata)`. + - [5fb74332](https://www.github.com/tauri-apps/tauri/commit/5fb74332ab9210ac062d96b0e9afd1c942ee2911) chore(deps): update wry to 0.14, tao to 0.7 ([#3790](https://www.github.com/tauri-apps/tauri/pull/3790)) on 2022-03-28 +- Support window parenting on macOS + - [4e807a53](https://www.github.com/tauri-apps/tauri/commit/4e807a53e2d6d3f3cd5293d90013d5cdded5454e) Support window parenting on macOS, closes [#3751](https://www.github.com/tauri-apps/tauri/pull/3751) ([#3754](https://www.github.com/tauri-apps/tauri/pull/3754)) on 2022-03-23 +- The file drop event is now part of the `WindowEvent` enum instead of a having a dedicated handler. + - [07d1584c](https://www.github.com/tauri-apps/tauri/commit/07d1584cf06ea326aa45d8044bee1b77ecba5006) feat(core): add `WindowEvent::FileDrop`, closes [#3664](https://www.github.com/tauri-apps/tauri/pull/3664) ([#3686](https://www.github.com/tauri-apps/tauri/pull/3686)) on 2022-03-13 +- **Breaking change:** Use the dedicated `WindowEvent` enum on `RunEvent`. + - [edad9f4f](https://www.github.com/tauri-apps/tauri/commit/edad9f4f55dcc69a06cd9d6d5a5068c94ecb77dd) refactor(core): add `RunEvent::WindowEvent` ([#3793](https://www.github.com/tauri-apps/tauri/pull/3793)) on 2022-03-28 +- Added `create_proxy` to the `Runtime` and `RuntimeHandle` traits. + - [5d538ec2](https://www.github.com/tauri-apps/tauri/commit/5d538ec27c246274df4ff5b8057ff78b6364a43f) refactor(core): use the event loop proxy to send updater events ([#3687](https://www.github.com/tauri-apps/tauri/pull/3687)) on 2022-03-15 +- Force `Error` boxed errors to be `Sync`. + - [da1e8793](https://www.github.com/tauri-apps/tauri/commit/da1e879358895f7b190b1c1b20d23da23666a74b) feat(core): improve and cleanup the `Error` enum ([#3748](https://www.github.com/tauri-apps/tauri/pull/3748)) on 2022-03-22 +- **Breaking change:** Move the `FileDropEvent` struct to the `window` module. + - [07d1584c](https://www.github.com/tauri-apps/tauri/commit/07d1584cf06ea326aa45d8044bee1b77ecba5006) feat(core): add `WindowEvent::FileDrop`, closes [#3664](https://www.github.com/tauri-apps/tauri/pull/3664) ([#3686](https://www.github.com/tauri-apps/tauri/pull/3686)) on 2022-03-13 +- Allow specifying a user event type for the event loop message. + - [5d538ec2](https://www.github.com/tauri-apps/tauri/commit/5d538ec27c246274df4ff5b8057ff78b6364a43f) refactor(core): use the event loop proxy to send updater events ([#3687](https://www.github.com/tauri-apps/tauri/pull/3687)) on 2022-03-15 +- Added the `WindowEvent::FileDrop` variant. + - [07d1584c](https://www.github.com/tauri-apps/tauri/commit/07d1584cf06ea326aa45d8044bee1b77ecba5006) feat(core): add `WindowEvent::FileDrop`, closes [#3664](https://www.github.com/tauri-apps/tauri/pull/3664) ([#3686](https://www.github.com/tauri-apps/tauri/pull/3686)) on 2022-03-13 + +## \[0.3.3] + +- **Breaking change:** Move `ico` and `png` parsing behind `icon-ico` and `icon-png` Cargo features. + - [8c935872](https://www.github.com/tauri-apps/tauri/commit/8c9358725a17dcc2acaf4d10c3f654afdff586b0) refactor(core): move `png` and `ico` behind Cargo features ([#3588](https://www.github.com/tauri-apps/tauri/pull/3588)) on 2022-03-05 +- The `PendingWindow::new` and `PendingWindow::with_config` functions now return `Result` validating the window label. + - [64e00542](https://www.github.com/tauri-apps/tauri/commit/64e0054299c95f10ef5a1a9d3f914bbaeff3d73f) refactor(core): do not panic on invalid window labels,[#3544](https://www.github.com/tauri-apps/tauri/pull/3544) ([#3596](https://www.github.com/tauri-apps/tauri/pull/3596)) on 2022-03-03 + +## \[0.3.2] + +- Fix requirements for `RuntimeHandle`, `ClipboardManager`, `GlobalShortcutHandle` and `TrayHandle`. + - [84895a9c](https://www.github.com/tauri-apps/tauri/commit/84895a9cd270fc743e236d0f4d4cd6210b24a30f) fix(runtime): trait requirements ([#3489](https://www.github.com/tauri-apps/tauri/pull/3489)) on 2022-02-17 + +## \[0.3.1] + +- Change default value for the `freezePrototype` configuration to `false`. + - Bumped due to a bump in tauri-utils. + - [3a4c0160](https://www.github.com/tauri-apps/tauri/commit/3a4c01606184be762adee055ddac803de0d28527) fix(core): change default `freezePrototype` to false, closes [#3416](https://www.github.com/tauri-apps/tauri/pull/3416) [#3406](https://www.github.com/tauri-apps/tauri/pull/3406) ([#3423](https://www.github.com/tauri-apps/tauri/pull/3423)) on 2022-02-12 + +## \[0.3.0] + +- Replace `WindowBuilder`'s `has_menu` with `get_menu`. + - [ac37b56e](https://www.github.com/tauri-apps/tauri/commit/ac37b56ef43c9e97039967a5fd99f0d2dccb5b5a) fix(core): menu id map not reflecting the current window menu ([#2726](https://www.github.com/tauri-apps/tauri/pull/2726)) on 2021-10-08 +- The `run_return` API is now available on Linux. + - [8483fde9](https://www.github.com/tauri-apps/tauri/commit/8483fde975aac8833d2ce426e42fb40aeaeecba9) feat(core): expose `run_return` on Linux ([#3352](https://www.github.com/tauri-apps/tauri/pull/3352)) on 2022-02-07 +- Add `Menu::with_items` constructor, taking an iterator of `MenuEntry`. + - [7cc95e10](https://www.github.com/tauri-apps/tauri/commit/7cc95e10ec66d8b155e9bb7f89cf73df56d1f107) feat(core): add `Menu::with_items`, closes [#2807](https://www.github.com/tauri-apps/tauri/pull/2807) ([#2966](https://www.github.com/tauri-apps/tauri/pull/2966)) on 2021-12-27 +- Change event loop callbacks definition to allow callers to move in mutable values. + - [bdbf905e](https://www.github.com/tauri-apps/tauri/commit/bdbf905e5d802b58693d2bd27582ce4269faf79c) Transformed event-loop callback to FnMut to allow mutable values ([#2667](https://www.github.com/tauri-apps/tauri/pull/2667)) on 2021-09-27 +- Added `any_thread` constructor on the `Runtime` trait (only possible on Linux and Windows). + - [af44bf81](https://www.github.com/tauri-apps/tauri/commit/af44bf8168310cf77fbe102a53e7c433f11641a3) feat(core): allow app run on any thread on Linux & Windows, closes [#3172](https://www.github.com/tauri-apps/tauri/pull/3172) ([#3353](https://www.github.com/tauri-apps/tauri/pull/3353)) on 2022-02-07 +- Added `run_on_main_thread` API on `RuntimeHandle`. + - [53fdfe52](https://www.github.com/tauri-apps/tauri/commit/53fdfe52bb30d52653c72ca9f42506c3863dcf4a) feat(core): expose `run_on_main_thread` API ([#2711](https://www.github.com/tauri-apps/tauri/pull/2711)) on 2021-10-04 +- **Breaking change:** Renamed the `RPC` interface to `IPC`. + - [3420aa50](https://www.github.com/tauri-apps/tauri/commit/3420aa5031b3274a95c6c5fa0f8683ca13213396) refactor: IPC handler \[TRI-019] ([#9](https://www.github.com/tauri-apps/tauri/pull/9)) on 2022-01-09 +- Added `open_devtools` to the `Dispatcher` trait. + - [55aa22de](https://www.github.com/tauri-apps/tauri/commit/55aa22de80c3de873e29bcffcb5b2fe236a637a6) feat(core): add `Window#open_devtools` API, closes [#1213](https://www.github.com/tauri-apps/tauri/pull/1213) ([#3350](https://www.github.com/tauri-apps/tauri/pull/3350)) on 2022-02-07 +- The minimum Rust version is now `1.56`. + - [a9dfc015](https://www.github.com/tauri-apps/tauri/commit/a9dfc015505afe91281c2027954ffcc588b1a59c) feat: update to edition 2021 and set minimum rust to 1.56 ([#2789](https://www.github.com/tauri-apps/tauri/pull/2789)) on 2021-10-22 +- The window label is now validated and must be alphanumeric, resulting in a panic if it isn't. + - [680554de](https://www.github.com/tauri-apps/tauri/commit/680554de3ef6b7fccf87c441ad355cfef7aab6fe) feat: validate window label \[TRI-021] ([#13](https://www.github.com/tauri-apps/tauri/pull/13)) on 2021-10-23 +- Added `clipboard` field on the `WebviewAttributes` struct, which must be set to `true` to enable clipboard access on the webview. + - [d42ccfb3](https://www.github.com/tauri-apps/tauri/commit/d42ccfb34f71851dfeb22fe74c83a8bdbddb5550) feat: add `clipboard` flag to `WebviewAttributes` \[TRI-032] ([#12](https://www.github.com/tauri-apps/tauri/pull/12)) on 2021-10-23 +- Replace all of the `winapi` crate references with the `windows` crate, and replace `webview2` and `webview2-sys` with `webview2-com` and `webview2-com-sys` built with the `windows` crate. This goes along with updates to the TAO and WRY `next` branches. + - [bb00d5bd](https://www.github.com/tauri-apps/tauri/commit/bb00d5bd6c9dfcb6bdd0d308dadb70e6c6aafe5c) Replace winapi with windows crate and use webview2-com instead of webview2 ([#2615](https://www.github.com/tauri-apps/tauri/pull/2615)) on 2021-09-24 +- Update the `windows` crate to 0.25.0, which comes with pre-built libraries. WRY and Tao can both reference the same types directly from the `windows` crate instead of sharing bindings in `webview2-com-sys`. + - [34be6cf3](https://www.github.com/tauri-apps/tauri/commit/34be6cf37a98ee7cbd66623ebddae08e5a6520fd) Update webview2-com and windows crates ([#2875](https://www.github.com/tauri-apps/tauri/pull/2875)) on 2021-11-11 + +## \[0.2.1] + +- **Breaking change:** Removed `register_uri_scheme_protocol` from the `WebviewAttributes` struct and renamed `register_global_uri_scheme_protocol` to `register_uri_scheme_protocol` on the `Builder` struct, which now takes a `Fn(&AppHandle, &http::Request) -> http::Response` closure. + - [539e4489](https://www.github.com/tauri-apps/tauri/commit/539e4489e0bac7029d86917e9982ea49e02fe489) refactor: custom protocol ([#2503](https://www.github.com/tauri-apps/tauri/pull/2503)) on 2021-08-23 +- Migrate to latest custom protocol allowing `Partial content` streaming and Header parsing. + - [539e4489](https://www.github.com/tauri-apps/tauri/commit/539e4489e0bac7029d86917e9982ea49e02fe489) refactor: custom protocol ([#2503](https://www.github.com/tauri-apps/tauri/pull/2503)) on 2021-08-23 + +## \[0.2.0] + +- Fix blur/focus events being incorrect on Windows. + - [d832d575](https://www.github.com/tauri-apps/tauri/commit/d832d575d9b03a0ff78accabe4631cc638c08c3b) fix(windows): use webview events on windows ([#2277](https://www.github.com/tauri-apps/tauri/pull/2277)) on 2021-07-23 + +- Add `ExitRequested` event that allows preventing the app from exiting when all windows are closed, and an `AppHandle.exit()` function to exit the app manually. + - [892c63a0](https://www.github.com/tauri-apps/tauri/commit/892c63a0538f8d62680dce5848657128ad6b7af3) feat([#2287](https://www.github.com/tauri-apps/tauri/pull/2287)): Add `ExitRequested` event to let users prevent app from exiting ([#2293](https://www.github.com/tauri-apps/tauri/pull/2293)) on 2021-08-09 + +- Fixes minimum window height being used as maximum height. + - [e3f99165](https://www.github.com/tauri-apps/tauri/commit/e3f9916526b226866137cb663e5cafab2b6a0e01) fix(core) minHeight being used as maxHeight ([#2247](https://www.github.com/tauri-apps/tauri/pull/2247)) on 2021-07-19 + +- Update gtk and its related libraries to v0.14. This also remove requirements of `clang` as build dependency. + - [63ad3039](https://www.github.com/tauri-apps/tauri/commit/63ad303903bbee7c9a7382413b342e2a05d3ea75) chore(linux): bump gtk to v0.14 ([#2361](https://www.github.com/tauri-apps/tauri/pull/2361)) on 2021-08-07 + +- Implement `Debug` on public API structs and enums. + - [fa9341ba](https://www.github.com/tauri-apps/tauri/commit/fa9341ba18ba227735341530900714dba0f27291) feat(core): implement `Debug` on public API structs/enums, closes [#2292](https://www.github.com/tauri-apps/tauri/pull/2292) ([#2387](https://www.github.com/tauri-apps/tauri/pull/2387)) on 2021-08-11 + +- Panic when a dispatcher getter method (`Window`, `GlobalShortcutHandle`, `ClipboardManager` and `MenuHandle` APIs) is called on the main thread. + - [50ffdc06](https://www.github.com/tauri-apps/tauri/commit/50ffdc06fbde56aba32b4291fd130104935d1408) feat(core): panic when a dispatcher getter is used on the main thread ([#2455](https://www.github.com/tauri-apps/tauri/pull/2455)) on 2021-08-16 + +- Remove menu feature flag since there's no package dependency need to be installed on any platform anymore. + - [f81ebddf](https://www.github.com/tauri-apps/tauri/commit/f81ebddfcc1aea0d4989706aef43538e8ea98bea) feat: remove menu feature flag ([#2415](https://www.github.com/tauri-apps/tauri/pull/2415)) on 2021-08-13 + +- Adds `Resumed` and `MainEventsCleared` variants to the `RunEvent` enum. + - [6be3f433](https://www.github.com/tauri-apps/tauri/commit/6be3f4339168651fe4e003b09f7d181fd12cd5a8) feat(core): add `Resumed` and `MainEventsCleared` events, closes [#2127](https://www.github.com/tauri-apps/tauri/pull/2127) ([#2439](https://www.github.com/tauri-apps/tauri/pull/2439)) on 2021-08-15 + +- Adds `set_activation_policy` API to the `Runtime` trait (macOS only). + - [4a031add](https://www.github.com/tauri-apps/tauri/commit/4a031add69014a1f3823f4ea19b172a2557f6794) feat(core): expose `set_activation_policy`, closes [#2258](https://www.github.com/tauri-apps/tauri/pull/2258) ([#2420](https://www.github.com/tauri-apps/tauri/pull/2420)) on 2021-08-13 + +- Allow creation of empty Window with `create_tao_window()` and management with `send_tao_window_event()` on the AppHandler. + - [88080855](https://www.github.com/tauri-apps/tauri/commit/8808085541a629b8e22b612a06cef01cf9b3722e) feat(window): Allow creation of Window without `wry` ([#2321](https://www.github.com/tauri-apps/tauri/pull/2321)) on 2021-07-29 + - [15566cfd](https://www.github.com/tauri-apps/tauri/commit/15566cfd64f5072fa4980a6ce5b33259958e9021) feat(core): add API to send wry window message to the event loop ([#2339](https://www.github.com/tauri-apps/tauri/pull/2339)) on 2021-08-02 + +- - Support [macOS tray icon template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) to adjust automatically based on taskbar color. + +- Images you mark as template images should consist of only black and clear colors. You can use the alpha channel in the image to adjust the opacity of black content, however. + +- [426a6b49](https://www.github.com/tauri-apps/tauri/commit/426a6b49962de8faf061db2e820ac10fcbb300d6) feat(macOS): Implement tray icon template ([#2322](https://www.github.com/tauri-apps/tauri/pull/2322)) on 2021-07-29 + +- Add `Event::Ready` on the `run()` callback. Triggered once when the event loop is ready. + - [28c6b7ad](https://www.github.com/tauri-apps/tauri/commit/28c6b7adfe98e701b158e936eafb7541ddc700e0) feat: add `Event::Ready` ([#2433](https://www.github.com/tauri-apps/tauri/pull/2433)) on 2021-08-15 + +## \[0.1.4] + +- Allow preventing window close when the user requests it. + - [8157a68a](https://www.github.com/tauri-apps/tauri/commit/8157a68af1d94de1b90a14aa44139bb123b3436b) feat(core): allow listening to event loop events & prevent window close ([#2131](https://www.github.com/tauri-apps/tauri/pull/2131)) on 2021-07-06 +- Expose `gtk_window` getter. + - [e0a8e09c](https://www.github.com/tauri-apps/tauri/commit/e0a8e09cab6799eeb9ec524b5f7780d1e5a84299) feat(core): expose `gtk_window`, closes [#2083](https://www.github.com/tauri-apps/tauri/pull/2083) ([#2141](https://www.github.com/tauri-apps/tauri/pull/2141)) on 2021-07-02 +- `Params` has been removed, along with all the associated types on it. Functions that previously accepted those + associated types now accept strings instead. Type that used a generic parameter `Params` now use `Runtime` instead. If + you use the `wry` feature, then types with a `Runtime` generic parameter should default to `Wry`, letting you omit the + explicit type and let the compiler infer it instead. + +`tauri`: + +- See `Params` note +- If you were using `Params` inside a function parameter or definition, all references to it have been replaced with a + simple runtime that defaults to `Wry`. If you are not using a custom runtime, just remove `Params` from the definition + of functions/items that previously took it. If you are using a custom runtime, you *may* need to pass the runtime type + to these functions. +- If you were using custom types for `Params` (uncommon and if you don't understand you probably were not using it), all + methods that were previously taking the custom type now takes an `Into` or a `&str`. The types were already + required to be string-able, so just make sure to convert it into a string before passing it in if this breaking change + affects you. + +`tauri-macros`: + +- (internal) Added private `default_runtime` proc macro to allow us to give item definitions a custom runtime only when + the specified feature is enabled. + +`tauri-runtime`: + +- See `Params` note +- Removed `Params`, `MenuId`, `Tag`, `TagRef`. +- Added `menu::{MenuHash, MenuId, MenuIdRef}` as type aliases for the internal type that menu types now use. + - All previous menu items that had a `MenuId` generic now use the underlying `MenuId` type without a generic. +- `Runtime`, `RuntimeHandle`, and `Dispatch` have no more generic parameter on `create_window(...)` and instead use the + `Runtime` type directly +- `Runtime::system_tray` has no more `MenuId` generic and uses the string based `SystemTray` type directly. +- (internal) `CustomMenuItem::id_value()` is now hashed on creation and exposed as the `id` field with type `MenuHash`. + +`tauri-runtime-wry`: + +- See `Params` note +- update menu and runtime related types to the ones changed in `tauri-runtime`. + +`tauri-utils`: + +- `Assets::get` signature has changed to take a `&AssetKey` instead of `impl Into` to become trait object + safe. +- [fd8fab50](https://www.github.com/tauri-apps/tauri/commit/fd8fab507c8fa1b113b841af14c6693eb3955f6b) refactor(core): remove `Params` and replace with strings ([#2191](https://www.github.com/tauri-apps/tauri/pull/2191)) on 2021-07-15 + +## \[0.1.3] + +- `Window` is now `Send + Sync` on Windows. + - [fe32afcc](https://www.github.com/tauri-apps/tauri/commit/fe32afcc933920d6282ae1d63b041b182278a031) fix(core): `Window` must be `Send + Sync` on Windows, closes [#2078](https://www.github.com/tauri-apps/tauri/pull/2078) ([#2093](https://www.github.com/tauri-apps/tauri/pull/2093)) on 2021-06-27 + +## \[0.1.2] + +- Adds `clipboard` APIs (write and read text). + - [285bf64b](https://www.github.com/tauri-apps/tauri/commit/285bf64bf9569efb2df904c69c6df405ff0d62e2) feat(core): add clipboard writeText and readText APIs ([#2035](https://www.github.com/tauri-apps/tauri/pull/2035)) on 2021-06-21 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `focus` API to the WindowBuilder. + - [5f351622](https://www.github.com/tauri-apps/tauri/commit/5f351622c7812ad1bb56ddb37364ccaa4124c24b) feat(core): add focus API to the WindowBuilder and WindowOptions, [#1737](https://www.github.com/tauri-apps/tauri/pull/1737) on 2021-05-30 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `is_decorated` getter on Window. + - [f58a2114](https://www.github.com/tauri-apps/tauri/commit/f58a2114fbfd5307c349f05c88f2e08fd8baa8aa) feat(core): add `is_decorated` Window getter on 2021-05-30 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `is_resizable` getter on Window. + - [1e8af280](https://www.github.com/tauri-apps/tauri/commit/1e8af280c27f381828d6209722b10e889082fa00) feat(core): add `is_resizable` Window getter on 2021-05-30 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `is_visible` getter on Window. + - [36506c96](https://www.github.com/tauri-apps/tauri/commit/36506c967de82bc7ff453d11e6104ecf66d7a588) feat(core): add `is_visible` API on 2021-05-30 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `accelerator` method to the `CustomMenuItem` struct to define a keyboard shortcut for the menu item. + - [034c2601](https://www.github.com/tauri-apps/tauri/commit/034c26013bce0c7bbe6db067ea7fd24a53a5c998) feat(core): add `accelerator` method to `CustomMenuItem` ([#2043](https://www.github.com/tauri-apps/tauri/pull/2043)) on 2021-06-22 +- Adds global shortcut interfaces. + - [3280c4aa](https://www.github.com/tauri-apps/tauri/commit/3280c4aa91e50a8ccdd561a8b48a12a4a13ea8d5) refactor(core): global shortcut is now provided by `tao` ([#2031](https://www.github.com/tauri-apps/tauri/pull/2031)) on 2021-06-21 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `request_user_attention` API to the `Dispatcher` trait. + - [7dcca6e9](https://www.github.com/tauri-apps/tauri/commit/7dcca6e9281182b11ad3d4a79871f09b30b9b419) feat(core): add `request_user_attention` API, closes [#2023](https://www.github.com/tauri-apps/tauri/pull/2023) ([#2026](https://www.github.com/tauri-apps/tauri/pull/2026)) on 2021-06-20 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `fn run_iteration` (macOS and Windows only) to the Runtime trait. + - [8c0d0739](https://www.github.com/tauri-apps/tauri/commit/8c0d0739eebf7286b64a5380e922746411eb52c6) feat(core): add `run_iteration`, `parent_window` and `owner_window` APIs, closes [#1872](https://www.github.com/tauri-apps/tauri/pull/1872) ([#1874](https://www.github.com/tauri-apps/tauri/pull/1874)) on 2021-05-21 +- Adds `show_menu`, `hide_menu` and `is_menu_visible` APIs to the `Dispatcher` trait. + - [954460c5](https://www.github.com/tauri-apps/tauri/commit/954460c5205d57444ef4b1412051fbedf3e38676) feat(core): MenuHandle `show`, `hide`, `is_visible` and `toggle` APIs ([#1958](https://www.github.com/tauri-apps/tauri/pull/1958)) on 2021-06-15 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `set_focus` API on Window. + - [bb6992f8](https://www.github.com/tauri-apps/tauri/commit/bb6992f888196ca7c87bb2fe74ad2bd8bf393e05) feat(core): add `set_focus` window API, fixes [#1737](https://www.github.com/tauri-apps/tauri/pull/1737) on 2021-05-30 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `set_skip_taskbar` API on Window. + - [e06aa277](https://www.github.com/tauri-apps/tauri/commit/e06aa277384450cfef617c0e57b0d5d403bb1e7f) feat(core): add `set_skip_taskbar` API on 2021-05-30 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `skip_taskbar` API to the WindowBuilder. + - [5525b03a](https://www.github.com/tauri-apps/tauri/commit/5525b03a78a2232c650043fbd9894ce1553cad41) feat(core): add `skip_taskbar` API to the WindowBuilder/WindowOptions on 2021-05-30 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- Adds `Window#center` and `WindowBuilder#center` APIs. + - [5cba6eb4](https://www.github.com/tauri-apps/tauri/commit/5cba6eb4d28d53f06855d60d4d0eae6b95233ccf) feat(core): add window `center` API, closes [#1822](https://www.github.com/tauri-apps/tauri/pull/1822) ([#1954](https://www.github.com/tauri-apps/tauri/pull/1954)) on 2021-06-05 +- Adds `parent_window` and `owner_window` setters to the `WindowBuilder` (Windows only). + - [8c0d0739](https://www.github.com/tauri-apps/tauri/commit/8c0d0739eebf7286b64a5380e922746411eb52c6) feat(core): add `run_iteration`, `parent_window` and `owner_window` APIs, closes [#1872](https://www.github.com/tauri-apps/tauri/pull/1872) ([#1874](https://www.github.com/tauri-apps/tauri/pull/1874)) on 2021-05-21 +- Adds window native handle getter (HWND on Windows). + - [abf78c58](https://www.github.com/tauri-apps/tauri/commit/abf78c5860cdc52fbfd2bc5dbca29a864e2da8f9) fix(core): set parent window handle on dialogs, closes [#1876](https://www.github.com/tauri-apps/tauri/pull/1876) ([#1889](https://www.github.com/tauri-apps/tauri/pull/1889)) on 2021-05-21 + +## \[0.1.1] + +- Fixes `system-tray` feature usage. + - [1ab8dd9](https://www.github.com/tauri-apps/tauri/commit/1ab8dd93e670d2a2d070c7a6ec48308a0ab32f1a) fix(core): `system-tray` cargo feature usage, fixes [#1798](https://www.github.com/tauri-apps/tauri/pull/1798) ([#1801](https://www.github.com/tauri-apps/tauri/pull/1801)) on 2021-05-12 + +## \[0.1.0] + +- **Breaking:** `Context` fields are now private, and is expected to be created through `Context::new(...)`. + All fields previously available through `Context` are now public methods. + - [5542359](https://www.github.com/tauri-apps/tauri/commit/55423590ddbf560684dab6a0214acf95aadfa8d2) refactor(core): Context fields now private, Icon used on all platforms ([#1774](https://www.github.com/tauri-apps/tauri/pull/1774)) on 2021-05-11 +- `tauri-runtime` crate initial release. + - [665ec1d](https://www.github.com/tauri-apps/tauri/commit/665ec1d4a199ad06e369221da187dc838da71cbf) refactor: move runtime to `tauri-runtime` crate ([#1751](https://www.github.com/tauri-apps/tauri/pull/1751)) on 2021-05-09 + - [45a7a11](https://www.github.com/tauri-apps/tauri/commit/45a7a111e0cf9d9956d713cc9a99fa7a5313eec7) feat(core): add `tauri-wry` crate ([#1756](https://www.github.com/tauri-apps/tauri/pull/1756)) on 2021-05-09 diff --git a/crates/tauri-runtime/Cargo.toml b/crates/tauri-runtime/Cargo.toml new file mode 100644 index 0000000..1ff8b06 --- /dev/null +++ b/crates/tauri-runtime/Cargo.toml @@ -0,0 +1,74 @@ +[package] +name = "tauri-runtime" +version = "2.11.3" +description = "Runtime for Tauri applications" +exclude = ["CHANGELOG.md", "/target"] +authors.workspace = true +homepage.workspace = true +repository.workspace = true +categories.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[package.metadata.docs.rs] +all-features = true +default-target = "x86_64-unknown-linux-gnu" +targets = [ + "x86_64-pc-windows-msvc", + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-linux-android", + "x86_64-apple-ios", +] + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2" +tauri-utils = { workspace = true } +http = "1" +raw-window-handle = "0.6" +url = { version = "2" } +dpi = { version = "0.1", features = ["serde"] } +# WARNING: cookie::Cookie is re-exported so bumping this is a breaking change, documented to be done as a minor bump +cookie = "0.18" + +[target."cfg(windows)".dependencies.windows] +version = "0.62" +features = ["Win32_Foundation", "Win32_System_WinRT"] + +[target."cfg(windows)".dependencies] +webview2-com = "0.39" + +[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] +gtk = { version = "0.18", features = ["v3_24"] } +webkit2gtk = { version = "=2.0", features = ["v2_40"] } + +[target."cfg(target_os = \"android\")".dependencies] +jni = "0.21" + +[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies] +objc2 = "0.6" +objc2-ui-kit = { version = "0.3.2", default-features = false, features = [ + "UIView", + "UIResponder", + "UIScene", + "UISceneOptions", +] } + +[target."cfg(target_os = \"macos\")".dependencies] +url = "2" +objc2 = "0.6" +objc2-web-kit = { version = "0.3.2", default-features = false, features = [ + "objc2-app-kit", + "WKWebView", + "WKWebViewConfiguration", +] } + +[features] +devtools = [] +macos-private-api = [] + +[lints] +workspace = true diff --git a/crates/tauri-runtime/LICENSE-APACHE-2.0 b/crates/tauri-runtime/LICENSE-APACHE-2.0 new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/crates/tauri-runtime/LICENSE-APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/tauri-runtime/LICENSE-MIT b/crates/tauri-runtime/LICENSE-MIT new file mode 100644 index 0000000..b08530d --- /dev/null +++ b/crates/tauri-runtime/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/tauri-runtime/README.md b/crates/tauri-runtime/README.md new file mode 100644 index 0000000..450fbd9 --- /dev/null +++ b/crates/tauri-runtime/README.md @@ -0,0 +1,44 @@ +# tauri-runtime + + + +[![status](https://img.shields.io/badge/Status-Beta-green.svg)](https://github.com/tauri-apps/tauri) +[![Chat Server](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/SpmNs4S) + +[![test core](https://img.shields.io/github/actions/workflow/status/tauri-apps/tauri/test-core.yml?label=test%20core&logo=github)](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml) +[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app) + +[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation) +[![support](https://img.shields.io/badge/sponsor-Opencollective-blue.svg)](https://opencollective.com/tauri) + +| Component | Version | +| ------------- | -------------------------------------------------------------------------------------------------------------- | +| tauri-runtime | [![](https://img.shields.io/crates/v/tauri-runtime?style=flat-square)](https://crates.io/crates/tauri-runtime) | + +## About Tauri + +Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily. + +Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task. + +## This module + +This is the glue layer between tauri itself and lower level webview libraries. +None of the exposed API of this crate is stable, and it may break semver +compatibility in the future. The major version only signifies the intended Tauri version. + +To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document. + +## Semver + +**tauri** is following [Semantic Versioning 2.0](https://semver.org/). + +## Licenses + +Code: (c) 2021 - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. + +Logo: CC-BY-NC-ND + +- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum) diff --git a/crates/tauri-runtime/build.rs b/crates/tauri-runtime/build.rs new file mode 100644 index 0000000..9974c50 --- /dev/null +++ b/crates/tauri-runtime/build.rs @@ -0,0 +1,19 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// creates a cfg alias if `has_feature` is true. +// `alias` must be a snake case string. +fn alias(alias: &str, has_feature: bool) { + println!("cargo:rustc-check-cfg=cfg({alias})"); + if has_feature { + println!("cargo:rustc-cfg={alias}"); + } +} + +fn main() { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let mobile = target_os == "ios" || target_os == "android"; + alias("desktop", !mobile); + alias("mobile", mobile); +} diff --git a/crates/tauri-runtime/src/dpi.rs b/crates/tauri-runtime/src/dpi.rs new file mode 100644 index 0000000..e30d489 --- /dev/null +++ b/crates/tauri-runtime/src/dpi.rs @@ -0,0 +1,60 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +pub use dpi::*; +use serde::Serialize; + +/// A rectangular region. +#[derive(Clone, Copy, Debug, Serialize)] +pub struct Rect { + /// Rect position. + pub position: dpi::Position, + /// Rect size. + pub size: dpi::Size, +} + +impl Default for Rect { + fn default() -> Self { + Self { + position: Position::Logical((0, 0).into()), + size: Size::Logical((0, 0).into()), + } + } +} + +/// A rectangular region in physical pixels. +#[derive(Clone, Copy, Debug, Serialize)] +pub struct PhysicalRect { + /// Rect position. + pub position: dpi::PhysicalPosition

, + /// Rect size. + pub size: dpi::PhysicalSize, +} + +impl Default for PhysicalRect { + fn default() -> Self { + Self { + position: (0, 0).into(), + size: (0, 0).into(), + } + } +} + +/// A rectangular region in logical pixels. +#[derive(Clone, Copy, Debug, Serialize)] +pub struct LogicalRect { + /// Rect position. + pub position: dpi::LogicalPosition

, + /// Rect size. + pub size: dpi::LogicalSize, +} + +impl Default for LogicalRect { + fn default() -> Self { + Self { + position: (0, 0).into(), + size: (0, 0).into(), + } + } +} diff --git a/crates/tauri-runtime/src/lib.rs b/crates/tauri-runtime/src/lib.rs new file mode 100644 index 0000000..0c924c0 --- /dev/null +++ b/crates/tauri-runtime/src/lib.rs @@ -0,0 +1,1034 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Internal runtime between Tauri and the underlying webview runtime. +//! +//! None of the exposed API of this crate is stable, and it may break semver +//! compatibility in the future. The major version only signifies the intended Tauri version. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" +)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +use raw_window_handle::DisplayHandle; +use serde::Deserialize; +use std::{borrow::Cow, fmt::Debug, sync::mpsc::Sender}; +use tauri_utils::Theme; +use tauri_utils::config::Color; +use url::Url; +use webview::{DetachedWebview, PendingWebview}; + +/// UI scaling utilities. +pub mod dpi; +/// Types useful for interacting with a user's monitors. +pub mod monitor; +pub mod webview; +mod webview_permissions; +pub mod window; + +use dpi::{PhysicalPosition, PhysicalSize, Position, Rect, Size}; +use monitor::Monitor; +use window::{ + CursorIcon, DetachedWindow, PendingWindow, RawWindow, WebviewEvent, WindowEvent, + WindowSizeConstraints, +}; +use window::{WindowBuilder, WindowId}; + +use http::{ + header::{InvalidHeaderName, InvalidHeaderValue}, + method::InvalidMethod, + status::InvalidStatusCode, +}; + +/// Cookie extraction +pub use cookie::Cookie; + +pub type WindowEventId = u32; +pub type WebviewEventId = u32; + +/// Progress bar status. +#[derive(Debug, Clone, Copy, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ProgressBarStatus { + /// Hide progress bar. + None, + /// Normal state. + Normal, + /// Indeterminate state. **Treated as Normal on Linux and macOS** + Indeterminate, + /// Paused state. **Treated as Normal on Linux** + Paused, + /// Error state. **Treated as Normal on Linux** + Error, +} + +/// Progress Bar State +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProgressBarState { + /// The progress bar status. + pub status: Option, + /// The progress bar progress. This can be a value ranging from `0` to `100` + pub progress: Option, + /// The `.desktop` filename with the Unity desktop window manager, for example `myapp.desktop` **Linux Only** + pub desktop_filename: Option, +} + +/// Type of user attention requested on a window. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(tag = "type")] +pub enum UserAttentionType { + /// ## Platform-specific + /// - **macOS:** Bounces the dock icon until the application is in focus. + /// - **Windows:** Flashes both the window and the taskbar button until the application is in focus. + Critical, + /// ## Platform-specific + /// - **macOS:** Bounces the dock icon once. + /// - **Windows:** Flashes the taskbar button until the application is in focus. + Informational, +} + +/// Defines which device events (raw input from mice, keyboards and other HID devices that is not +/// bound to a specific window) the event loop should deliver to the application. +/// +/// Listening to device events can be expensive, so the runtime filters them out by default +/// while the application has no focused window. See [`crate::Runtime::set_device_event_filter`]. +/// +/// ## Platform-specific +/// +/// - **Linux / macOS / iOS / Android**: Unsupported, device events are always filtered out. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(tag = "type")] +pub enum DeviceEventFilter { + /// Always filter out device events. + Always, + /// Filter out device events while the window is not focused. + #[default] + Unfocused, + /// Report all device events regardless of window focus. + Never, +} + +/// Defines the orientation that a window resize will be performed. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +pub enum ResizeDirection { + East, + North, + NorthEast, + NorthWest, + South, + SouthEast, + SouthWest, + West, +} + +/// Errors returned by the webview runtime. +/// +/// These are surfaced to Tauri applications wrapped in +/// [`tauri::Error::Runtime`](https://docs.rs/tauri/latest/tauri/enum.Error.html). +/// +/// This enum is `#[non_exhaustive]`: new variants can be added in minor releases. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum Error { + /// Failed to create webview. + #[error("failed to create webview: {0}")] + CreateWebview(Box), + // TODO: Make it take an error like `CreateWebview` in v3 + /// Failed to create window. + #[error("failed to create window")] + CreateWindow, + /// The given window label is invalid. + #[error("Window labels must only include alphanumeric characters, `-`, `/`, `:` and `_`.")] + InvalidWindowLabel, + /// Failed to send message to webview. + #[error("failed to send message to the webview")] + FailedToSendMessage, + /// Failed to receive message from webview. + #[error("failed to receive message from webview")] + FailedToReceiveMessage, + /// Failed to serialize/deserialize. + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + /// Failed to load window icon. + #[error("invalid icon: {0}")] + InvalidIcon(Box), + /// Failed to get monitor on window operation. + #[error("failed to get monitor")] + FailedToGetMonitor, + /// Failed to get cursor position. + #[error("failed to get cursor position")] + FailedToGetCursorPosition, + #[error("Invalid header name: {0}")] + InvalidHeaderName(#[from] InvalidHeaderName), + #[error("Invalid header value: {0}")] + InvalidHeaderValue(#[from] InvalidHeaderValue), + #[error("Invalid status code: {0}")] + InvalidStatusCode(#[from] InvalidStatusCode), + #[error("Invalid method: {0}")] + InvalidMethod(#[from] InvalidMethod), + #[error("Infallible error, something went really wrong: {0}")] + Infallible(#[from] std::convert::Infallible), + #[error("the event loop has been closed")] + EventLoopClosed, + #[error("Invalid proxy url")] + InvalidProxyUrl, + #[error("window not found")] + WindowNotFound, + #[cfg(any(target_os = "macos", target_os = "ios"))] + #[error("failed to remove data store")] + FailedToRemoveDataStore, + #[error("Could not find the webview runtime, make sure it is installed")] + WebviewRuntimeNotInstalled, +} + +/// Result type. +pub type Result = std::result::Result; + +/// Window icon. +#[derive(Debug, Clone)] +pub struct Icon<'a> { + /// RGBA bytes of the icon. + pub rgba: Cow<'a, [u8]>, + /// Icon width. + pub width: u32, + /// Icon height. + pub height: u32, +} + +/// A type that can be used as an user event. +pub trait UserEvent: Debug + Clone + Send + 'static {} + +impl UserEvent for T {} + +/// Event triggered on the event loop run. +#[derive(Debug)] +#[non_exhaustive] +pub enum RunEvent { + /// Event loop is exiting. + Exit, + /// Event loop is about to exit + ExitRequested { + /// The exit code. + code: Option, + tx: Sender, + }, + /// An event associated with a window. + WindowEvent { + /// The window label. + label: String, + /// The detailed event. + event: WindowEvent, + }, + /// An event associated with a webview. + WebviewEvent { + /// The webview label. + label: String, + /// The detailed event. + event: WebviewEvent, + }, + /// Application ready. + Ready, + /// Sent if the event loop is being resumed. + Resumed, + /// Emitted when all of the event loop's input events have been processed and redraw processing is about to begin. + /// + /// This event is useful as a place to put your code that should be run after all state-changing events have been handled and you want to do stuff (updating state, performing calculations, etc) that happens as the "main body" of your event loop. + MainEventsCleared, + /// Emitted when the user wants to open the specified resource with the app. + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))] + Opened { urls: Vec }, + /// Emitted when the NSApplicationDelegate's applicationShouldHandleReopen gets called + #[cfg(target_os = "macos")] + Reopen { + /// Indicates whether the NSApplication object found any visible windows in your application. + has_visible_windows: bool, + }, + /// 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 +#[derive(Debug)] +pub enum ExitRequestedEventAction { + /// Prevent the event loop from exiting + Prevent, +} + +/// Application's activation policy. Corresponds to NSApplicationActivationPolicy. +#[cfg(target_os = "macos")] +#[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] +#[non_exhaustive] +pub enum ActivationPolicy { + /// Corresponds to NSApplicationActivationPolicyRegular. + Regular, + /// Corresponds to NSApplicationActivationPolicyAccessory. + Accessory, + /// Corresponds to NSApplicationActivationPolicyProhibited. + Prohibited, +} + +/// A [`Send`] handle to the runtime. +pub trait RuntimeHandle: Debug + Clone + Send + Sync + Sized + 'static { + type Runtime: Runtime; + + /// Creates an `EventLoopProxy` that can be used to dispatch user events to the main event loop. + fn create_proxy(&self) -> >::EventLoopProxy; + + /// Sets the activation policy for the application. + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_activation_policy(&self, activation_policy: ActivationPolicy) -> Result<()>; + + /// Sets the dock visibility for the application. + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_dock_visibility(&self, visible: bool) -> Result<()>; + + /// Requests an exit of the event loop. + fn request_exit(&self, code: i32) -> Result<()>; + + /// Create a new window. + fn create_window( + &self, + pending: PendingWindow, + after_window_creation: Option, + ) -> Result>; + + /// Create a new webview. + fn create_webview( + &self, + window_id: WindowId, + pending: PendingWebview, + ) -> Result>; + + /// Run a task on the main thread. + fn run_on_main_thread(&self, f: F) -> Result<()>; + + /// Get a handle to the display controller of the windowing system. + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError>; + + /// Returns the primary monitor of the system. + /// + /// Returns None if it can't identify any monitor as a primary one. + fn primary_monitor(&self) -> Result>; + + /// Returns the monitor that contains the given point. + fn monitor_from_point(&self, x: f64, y: f64) -> Result>; + + /// Returns the list of all the monitors available on the system. + fn available_monitors(&self) -> Result>; + + /// Get the cursor position relative to the top-left hand corner of the desktop. + fn cursor_position(&self) -> Result>; + + /// Sets the app theme. + fn set_theme(&self, theme: Option); + + /// Shows the application, but does not automatically focus it. + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn show(&self) -> Result<()>; + + /// Hides the application. + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn hide(&self) -> Result<()>; + + /// Change the device event filter mode. + /// + /// See [Runtime::set_device_event_filter] for details. + /// + /// ## Platform-specific + /// + /// See [Runtime::set_device_event_filter] for details. + fn set_device_event_filter(&self, filter: DeviceEventFilter); + + /// Finds an Android class in the project scope. + #[cfg(target_os = "android")] + fn find_class<'a>( + &self, + env: &mut jni::JNIEnv<'a>, + activity: &jni::objects::JObject<'_>, + name: impl Into, + ) -> std::result::Result, jni::errors::Error>; + + /// Dispatch a closure to run on the Android context. + /// + /// The closure takes the JNI env, the Android activity instance and the possibly null webview. + #[cfg(target_os = "android")] + fn run_on_android_context(&self, f: F) + where + F: FnOnce(&mut jni::JNIEnv, &jni::objects::JObject, &jni::objects::JObject) + Send + 'static; + + #[cfg(any(target_os = "macos", target_os = "ios"))] + #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))] + fn fetch_data_store_identifiers) + Send + 'static>( + &self, + cb: F, + ) -> Result<()>; + + #[cfg(any(target_os = "macos", target_os = "ios"))] + #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))] + fn remove_data_store) + Send + 'static>( + &self, + uuid: [u8; 16], + cb: F, + ) -> Result<()>; +} + +pub trait EventLoopProxy: Debug + Clone + Send + Sync { + fn send_event(&self, event: T) -> Result<()>; +} + +#[derive(Default)] +pub struct RuntimeInitArgs { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub app_id: Option, + #[cfg(windows)] + pub msg_hook: Option bool + 'static>>, +} + +/// The webview runtime interface. +pub trait Runtime: Debug + Sized + 'static { + /// The window message dispatcher. + type WindowDispatcher: WindowDispatch; + /// The webview message dispatcher. + type WebviewDispatcher: WebviewDispatch; + /// The runtime handle type. + type Handle: RuntimeHandle; + /// The proxy type. + type EventLoopProxy: EventLoopProxy; + + /// Creates a new webview runtime. Must be used on the main thread. + fn new(args: RuntimeInitArgs) -> Result; + + /// Creates a new webview runtime on any thread. + #[cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + #[cfg_attr( + docsrs, + doc(cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))) + )] + fn new_any_thread(args: RuntimeInitArgs) -> Result; + + /// Creates an `EventLoopProxy` that can be used to dispatch user events to the main event loop. + fn create_proxy(&self) -> Self::EventLoopProxy; + + /// Gets a runtime handle. + fn handle(&self) -> Self::Handle; + + /// Create a new window. + fn create_window( + &self, + pending: PendingWindow, + after_window_creation: Option, + ) -> Result>; + + /// Create a new webview. + fn create_webview( + &self, + window_id: WindowId, + pending: PendingWebview, + ) -> Result>; + + /// Returns the primary monitor of the system. + /// + /// Returns None if it can't identify any monitor as a primary one. + fn primary_monitor(&self) -> Option; + + /// Returns the monitor that contains the given point. + fn monitor_from_point(&self, x: f64, y: f64) -> Option; + + /// Returns the list of all the monitors available on the system. + fn available_monitors(&self) -> Vec; + + /// Get the cursor position relative to the top-left hand corner of the desktop. + fn cursor_position(&self) -> Result>; + + /// Sets the app theme. + fn set_theme(&self, theme: Option); + + /// Sets the activation policy for the application. + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_activation_policy(&mut self, activation_policy: ActivationPolicy); + + /// Sets whether the application activates when launched while another application is already active. + /// + /// This API must be called before the event loop starts. + /// + /// If `false`, the app activates only if no other app is currently active. + /// If `true`, the app activates regardless. + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_activate_ignoring_other_apps(&mut self, ignore: bool); + + /// Sets the dock visibility for the application. + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_dock_visibility(&mut self, visible: bool); + + /// Shows the application, but does not automatically focus it. + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn show(&self); + + /// Hides the application. + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn hide(&self); + + /// Change the device event filter mode. + /// + /// Since the DeviceEvent capture can lead to high CPU usage for unfocused windows, [`tao`] + /// will ignore them by default for unfocused windows on Windows. This method allows changing + /// the filter to explicitly capture them again. + /// + /// ## Platform-specific + /// + /// - ** Linux / macOS / iOS / Android**: Unsupported. + /// + /// [`tao`]: https://crates.io/crates/tao + fn set_device_event_filter(&mut self, filter: DeviceEventFilter); + + /// Runs an iteration of the runtime event loop and returns control flow to the caller. + #[cfg(desktop)] + fn run_iteration) + 'static>(&mut self, callback: F); + + /// Equivalent to [`Runtime::run`] but returns the exit code instead of exiting the process. + fn run_return) + 'static>(self, callback: F) -> i32; + + /// Run the webview runtime. + fn run) + 'static>(self, callback: F); +} + +/// Webview dispatcher. A thread-safe handle to the webview APIs. +pub trait WebviewDispatch: Debug + Clone + Send + Sync + Sized + 'static { + /// The runtime this [`WebviewDispatch`] runs under. + type Runtime: Runtime; + + /// Run a task on the main thread. + fn run_on_main_thread(&self, f: F) -> Result<()>; + + /// Registers a webview event handler. + fn on_webview_event(&self, f: F) -> WebviewEventId; + + /// Runs a closure with the platform webview object as argument. + fn with_webview) + Send + 'static>(&self, f: F) -> Result<()>; + + /// Open the web inspector which is usually called devtools. + #[cfg(any(debug_assertions, feature = "devtools"))] + fn open_devtools(&self); + + /// Close the web inspector which is usually called devtools. + #[cfg(any(debug_assertions, feature = "devtools"))] + fn close_devtools(&self); + + /// Gets the devtools window's current open state. + #[cfg(any(debug_assertions, feature = "devtools"))] + fn is_devtools_open(&self) -> Result; + + // GETTERS + + /// Returns the webview's current URL. + fn url(&self) -> Result; + + /// Returns the webview's bounds. + fn bounds(&self) -> Result; + + /// Returns the position of the top-left hand corner of the webviews's client area relative to the top-left hand corner of the window. + fn position(&self) -> Result>; + + /// Returns the physical size of the webviews's client area. + fn size(&self) -> Result>; + + // SETTER + + /// Navigate to the given URL. + fn navigate(&self, url: Url) -> Result<()>; + + /// Reloads the current page. + fn reload(&self) -> Result<()>; + + /// Opens the dialog to prints the contents of the webview. + fn print(&self) -> Result<()>; + + /// Closes the webview. + fn close(&self) -> Result<()>; + + /// Sets the webview's bounds. + fn set_bounds(&self, bounds: Rect) -> Result<()>; + + /// Resizes the webview. + fn set_size(&self, size: Size) -> Result<()>; + + /// Updates the webview position. + fn set_position(&self, position: Position) -> Result<()>; + + /// Bring the window to front and focus the webview. + fn set_focus(&self) -> Result<()>; + + /// Hide the webview + fn hide(&self) -> Result<()>; + + /// Show the webview + fn show(&self) -> Result<()>; + + /// 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<()>; + + /// Get cookies for a particular url. + /// + /// # Stability + /// + /// See [WebviewDispatch::cookies]. + fn cookies_for_url(&self, url: Url) -> Result>>; + + /// Return all cookies in the cookie store. + /// + /// # Stability + /// + /// The return value of this function leverages [`cookie::Cookie`] which re-exports the cookie crate. + /// This dependency might receive updates in minor Tauri releases. + fn cookies(&self) -> Result>>; + + /// Set a cookie for the webview. + /// + /// # Stability + /// + /// See [WebviewDispatch::cookies]. + fn set_cookie(&self, cookie: cookie::Cookie<'_>) -> Result<()>; + + /// Delete a cookie for the webview. + /// + /// # Stability + /// + /// See [WebviewDispatch::cookies]. + fn delete_cookie(&self, cookie: cookie::Cookie<'_>) -> Result<()>; + + /// Sets whether the webview should automatically grow and shrink its size and position when the parent window resizes. + fn set_auto_resize(&self, auto_resize: bool) -> Result<()>; + + /// Set the webview zoom level + fn set_zoom(&self, scale_factor: f64) -> Result<()>; + + /// Set the webview background. + fn set_background_color(&self, color: Option) -> Result<()>; + + /// Clear all browsing data for this webview. + fn clear_all_browsing_data(&self) -> Result<()>; +} + +/// Window dispatcher. A thread-safe handle to the window APIs. +pub trait WindowDispatch: Debug + Clone + Send + Sync + Sized + 'static { + /// The runtime this [`WindowDispatch`] runs under. + type Runtime: Runtime; + + /// The window builder type. + type WindowBuilder: WindowBuilder; + + /// Run a task on the main thread. + fn run_on_main_thread(&self, f: F) -> Result<()>; + + /// Registers a window event handler. + fn on_window_event(&self, f: F) -> WindowEventId; + + // GETTERS + + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa. + fn scale_factor(&self) -> Result; + + /// Returns the position of the top-left hand corner of the window's client area relative to the top-left hand corner of the desktop. + fn inner_position(&self) -> Result>; + + /// Returns the position of the top-left hand corner of the window relative to the top-left hand corner of the desktop. + fn outer_position(&self) -> Result>; + + /// Returns the physical size of the window's client area. + /// + /// The client area is the content of the window, excluding the title bar and borders. + fn inner_size(&self) -> Result>; + + /// Returns the physical size of the entire window. + /// + /// These dimensions include the title bar and borders. If you don't want that (and you usually don't), use inner_size instead. + fn outer_size(&self) -> Result>; + + /// Gets the window's current fullscreen state. + fn is_fullscreen(&self) -> Result; + + /// Gets the window's current minimized state. + fn is_minimized(&self) -> Result; + + /// Gets the window's current maximized state. + fn is_maximized(&self) -> Result; + + /// Gets the window's current focus state. + fn is_focused(&self) -> Result; + + /// Gets the window's current decoration state. + fn is_decorated(&self) -> Result; + + /// Gets the window's current resizable state. + fn is_resizable(&self) -> Result; + + /// Gets the window's native maximize button state. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + fn is_maximizable(&self) -> Result; + + /// Gets the window's native minimize button state. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + fn is_minimizable(&self) -> Result; + + /// Gets the window's native close button state. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + fn is_closable(&self) -> Result; + + /// Gets the window's current visibility state. + fn is_visible(&self) -> Result; + + /// Whether the window is enabled or disable. + fn is_enabled(&self) -> Result; + + /// Gets the window alwaysOnTop flag state. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + fn is_always_on_top(&self) -> Result; + + /// Gets the window's current title. + fn title(&self) -> Result; + + /// Returns the monitor on which the window currently resides. + /// + /// Returns None if current monitor can't be detected. + fn current_monitor(&self) -> Result>; + + /// Returns the primary monitor of the system. + /// + /// Returns None if it can't identify any monitor as a primary one. + fn primary_monitor(&self) -> Result>; + + /// Returns the monitor that contains the given point. + fn monitor_from_point(&self, x: f64, y: f64) -> Result>; + + /// Returns the list of all the monitors available on the system. + fn available_monitors(&self) -> Result>; + + /// Returns the `ApplicationWindow` from gtk crate that is used by this window. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn gtk_window(&self) -> Result; + + /// Returns the vertical [`gtk::Box`] that is added by default as the sole child of this window. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + 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, + ) -> std::result::Result, raw_window_handle::HandleError>; + + /// Returns the current window theme. + fn theme(&self) -> Result; + + // SETTERS + + /// Centers the window. + fn center(&self) -> Result<()>; + + /// Requests user attention to the window. + /// + /// Providing `None` will unset the request for user attention. + fn request_user_attention(&self, request_type: Option) -> Result<()>; + + /// Create a new window. + fn create_window( + &mut self, + pending: PendingWindow, + after_window_creation: Option, + ) -> Result>; + + /// Create a new webview. + fn create_webview( + &mut self, + pending: PendingWebview, + ) -> Result>; + + /// Updates the window resizable flag. + fn set_resizable(&self, resizable: bool) -> Result<()>; + + /// Enable or disable the window. + /// + /// ## Platform-specific + /// + /// - **Android / iOS**: Unsupported. + fn set_enabled(&self, enabled: bool) -> Result<()>; + + /// Updates the window's native maximize button state. + /// + /// ## Platform-specific + /// + /// - **macOS:** Disables the "zoom" button in the window titlebar, which is also used to enter fullscreen mode. + /// - **Linux / iOS / Android:** Unsupported. + fn set_maximizable(&self, maximizable: bool) -> Result<()>; + + /// Updates the window's native minimize button state. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + fn set_minimizable(&self, minimizable: bool) -> Result<()>; + + /// Updates the window's native close button state. + /// + /// ## 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. + fn set_closable(&self, closable: bool) -> Result<()>; + + /// Updates the window title. + fn set_title>(&self, title: S) -> Result<()>; + + /// Maximizes the window. + fn maximize(&self) -> Result<()>; + + /// Unmaximizes the window. + fn unmaximize(&self) -> Result<()>; + + /// Minimizes the window. + fn minimize(&self) -> Result<()>; + + /// Unminimizes the window. + fn unminimize(&self) -> Result<()>; + + /// Shows the window. + fn show(&self) -> Result<()>; + + /// Hides the window. + fn hide(&self) -> Result<()>; + + /// Closes the window. + fn close(&self) -> Result<()>; + + /// Destroys the window. + fn destroy(&self) -> Result<()>; + + /// Updates the decorations flag. + fn set_decorations(&self, decorations: bool) -> Result<()>; + + /// Updates the shadow flag. + fn set_shadow(&self, enable: bool) -> Result<()>; + + /// Updates the window alwaysOnBottom flag. + fn set_always_on_bottom(&self, always_on_bottom: bool) -> Result<()>; + + /// Updates the window alwaysOnTop flag. + fn set_always_on_top(&self, always_on_top: bool) -> Result<()>; + + /// Updates the window visibleOnAllWorkspaces flag. + fn set_visible_on_all_workspaces(&self, visible_on_all_workspaces: bool) -> Result<()>; + + /// Set the window background. + fn set_background_color(&self, color: Option) -> Result<()>; + + /// Prevents the window contents from being captured by other apps. + fn set_content_protected(&self, protected: bool) -> Result<()>; + + /// Resizes the window. + fn set_size(&self, size: Size) -> Result<()>; + + /// Updates the window min inner size. + fn set_min_size(&self, size: Option) -> Result<()>; + + /// Updates the window max inner size. + fn set_max_size(&self, size: Option) -> Result<()>; + + /// Sets this window's minimum inner width. + fn set_size_constraints(&self, constraints: WindowSizeConstraints) -> Result<()>; + + /// Updates the window position. + fn set_position(&self, position: Position) -> Result<()>; + + /// Updates the window fullscreen state. + fn set_fullscreen(&self, fullscreen: bool) -> Result<()>; + + /// Sets the window as fullscreen on the monitor that contains the given physical position. + /// + /// Does nothing if no monitor contains the position. + fn set_fullscreen_on_monitor(&self, position: PhysicalPosition) -> Result<()>; + + #[cfg(target_os = "macos")] + fn set_simple_fullscreen(&self, enable: bool) -> Result<()>; + + /// Bring the window to front and focus. + fn set_focus(&self) -> Result<()>; + + /// Sets whether the window can be focused. + fn set_focusable(&self, focusable: bool) -> Result<()>; + + /// Updates the window icon. + fn set_icon(&self, icon: Icon) -> Result<()>; + + /// Whether to hide the window icon from the taskbar or not. + fn set_skip_taskbar(&self, skip: bool) -> Result<()>; + + /// Grabs the cursor, preventing it from leaving the window. + /// + /// There's no guarantee that the cursor will be hidden. You should + /// hide it by yourself if you want so. + fn set_cursor_grab(&self, grab: bool) -> Result<()>; + + /// Modifies the cursor's visibility. + /// + /// If `false`, this will hide the cursor. If `true`, this will show the cursor. + fn set_cursor_visible(&self, visible: bool) -> Result<()>; + + // Modifies the cursor icon of the window. + fn set_cursor_icon(&self, icon: CursorIcon) -> Result<()>; + + /// Changes the position of the cursor in window coordinates. + fn set_cursor_position>(&self, position: Pos) -> Result<()>; + + /// Ignores the window cursor events. + fn set_ignore_cursor_events(&self, ignore: bool) -> Result<()>; + + /// Starts dragging the window. + fn start_dragging(&self) -> Result<()>; + + /// Starts resize-dragging the window. + fn start_resize_dragging(&self, direction: ResizeDirection) -> Result<()>; + + /// Sets the badge count on the taskbar + /// The badge count appears as a whole for the application + /// Using `0` or using `None` will remove the badge + /// + /// ## Platform-specific + /// - **Windows:** Unsupported, use [`WindowDispatch::set_overlay_icon`] instead. + /// - **Android:** Unsupported. + /// - **iOS:** iOS expects i32, if the value is larger than i32::MAX, it will be clamped to i32::MAX. + fn set_badge_count(&self, count: Option, desktop_filename: Option) -> Result<()>; + + /// Sets the badge count on the taskbar **macOS only**. Using `None` will remove the badge + fn set_badge_label(&self, label: Option) -> Result<()>; + + /// Sets the overlay icon on the taskbar **Windows only**. Using `None` will remove the icon + /// + /// The overlay icon can be unique for each window. + fn set_overlay_icon(&self, icon: Option) -> Result<()>; + + /// Sets the taskbar progress state. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS**: Progress bar is app-wide and not specific to this window. Only supported desktop environments with `libunity` (e.g. GNOME). + /// - **iOS / Android:** Unsupported. + fn set_progress_bar(&self, progress_state: ProgressBarState) -> Result<()>; + + /// Sets the title bar style. Available on macOS only. + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / iOS / Android:** Unsupported. + fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> Result<()>; + + /// Change the position of the window controls. Available on macOS only. + /// + /// Requires titleBarStyle: Overlay and decorations: true. + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / iOS / Android:** Unsupported. + fn set_traffic_light_position(&self, position: Position) -> Result<()>; + + /// Sets the theme for this window. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS**: Theme is app-wide and not specific to this window. + /// - **iOS / Android:** Unsupported. + fn set_theme(&self, theme: Option) -> Result<()>; +} diff --git a/crates/tauri-runtime/src/monitor.rs b/crates/tauri-runtime/src/monitor.rs new file mode 100644 index 0000000..9abda36 --- /dev/null +++ b/crates/tauri-runtime/src/monitor.rs @@ -0,0 +1,21 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::dpi::{PhysicalPosition, PhysicalRect, PhysicalSize}; + +/// Monitor descriptor. +#[derive(Debug, Clone)] +pub struct Monitor { + /// A human-readable name of the monitor. + /// `None` if the monitor doesn't exist anymore. + pub name: Option, + /// The monitor's resolution. + pub size: PhysicalSize, + /// The top-left corner position of the monitor relative to the larger full screen area. + pub position: PhysicalPosition, + /// The monitor's work_area. + pub work_area: PhysicalRect, + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa. + pub scale_factor: f64, +} diff --git a/crates/tauri-runtime/src/webview.rs b/crates/tauri-runtime/src/webview.rs new file mode 100644 index 0000000..f2375ad --- /dev/null +++ b/crates/tauri-runtime/src/webview.rs @@ -0,0 +1,937 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! A layer between raw [`Runtime`] webviews and Tauri. +//! +pub use crate::webview_permissions::{PermissionKind, PermissionResponse}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::window::WindowId; +use crate::{Rect, Runtime, UserEvent, window::is_label_valid}; + +use http::Request; +use tauri_utils::config::{ + BackgroundThrottlingPolicy, Color, ScrollBarStyle as ConfigScrollBarStyle, WebviewUrl, + WindowConfig, WindowEffectsConfig, +}; +use url::Url; + +use std::{ + borrow::Cow, + collections::HashMap, + hash::{Hash, Hasher}, + path::PathBuf, + sync::Arc, +}; + +type UriSchemeProtocolHandler = dyn Fn(&str, http::Request>, Box>) + Send>) + + Send + + Sync + + 'static; + +type WebResourceRequestHandler = + dyn Fn(http::Request>, &mut http::Response>) + Send + Sync; + +type NavigationHandler = dyn Fn(&Url) -> bool + Send; + +type NewWindowHandler = dyn Fn(Url, NewWindowFeatures) -> NewWindowResponse + Send; + +type OnPageLoadHandler = dyn Fn(Url, PageLoadEvent) + Send; + +type DocumentTitleChangedHandler = dyn Fn(String) + Send + 'static; + +type DownloadHandler = dyn Fn(DownloadEvent) -> bool + Send + Sync; + +type PermissionRequestHandler = dyn Fn(PermissionKind) -> PermissionResponse + 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 + + Sync + + 'static; + +/// Download event. +pub enum DownloadEvent<'a> { + /// Download requested. + Requested { + /// The url being downloaded. + url: Url, + /// Represents where the file will be downloaded to. + /// Can be used to set the download location by assigning a new path to it. + /// The assigned path _must_ be absolute. + destination: &'a mut PathBuf, + }, + /// Download finished. + Finished { + /// The URL of the original download request. + url: Url, + /// Potentially representing the filesystem path the file was downloaded to. + path: Option, + /// Indicates if the download succeeded or not. + success: bool, + }, +} + +#[cfg(target_os = "android")] +pub struct CreationContext<'a, 'b> { + pub env: &'a mut jni::JNIEnv<'b>, + pub activity: &'a jni::objects::JObject<'b>, + pub webview: &'a jni::objects::JObject<'b>, +} + +/// Kind of event for the page load handler. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PageLoadEvent { + /// Page started to load. + Started, + /// Page finished loading. + Finished, +} + +/// Information about the webview that initiated a new window request. +#[derive(Debug)] +pub struct NewWindowOpener { + /// The instance of the webview that initiated the new window request. + /// + /// This must be set as the related view of the new webview. See [`WebviewAttributes::related_view`]. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + pub webview: webkit2gtk::WebView, + /// The instance of the webview that initiated the new window request. + /// + /// The target webview environment **MUST** match the environment of the opener webview. See [`WebviewAttributes::with_environment`]. + #[cfg(windows)] + pub webview: webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2, + #[cfg(windows)] + pub environment: webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Environment, + /// The instance of the webview that initiated the new window request. + #[cfg(target_os = "macos")] + pub webview: objc2::rc::Retained, + /// Configuration of the target webview. + /// + /// This **MUST** be used when creating the target webview. See [`WebviewAttributes::webview_configuration`]. + #[cfg(target_os = "macos")] + pub target_configuration: objc2::rc::Retained, +} + +/// Window features of a window requested to open. +#[derive(Debug)] +pub struct NewWindowFeatures { + pub(crate) size: Option>, + pub(crate) position: Option>, + pub(crate) opener: NewWindowOpener, +} + +impl NewWindowFeatures { + pub fn new( + size: Option>, + position: Option>, + opener: NewWindowOpener, + ) -> Self { + Self { + size, + position, + opener, + } + } + + /// Specifies the size of the content area + /// as defined by the user's operating system where the new window will be generated. + pub fn size(&self) -> Option> { + self.size + } + + /// Specifies the position of the window relative to the work area + /// as defined by the user's operating system where the new window will be generated. + pub fn position(&self) -> Option> { + self.position + } + + /// Returns information about the webview that initiated a new window request. + pub fn opener(&self) -> &NewWindowOpener { + &self.opener + } +} + +/// Response for the new window request handler. +pub enum NewWindowResponse { + /// Allow the window to be opened with the default implementation. + Allow, + /// Allow the window to be opened, with the given window. + /// + /// ## Platform-specific: + /// + /// **Linux**: The webview must be related to the caller webview. See [`WebviewAttributes::related_view`]. + /// **Windows**: The webview must use the same environment as the caller webview. See [`WebviewAttributes::with_environment`]. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Create { window_id: WindowId }, + /// Deny the window from being opened. + Deny, +} + +/// The scrollbar style to use in the webview. +/// +/// ## Platform-specific +/// +/// - **Windows**: This option must be given the same value for all webviews that target the same data directory. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, Default)] +pub enum ScrollBarStyle { + #[default] + /// The default scrollbar style for the webview. + Default, + + #[cfg(windows)] + /// Fluent UI style overlay scrollbars. **Windows Only** + /// + /// Requires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions, + /// see + FluentOverlay, +} + +/// A webview that has yet to be built. +pub struct PendingWebview> { + /// The label that the webview will be named. + pub label: String, + + /// The [`WebviewAttributes`] that the webview will be created with. + pub webview_attributes: WebviewAttributes, + + /// Custom protocols to register on the webview + pub uri_scheme_protocols: HashMap>, + + /// How to handle IPC calls on the webview. + pub ipc_handler: Option>, + + /// A handler to decide if incoming url is allowed to navigate. + pub navigation_handler: Option>, + + pub new_window_handler: Option>, + + pub document_title_changed_handler: Option>, + + /// The resolved URL to load on the webview. + pub url: String, + + #[cfg(target_os = "android")] + #[allow(clippy::type_complexity)] + pub on_webview_created: + Option) -> Result<(), jni::errors::Error> + Send + Sync>>, + + pub web_resource_request_handler: Option>, + + pub on_page_load_handler: Option>, + + pub download_handler: Option>, + + pub permission_request_handler: Option>, + + #[cfg(any(target_os = "macos", target_os = "ios"))] + pub on_web_content_process_terminate_handler: Option>, +} + +impl> PendingWebview { + /// Create a new [`PendingWebview`] with a label from the given [`WebviewAttributes`]. + pub fn new( + webview_attributes: WebviewAttributes, + label: impl Into, + ) -> crate::Result { + let label = label.into(); + if !is_label_valid(&label) { + Err(crate::Error::InvalidWindowLabel) + } else { + Ok(Self { + webview_attributes, + uri_scheme_protocols: Default::default(), + label, + ipc_handler: None, + navigation_handler: None, + new_window_handler: None, + document_title_changed_handler: None, + url: "tauri://localhost".to_string(), + #[cfg(target_os = "android")] + on_webview_created: None, + web_resource_request_handler: None, + on_page_load_handler: None, + download_handler: None, + permission_request_handler: None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + on_web_content_process_terminate_handler: None, + }) + } + } + + pub fn register_uri_scheme_protocol< + N: Into, + H: Fn(&str, http::Request>, Box>) + Send>) + + Send + + Sync + + 'static, + >( + &mut self, + uri_scheme: N, + protocol_handler: H, + ) { + let uri_scheme = uri_scheme.into(); + self + .uri_scheme_protocols + .insert(uri_scheme, Box::new(protocol_handler)); + } + + #[cfg(target_os = "android")] + pub fn on_webview_created< + F: Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send + Sync + 'static, + >( + mut self, + f: F, + ) -> Self { + self.on_webview_created.replace(Box::new(f)); + self + } +} + +/// A webview that is not yet managed by Tauri. +#[derive(Debug)] +pub struct DetachedWebview> { + /// Name of the window + pub label: String, + + /// The [`crate::WebviewDispatch`] associated with the window. + pub dispatcher: R::WebviewDispatcher, +} + +impl> Clone for DetachedWebview { + fn clone(&self) -> Self { + Self { + label: self.label.clone(), + dispatcher: self.dispatcher.clone(), + } + } +} + +impl> Hash for DetachedWebview { + /// Only use the [`DetachedWebview`]'s label to represent its hash. + fn hash(&self, state: &mut H) { + self.label.hash(state) + } +} + +impl> Eq for DetachedWebview {} +impl> PartialEq for DetachedWebview { + /// Only use the [`DetachedWebview`]'s label to compare equality. + fn eq(&self, other: &Self) -> bool { + self.label.eq(&other.label) + } +} + +/// The attributes used to create an webview. +#[derive(Debug)] +pub struct WebviewAttributes { + pub url: WebviewUrl, + pub user_agent: Option, + /// A list of initialization javascript scripts to run when loading new pages. + /// When webview load a new page, this initialization code will be executed. + /// It is guaranteed that code is executed before `window.onload`. + /// + /// ## Platform-specific + /// + /// - **Windows:** scripts are always added to subframes. + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// [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) + pub initialization_scripts: Vec, + pub data_directory: Option, + pub drag_drop_handler_enabled: bool, + pub clipboard: bool, + pub accept_first_mouse: bool, + pub additional_browser_args: Option, + pub window_effects: Option, + pub incognito: bool, + pub transparent: bool, + pub focus: bool, + pub bounds: Option, + pub auto_resize: bool, + pub proxy_url: Option, + pub zoom_hotkeys_enabled: bool, + pub browser_extensions_enabled: bool, + pub extensions_path: Option, + pub data_store_identifier: Option<[u8; 16]>, + pub use_https_scheme: bool, + pub devtools: Option, + pub background_color: Option, + pub traffic_light_position: Option, + pub background_throttling: Option, + pub javascript_disabled: bool, + /// on macOS and iOS there is a link preview on long pressing links, this is enabled by default. + /// 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. + /// + /// The closure parameter is the webview instance. + /// + /// The accessory view is the view that appears above the keyboard when a text input element is focused. + /// It usually displays a view with "Done", "Next" buttons. + /// + /// # Stability + /// + /// This relies on [`objc2_ui_kit`] which does not provide a stable API yet, so it can receive breaking changes in minor releases. + #[cfg(target_os = "ios")] + pub input_accessory_view_builder: Option, + #[cfg(target_os = "ios")] + pub limit_navigations_to_app_bound_domains: bool, + + /// Set the environment for the webview. + /// Useful if you need to share the same environment, for instance when using the [`PendingWebview::new_window_handler`]. + #[cfg(windows)] + pub environment: Option, + + /// Creates a new webview sharing the same web process with the provided webview. + /// Useful if you need to link a webview to another, for instance when using the [`PendingWebview::new_window_handler`]. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + pub related_view: Option, + + #[cfg(target_os = "macos")] + pub webview_configuration: Option>, +} + +unsafe impl Send for WebviewAttributes {} +unsafe impl Sync for WebviewAttributes {} + +#[cfg(target_os = "ios")] +#[non_exhaustive] +pub struct InputAccessoryViewBuilder(pub Box); + +#[cfg(target_os = "ios")] +impl std::fmt::Debug for InputAccessoryViewBuilder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("InputAccessoryViewBuilder").finish() + } +} + +#[cfg(target_os = "ios")] +impl InputAccessoryViewBuilder { + pub fn new(builder: Box) -> Self { + Self(builder) + } +} + +impl From<&WindowConfig> for WebviewAttributes { + fn from(config: &WindowConfig) -> Self { + let mut builder = Self::new(config.url.clone()) + .incognito(config.incognito) + .focused(config.focus) + .zoom_hotkeys_enabled(config.zoom_hotkeys_enabled) + .use_https_scheme(config.use_https_scheme) + .browser_extensions_enabled(config.browser_extensions_enabled) + .background_throttling(config.background_throttling.clone()) + .devtools(config.devtools) + .scroll_bar_style(match config.scroll_bar_style { + ConfigScrollBarStyle::Default => ScrollBarStyle::Default, + #[cfg(windows)] + ConfigScrollBarStyle::FluentOverlay => ScrollBarStyle::FluentOverlay, + _ => ScrollBarStyle::Default, + }) + .limit_navigations_to_app_bound_domains(config.limit_navigations_to_app_bound_domains) + .general_autofill_enabled(config.general_autofill_enabled); + + #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] + { + builder = builder.transparent(config.transparent); + } + #[cfg(target_os = "macos")] + { + if let Some(position) = &config.traffic_light_position { + builder = + builder.traffic_light_position(dpi::LogicalPosition::new(position.x, position.y).into()); + } + } + builder = builder.accept_first_mouse(config.accept_first_mouse); + if !config.drag_drop_enabled { + builder = builder.disable_drag_drop_handler(); + } + if let Some(user_agent) = &config.user_agent { + builder = builder.user_agent(user_agent); + } + if let Some(additional_browser_args) = &config.additional_browser_args { + builder = builder.additional_browser_args(additional_browser_args); + } + if let Some(effects) = &config.window_effects { + builder = builder.window_effects(effects.clone()); + } + if let Some(url) = &config.proxy_url { + builder = builder.proxy_url(url.to_owned()); + } + if let Some(color) = config.background_color { + builder = builder.background_color(color); + } + builder.javascript_disabled = config.javascript_disabled; + builder.allow_link_preview = config.allow_link_preview; + #[cfg(target_os = "ios")] + if config.disable_input_accessory_view { + builder + .input_accessory_view_builder + .replace(InputAccessoryViewBuilder::new(Box::new(|_webview| None))); + } + builder + } +} + +impl WebviewAttributes { + /// Initializes the default attributes for a webview. + pub fn new(url: WebviewUrl) -> Self { + Self { + url, + user_agent: None, + initialization_scripts: Vec::new(), + data_directory: None, + drag_drop_handler_enabled: true, + clipboard: false, + accept_first_mouse: false, + additional_browser_args: None, + window_effects: None, + incognito: false, + transparent: false, + focus: true, + bounds: None, + auto_resize: false, + proxy_url: None, + zoom_hotkeys_enabled: false, + browser_extensions_enabled: false, + data_store_identifier: None, + extensions_path: None, + use_https_scheme: false, + devtools: None, + background_color: None, + traffic_light_position: None, + background_throttling: None, + 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, + #[cfg(target_os = "ios")] + limit_navigations_to_app_bound_domains: false, + #[cfg(windows)] + environment: None, + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ))] + related_view: None, + #[cfg(target_os = "macos")] + webview_configuration: None, + } + } + + /// Sets the user agent + #[must_use] + pub fn user_agent(mut self, user_agent: &str) -> Self { + self.user_agent = Some(user_agent.to_string()); + self + } + + /// Adds an init script for the main frame. + /// + /// When webview load a new page, this initialization code will be executed. + /// It is guaranteed that code is executed before `window.onload`. + /// + /// This is executed only on the main frame. + /// If you only want to run it in all frames, use [`Self::initialization_script_on_all_frames`] instead. + /// + /// ## Platform-specific + /// + /// - **Windows:** scripts are always added to subframes. + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// [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.initialization_scripts.push(InitializationScript { + script: script.into(), + for_main_frame_only: true, + }); + self + } + + /// Adds an init script for all frames. + /// + /// When webview load a new page, this initialization code will be executed. + /// It is guaranteed that code is executed before `window.onload`. + /// + /// This is executed on all frames, main frame and also sub frames. + /// If you only want to run it in the main frame, use [`Self::initialization_script`] instead. + /// + /// ## Platform-specific + /// + /// - **Windows:** scripts are always added to subframes. + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// [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_on_all_frames(mut self, script: impl Into) -> Self { + self.initialization_scripts.push(InitializationScript { + script: script.into(), + for_main_frame_only: false, + }); + self + } + + /// Data directory for the webview. + #[must_use] + pub fn data_directory(mut self, data_directory: PathBuf) -> Self { + self.data_directory.replace(data_directory); + self + } + + /// Disables the drag and drop handler used internally to generate [`DragDropEvent`](crate::window::DragDropEvent)s. + /// + /// This is required to use HTML5 drag and drop APIs on the frontend on Windows since we replace the drag drop handler of WebView2. + #[must_use] + pub fn disable_drag_drop_handler(mut self) -> Self { + self.drag_drop_handler_enabled = false; + self + } + + /// Enables clipboard access for the page rendered on **Linux** and **Windows**. + /// + /// **macOS** doesn't provide such method and is always enabled by default, + /// but you still need to add menu item accelerators to use shortcuts. + #[must_use] + pub fn enable_clipboard_access(mut self) -> Self { + self.clipboard = true; + self + } + + /// Sets whether clicking an inactive window also clicks through to the webview. + #[must_use] + pub fn accept_first_mouse(mut self, accept: bool) -> Self { + self.accept_first_mouse = accept; + self + } + + /// Sets additional browser arguments. **Windows Only** + #[must_use] + pub fn additional_browser_args(mut self, additional_args: &str) -> Self { + self.additional_browser_args = Some(additional_args.to_string()); + self + } + + /// Sets window effects + #[must_use] + pub fn window_effects(mut self, effects: WindowEffectsConfig) -> Self { + self.window_effects = Some(effects); + self + } + + /// Enable or disable incognito mode for the WebView. + #[must_use] + pub fn incognito(mut self, incognito: bool) -> Self { + self.incognito = incognito; + self + } + + /// Enable or disable transparency for the WebView. + #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] + #[must_use] + pub fn transparent(mut self, transparent: bool) -> Self { + self.transparent = transparent; + self + } + + /// Whether the webview should be focused or not. + #[must_use] + pub fn focused(mut self, focus: bool) -> Self { + self.focus = focus; + self + } + + /// Sets the webview to automatically grow and shrink its size and position when the parent window resizes. + #[must_use] + pub fn auto_resize(mut self) -> Self { + self.auto_resize = true; + self + } + + /// Enable proxy for the WebView + #[must_use] + pub fn proxy_url(mut self, url: Url) -> Self { + self.proxy_url = Some(url); + self + } + + /// Whether page zooming by hotkeys is enabled + /// + /// ## Platform-specific: + /// + /// - **Windows**: Controls WebView2's [`IsZoomControlEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2settings?view=webview2-winrt-1.0.2420.47#iszoomcontrolenabled) setting. + /// - **MacOS / Linux**: Injects a polyfill that zooms in and out with `ctrl/command` + `-/=`, + /// 20% in each step, ranging from 20% to 1000%. Requires `webview:allow-set-webview-zoom` permission + /// + /// - **Android / iOS**: Unsupported. + #[must_use] + pub fn zoom_hotkeys_enabled(mut self, enabled: bool) -> Self { + self.zoom_hotkeys_enabled = enabled; + self + } + + /// Whether browser extensions can be installed for the webview process + /// + /// ## Platform-specific: + /// + /// - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled) + /// - **MacOS / Linux / iOS / Android** - Unsupported. + #[must_use] + pub fn browser_extensions_enabled(mut self, enabled: bool) -> Self { + self.browser_extensions_enabled = enabled; + self + } + + /// Sets whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android. Defaults to `false`. + /// + /// ## Note + /// + /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `://localhost` protocols used on macOS and Linux. + /// + /// ## Warning + /// + /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data. + #[must_use] + pub fn use_https_scheme(mut self, enabled: bool) -> Self { + self.use_https_scheme = enabled; + self + } + + /// Whether web inspector, which is usually called browser devtools, is enabled or not. Enabled by default. + /// + /// This API works in **debug** builds, but requires `devtools` feature flag to enable it in **release** builds. + /// + /// ## Platform-specific + /// + /// - macOS: This will call private functions on **macOS**. + /// - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android. + /// - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window. + #[must_use] + pub fn devtools(mut self, enabled: Option) -> Self { + self.devtools = enabled; + self + } + + /// Set the window and webview background color. + /// ## Platform-specific: + /// + /// - **Windows**: On Windows 7, alpha channel is ignored for the webview layer. + /// - **Windows**: On Windows 8 and newer, if alpha channel is not `0`, it will be ignored. + #[must_use] + pub fn background_color(mut self, color: Color) -> Self { + self.background_color = Some(color); + self + } + + /// Change the position of the window controls. Available on macOS only. + /// + /// Requires titleBarStyle: Overlay and decorations: true. + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / iOS / Android:** Unsupported. + #[must_use] + pub fn traffic_light_position(mut self, position: dpi::Position) -> Self { + self.traffic_light_position = Some(position); + self + } + + /// Whether to show a link preview when long pressing on links. Available on macOS and iOS only. + /// + /// Default is true. + /// + /// See https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / Android:** Unsupported. + #[must_use] + pub fn allow_link_preview(mut self, allow_link_preview: bool) -> Self { + self.allow_link_preview = allow_link_preview; + self + } + + /// Whether to limit navigations to App-Bound Domains. This is necessary to + /// enable Service Workers on iOS according to + /// [StackOverflow](https://stackoverflow.com/questions/49673399/service-workers-unavailable-in-wkwebview-in-ios-11-3/64155509#64155509). + /// + /// Default is false. + /// + /// Note: If you pass in `true` make sure to add localhost and any [`registrable + /// domains`](https://developer.mozilla.org/en-US/docs/Glossary/Registrable_domain) + /// used in this webview to tauri-src/Info.ios.plist: + /// + /// ```xml + /// + /// + /// WKAppBoundDomains + /// + /// localhost + /// aregistrabledomain.example + /// + /// + /// + /// ``` + /// + /// You must add `localhost` if any webview with this set to true opens a + /// local webpage, makes any localhost calls, or uses the isolation pattern + /// because Tauri uses the `localhost` domain for hosting the application + /// webpage, the IPC protocol, and the isolation pattern's iframe. + /// + /// Requests served through custom uri schemes are allowed so long as they use + /// a registrable domain specified in the `WKAppBoundDomains` array for all the + /// requests from the app, including requests for the `localhost` domain. + /// + /// In theory, you can whitelist an entire uri scheme by including the + /// protocol name followed by a colon. For example, to allow all requests + /// using a custom "stream" uri scheme (see [this tauri + /// example](https://github.com/tauri-apps/tauri/blob/dev/examples/streaming/main.rs)), + /// you could add `stream:` to the AppBoundDomains array. That said, I'm not + /// sure whether Apple would let your app through app review if you do + /// whitelist an entire protocol because this feature is not mentioned in + /// [their blog post on App-Bound + /// Domains](https://webkit.org/blog/10882/app-bound-domains/). + /// + /// See https://webkit.org/blog/10882/app-bound-domains/ and + /// https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/limitsnavigationstoappbounddomains + /// for the official documentation on App-Bound Domains. + /// + /// ## Platform-specific + /// + /// - **iOS**: Supported since version 14.0+. + /// - **Linux / Windows / Android / MacOS:** Unsupported. + #[must_use] + #[allow(unused_variables, unused_mut)] + pub fn limit_navigations_to_app_bound_domains(mut self, limit_navigations: bool) -> Self { + #[cfg(target_os = "ios")] + { + self.limit_navigations_to_app_bound_domains = limit_navigations; + } + self + } + + /// Change the default background throttling behavior. + /// + /// By default, browsers use a suspend policy that will throttle timers and even unload + /// the whole tab (view) to free resources after roughly 5 minutes when a view became + /// minimized or hidden. This will pause all tasks until the documents visibility state + /// changes back from hidden to visible by bringing the view back to the foreground. + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice. + /// - **iOS**: Supported since version 17.0+. + /// - **macOS**: Supported since version 14.0+. + /// + /// see + #[must_use] + pub fn background_throttling(mut self, policy: Option) -> Self { + self.background_throttling = policy; + self + } + + /// Specifies the native scrollbar style to use with the webview. + /// CSS styles that modify the scrollbar are applied on top of the native appearance configured here. + /// + /// Defaults to [`ScrollBarStyle::Default`], which is the browser default. + /// + /// ## Platform-specific + /// + /// - **Windows**: + /// - [`ScrollBarStyle::FluentOverlay`] requires WebView2 Runtime version 125.0.2535.41 or higher, + /// and does nothing on older versions. + /// - This option must be given the same value for all webviews that target the same data directory. Use + /// [`WebviewAttributes::data_directory`] to change data directories if needed. + /// - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation. + #[must_use] + pub fn scroll_bar_style(mut self, style: ScrollBarStyle) -> Self { + 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. +pub type WebviewIpcHandler = Box, Request) + Send>; + +/// An initialization script +#[derive(Debug, Clone)] +pub struct InitializationScript { + /// The script to run + pub script: String, + /// Whether the script should be injected to main frame only + pub for_main_frame_only: bool, +} diff --git a/crates/tauri-runtime/src/webview_permissions.rs b/crates/tauri-runtime/src/webview_permissions.rs new file mode 100644 index 0000000..7221c7e --- /dev/null +++ b/crates/tauri-runtime/src/webview_permissions.rs @@ -0,0 +1,161 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/// Permission types that can be requested by the webview. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum PermissionKind { + /// Microphone access permission. + Microphone, + /// Camera access permission. + Camera, + /// Geolocation access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION`. + /// - **Linux**: Supported via `GeolocationPermissionRequest`. + /// - **Android**: Supported via `WebChromeClient.onGeolocationPermissionsShowPrompt`. + /// - **macOS / iOS**: Not yet supported by platform backends. + Geolocation, + /// Notifications permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS`. + /// - **Linux**: Supported via `NotificationPermissionRequest`. + /// - **macOS / Android / iOS**: Not yet supported by platform backends. + Notifications, + /// Clipboard read permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_CLIPBOARD_READ`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + ClipboardRead, + /// Display capture permission (for getDisplayMedia). + DisplayCapture, + /// Midi access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_MIDI_SYSTEM_EXCLUSIVE_MESSAGES`. + /// - **Android**: Supported via `android.webkit.resource.MIDI_SYSEX`. + /// - **macOS / Linux / iOS**: Not yet supported by platform backends. + Midi, + /// Sensors (accelerometer, gyroscope, etc.) access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_OTHER_SENSORS`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + Sensors, + /// Media key system access permission. + /// + /// ## Platform-specific + /// + /// - **Android**: Supported via `android.webkit.resource.PROTECTED_MEDIA_ID`. + /// - **Windows / macOS / Linux / iOS**: Not yet supported by platform backends. + MediaKeySystemAccess, + /// Local fonts access permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_LOCAL_FONTS`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + LocalFonts, + /// Window management permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_WINDOW_MANAGEMENT`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + WindowManagement, + /// Pointer lock permission. + /// + /// ## Platform-specific + /// + /// - **Linux**: Supported via `PointerLockPermissionRequest`. + /// - **Windows / macOS / Android / iOS**: Not yet supported by platform backends. + PointerLock, + /// Automatic downloads permission (multiple downloads without user interaction). + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_MULTIPLE_AUTOMATIC_DOWNLOADS`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + AutomaticDownloads, + /// File system access permission (read/write via File System Access API). + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_FILE_READ_WRITE`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + FileSystemAccess, + /// Media autoplay permission. + /// + /// ## Platform-specific + /// + /// - **Windows**: Supported via `COREWEBVIEW2_PERMISSION_KIND_AUTOPLAY`. + /// - **macOS / Linux / Android / iOS**: Not yet supported by platform backends. + Autoplay, + /// Other unrecognized permission type. + Other, +} + +impl std::fmt::Display for PermissionKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Microphone => write!(f, "microphone"), + Self::Camera => write!(f, "camera"), + Self::Geolocation => write!(f, "geolocation"), + Self::Notifications => write!(f, "notifications"), + Self::ClipboardRead => write!(f, "clipboard-read"), + Self::DisplayCapture => write!(f, "display-capture"), + Self::Midi => write!(f, "midi"), + Self::Sensors => write!(f, "sensors"), + Self::MediaKeySystemAccess => write!(f, "media-key-system-access"), + Self::LocalFonts => write!(f, "local-fonts"), + Self::WindowManagement => write!(f, "window-management"), + Self::PointerLock => write!(f, "pointer-lock"), + Self::AutomaticDownloads => write!(f, "automatic-downloads"), + Self::FileSystemAccess => write!(f, "file-system-access"), + Self::Autoplay => write!(f, "autoplay"), + Self::Other => write!(f, "other"), + } + } +} + +/// Response for permission requests. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum PermissionResponse { + /// Grant the permission. + /// + /// ## Platform-specific + /// + /// - **Android**: Not supported for runtime permissions; the normal Android + /// permission flow is used instead. + Allow, + /// Deny the permission. + Deny, + /// Use the platform or browser default behavior. + /// + /// ## Platform-specific + /// + /// - **Windows / macOS / Android**: The default behavior is to continue the + /// platform or browser permission flow. + /// - **Linux**: The default behavior is [`Self::Deny`] + #[default] + Default, +} + +impl std::fmt::Display for PermissionResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Allow => write!(f, "allow"), + Self::Deny => write!(f, "deny"), + Self::Default => write!(f, "default"), + } + } +} diff --git a/crates/tauri-runtime/src/window.rs b/crates/tauri-runtime/src/window.rs new file mode 100644 index 0000000..2ad7c63 --- /dev/null +++ b/crates/tauri-runtime/src/window.rs @@ -0,0 +1,670 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! A layer between raw [`Runtime`] windows and Tauri. + +use crate::{ + Icon, Runtime, UserEvent, WindowDispatch, + webview::{DetachedWebview, PendingWebview}, +}; + +use dpi::PixelUnit; +use serde::{Deserialize, Deserializer, Serialize}; +use tauri_utils::{ + Theme, + config::{Color, WindowConfig}, +}; +#[cfg(windows)] +use windows::Win32::Foundation::HWND; + +use std::{ + hash::{Hash, Hasher}, + marker::PhantomData, + path::PathBuf, + sync::mpsc::Sender, +}; + +/// An event from a window. +#[derive(Debug, Clone)] +pub enum WindowEvent { + /// The size of the window has changed. Contains the client area's new dimensions. + Resized(dpi::PhysicalSize), + /// The position of the window has changed. Contains the window's new position. + Moved(dpi::PhysicalPosition), + /// The window has been requested to close. + CloseRequested { + /// A signal sender. If a `true` value is emitted, the window won't be closed. + signal_tx: Sender, + }, + /// The window has been destroyed. + Destroyed, + /// The window gained or lost focus. + /// + /// The parameter is true if the window has gained focus, and false if it has lost focus. + Focused(bool), + /// The window's scale factor has changed. + /// + /// The following user actions can cause DPI changes: + /// + /// - Changing the display's resolution. + /// - Changing the display's scale factor (e.g. in Control Panel on Windows). + /// - Moving the window to a display with a different scale factor. + ScaleFactorChanged { + /// The new scale factor. + scale_factor: f64, + /// The window inner size. + new_inner_size: dpi::PhysicalSize, + }, + /// An event associated with the drag and drop action. + DragDrop(DragDropEvent), + /// The system window theme has changed. + /// + /// 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)] + #[cfg_attr(docsrs, doc(cfg(any(target_os = "android", target_os = "ios"))))] + 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)] + #[cfg_attr(docsrs, doc(cfg(any(target_os = "android", target_os = "ios"))))] + Resumed, +} + +/// An event from a window. +#[derive(Debug, Clone)] +pub enum WebviewEvent { + /// An event associated with the drag and drop action. + DragDrop(DragDropEvent), +} + +/// The drag drop event payload. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum DragDropEvent { + /// A drag operation has entered the webview. + Enter { + /// List of paths that are being dragged onto the webview. + paths: Vec, + /// The position of the mouse cursor. + position: dpi::PhysicalPosition, + }, + /// A drag operation is moving over the webview. + Over { + /// The position of the mouse cursor. + position: dpi::PhysicalPosition, + }, + /// The file(s) have been dropped onto the webview. + Drop { + /// List of paths that are being dropped onto the window. + paths: Vec, + /// The position of the mouse cursor. + position: dpi::PhysicalPosition, + }, + /// The drag operation has been cancelled or left the window. + Leave, +} + +/// Describes the appearance of the mouse cursor. +#[non_exhaustive] +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] +pub enum CursorIcon { + /// The platform-dependent default cursor. + #[default] + Default, + /// A simple crosshair. + Crosshair, + /// A hand (often used to indicate links in web browsers). + Hand, + /// Self explanatory. + Arrow, + /// Indicates something is to be moved. + Move, + /// Indicates text that may be selected or edited. + Text, + /// Program busy indicator. + Wait, + /// Help indicator (often rendered as a "?") + Help, + /// Progress indicator. Shows that processing is being done. But in contrast + /// with "Wait" the user may still interact with the program. Often rendered + /// as a spinning beach ball, or an arrow with a watch or hourglass. + Progress, + + /// Cursor showing that something cannot be done. + NotAllowed, + ContextMenu, + Cell, + VerticalText, + Alias, + Copy, + NoDrop, + /// Indicates something can be grabbed. + Grab, + /// Indicates something is grabbed. + Grabbing, + AllScroll, + ZoomIn, + ZoomOut, + + /// Indicate that some edge is to be moved. For example, the 'SeResize' cursor + /// is used when the movement starts from the south-east corner of the box. + EResize, + NResize, + NeResize, + NwResize, + SResize, + SeResize, + SwResize, + WResize, + EwResize, + NsResize, + NeswResize, + NwseResize, + ColResize, + RowResize, +} + +impl<'de> Deserialize<'de> for CursorIcon { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(match s.to_lowercase().as_str() { + "default" => CursorIcon::Default, + "crosshair" => CursorIcon::Crosshair, + "hand" => CursorIcon::Hand, + "arrow" => CursorIcon::Arrow, + "move" => CursorIcon::Move, + "text" => CursorIcon::Text, + "wait" => CursorIcon::Wait, + "help" => CursorIcon::Help, + "progress" => CursorIcon::Progress, + "notallowed" => CursorIcon::NotAllowed, + "contextmenu" => CursorIcon::ContextMenu, + "cell" => CursorIcon::Cell, + "verticaltext" => CursorIcon::VerticalText, + "alias" => CursorIcon::Alias, + "copy" => CursorIcon::Copy, + "nodrop" => CursorIcon::NoDrop, + "grab" => CursorIcon::Grab, + "grabbing" => CursorIcon::Grabbing, + "allscroll" => CursorIcon::AllScroll, + "zoomin" => CursorIcon::ZoomIn, + "zoomout" => CursorIcon::ZoomOut, + "eresize" => CursorIcon::EResize, + "nresize" => CursorIcon::NResize, + "neresize" => CursorIcon::NeResize, + "nwresize" => CursorIcon::NwResize, + "sresize" => CursorIcon::SResize, + "seresize" => CursorIcon::SeResize, + "swresize" => CursorIcon::SwResize, + "wresize" => CursorIcon::WResize, + "ewresize" => CursorIcon::EwResize, + "nsresize" => CursorIcon::NsResize, + "neswresize" => CursorIcon::NeswResize, + "nwseresize" => CursorIcon::NwseResize, + "colresize" => CursorIcon::ColResize, + "rowresize" => CursorIcon::RowResize, + _ => CursorIcon::Default, + }) + } +} + +/// Window size constraints +#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WindowSizeConstraints { + /// The minimum width a window can be, If this is `None`, the window will have no minimum width. + /// + /// The default is `None`. + pub min_width: Option, + /// The minimum height a window can be, If this is `None`, the window will have no minimum height. + /// + /// The default is `None`. + pub min_height: Option, + /// The maximum width a window can be, If this is `None`, the window will have no maximum width. + /// + /// The default is `None`. + pub max_width: Option, + /// The maximum height a window can be, If this is `None`, the window will have no maximum height. + /// + /// The default is `None`. + pub max_height: Option, +} + +/// Do **NOT** implement this trait except for use in a custom [`Runtime`] +/// +/// This trait is separate from [`WindowBuilder`] to prevent "accidental" implementation. +pub trait WindowBuilderBase: std::fmt::Debug + Clone + Sized {} + +/// A builder for all attributes related to a single window. +/// +/// This trait is only meant to be implemented by a custom [`Runtime`] +/// and not by applications. +pub trait WindowBuilder: WindowBuilderBase { + /// Initializes a new window attributes builder. + fn new() -> Self; + + /// Initializes a new window builder from a [`WindowConfig`] + fn with_config(config: &WindowConfig) -> Self; + + /// Show window in the center of the screen. + #[must_use] + fn center(self) -> Self; + + /// The initial position of the window in logical pixels. + #[must_use] + fn position(self, x: f64, y: f64) -> Self; + + /// Window size in logical pixels. + #[must_use] + fn inner_size(self, width: f64, height: f64) -> Self; + + /// Window min inner size in logical pixels. + #[must_use] + fn min_inner_size(self, min_width: f64, min_height: f64) -> Self; + + /// Window max inner size in logical pixels. + #[must_use] + fn max_inner_size(self, max_width: f64, max_height: f64) -> Self; + + /// Window inner size constraints. + #[must_use] + fn inner_size_constraints(self, constraints: WindowSizeConstraints) -> Self; + + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) on creation + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + fn prevent_overflow(self) -> Self; + + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) + /// on creation with a margin + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + fn prevent_overflow_with_margin(self, margin: dpi::Size) -> Self; + + /// Whether the window is resizable or not. + /// When resizable is set to false, native window's maximize button is automatically disabled. + #[must_use] + fn resizable(self, resizable: bool) -> Self; + + /// Whether the window's native maximize button is enabled or not. + /// 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. + #[must_use] + fn maximizable(self, maximizable: bool) -> Self; + + /// Whether the window's native minimize button is enabled or not. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + #[must_use] + fn minimizable(self, minimizable: bool) -> Self; + + /// Whether the window's native close button is enabled or not. + /// + /// ## 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. + #[must_use] + fn closable(self, closable: bool) -> Self; + + /// The title of the window in the title bar. + #[must_use] + fn title>(self, title: S) -> Self; + + /// Whether to start the window in fullscreen or not. + #[must_use] + fn fullscreen(self, fullscreen: bool) -> Self; + + /// Whether the window will be initially focused or not. + #[must_use] + fn focused(self, focused: bool) -> Self; + + /// Whether the window will be focusable or not. + #[must_use] + fn focusable(self, focusable: bool) -> Self; + + /// Whether the window should be maximized upon creation. + #[must_use] + fn maximized(self, maximized: bool) -> Self; + + /// Whether the window should be immediately visible upon creation. + #[must_use] + fn visible(self, visible: bool) -> 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. + /// + /// On Windows, using `no_redirection_bitmap` can help avoid a white flash when + /// creating 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] + fn transparent(self, transparent: bool) -> Self; + + /// Whether the window should have borders and bars. + #[must_use] + fn decorations(self, decorations: bool) -> Self; + + /// Whether the window should always be below other windows. + #[must_use] + fn always_on_bottom(self, always_on_bottom: bool) -> Self; + + /// Whether the window should always be on top of other windows. + #[must_use] + fn always_on_top(self, always_on_top: bool) -> Self; + + /// Whether the window should be visible on all workspaces or virtual desktops. + #[must_use] + fn visible_on_all_workspaces(self, visible_on_all_workspaces: bool) -> Self; + + /// Prevents the window contents from being captured by other apps. + #[must_use] + fn content_protected(self, protected: bool) -> Self; + + /// Sets the window icon. + fn icon(self, icon: Icon) -> crate::Result; + + /// Sets whether or not the window icon should be added to the taskbar. + #[must_use] + fn skip_taskbar(self, skip: bool) -> Self; + + /// Set the window background color. + #[must_use] + fn background_color(self, color: Color) -> Self; + + /// Sets whether or not the window has shadow. + /// + /// ## Platform-specific + /// + /// - **Windows:** + /// - `false` has no effect on decorated window, shadows are always ON. + /// - `true` will make undecorated window have a 1px white border, + /// and on Windows 11, it will have a rounded corners. + /// - **Linux:** Unsupported. + #[must_use] + fn shadow(self, enable: bool) -> Self; + + /// Set an owner to the window to be created. + /// + /// From MSDN: + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// + /// For more information, see + #[cfg(windows)] + #[must_use] + fn owner(self, owner: HWND) -> Self; + + /// Sets a parent to the window to be created. + /// + /// A child window has the WS_CHILD style and is confined to the client area of its parent window. + /// + /// For more information, see + #[cfg(windows)] + #[must_use] + fn parent(self, parent: HWND) -> Self; + + /// Sets a parent to the window to be created. + /// + /// See + #[cfg(target_os = "macos")] + #[must_use] + fn parent(self, parent: *mut std::ffi::c_void) -> Self; + + /// Sets the window to be created transient for parent. + /// + /// See + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn transient_for(self, parent: &impl gtk::glib::IsA) -> Self; + + /// Enables or disables drag and drop support. + #[cfg(windows)] + #[must_use] + fn drag_and_drop(self, enabled: bool) -> Self; + + /// Hide the titlebar. Titlebar buttons will still be visible. + #[cfg(target_os = "macos")] + #[must_use] + fn title_bar_style(self, style: tauri_utils::TitleBarStyle) -> Self; + + /// Change the position of the window controls on macOS. + /// + /// Requires titleBarStyle: Overlay and decorations: true. + #[cfg(target_os = "macos")] + #[must_use] + fn traffic_light_position>(self, position: P) -> Self; + + /// Hide the window title. + #[cfg(target_os = "macos")] + #[must_use] + fn hidden_title(self, hidden: bool) -> Self; + + /// Defines the window [tabbing identifier] for macOS. + /// + /// Windows with matching tabbing identifiers will be grouped together. + /// If the tabbing identifier is not set, automatic tabbing will be disabled. + /// + /// [tabbing identifier]: + #[cfg(target_os = "macos")] + #[must_use] + fn tabbing_identifier(self, identifier: &str) -> Self; + + /// Forces a theme or uses the system settings if None was provided. + fn theme(self, theme: Option) -> Self; + + /// Whether the icon was set or not. + fn has_icon(&self) -> bool; + + fn get_theme(&self) -> Option; + + /// Sets custom name for Windows' window class. **Windows only**. + #[must_use] + fn window_classname>(self, window_classname: S) -> Self; + + /// This sets `WS_EX_NOREDIRECTIONBITMAP`. + /// + /// This can avoid the white flash that may appear before the webview content is rendered + /// when using a transparent window. **Windows only**. + #[must_use] + fn no_redirection_bitmap(self, enable: bool) -> 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. +pub struct PendingWindow> { + /// The label that the window will be named. + pub label: String, + + /// The [`WindowBuilder`] that the window will be created with. + pub window_builder: >::WindowBuilder, + + /// The webview that gets added to the window. Optional in case you want to use child webviews or other window content instead. + pub webview: Option>, +} + +pub fn is_label_valid(label: &str) -> bool { + label + .chars() + .all(|c| char::is_alphanumeric(c) || c == '-' || c == '/' || c == ':' || c == '_') +} + +pub fn assert_label_is_valid(label: &str) { + assert!( + is_label_valid(label), + "Window label must include only alphanumeric characters, `-`, `/`, `:` and `_`." + ); +} + +impl> PendingWindow { + /// Create a new [`PendingWindow`] with a label from the given [`WindowBuilder`]. + pub fn new( + window_builder: >::WindowBuilder, + label: impl Into, + ) -> crate::Result { + let label = label.into(); + if !is_label_valid(&label) { + Err(crate::Error::InvalidWindowLabel) + } else { + Ok(Self { + window_builder, + label, + webview: None, + }) + } + } + + /// Sets a webview to be created on the window. + pub fn set_webview(&mut self, webview: PendingWebview) -> &mut Self { + self.webview.replace(webview); + self + } +} + +/// Identifier of a window. +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd)] +pub struct WindowId(u32); + +impl From for WindowId { + fn from(value: u32) -> Self { + Self(value) + } +} + +/// A window that is not yet managed by Tauri. +#[derive(Debug)] +pub struct DetachedWindow> { + /// The identifier of the window. + pub id: WindowId, + /// Name of the window + pub label: String, + + /// The [`WindowDispatch`] associated with the window. + pub dispatcher: R::WindowDispatcher, + + /// The webview dispatcher in case this window has an attached webview. + pub webview: Option>, +} + +/// A detached webview associated with a window. +#[derive(Debug)] +pub struct DetachedWindowWebview> { + pub webview: DetachedWebview, + pub use_https_scheme: bool, +} + +impl> Clone for DetachedWindowWebview { + fn clone(&self) -> Self { + Self { + webview: self.webview.clone(), + use_https_scheme: self.use_https_scheme, + } + } +} + +impl> Clone for DetachedWindow { + fn clone(&self) -> Self { + Self { + id: self.id, + label: self.label.clone(), + dispatcher: self.dispatcher.clone(), + webview: self.webview.clone(), + } + } +} + +impl> Hash for DetachedWindow { + /// Only use the [`DetachedWindow`]'s label to represent its hash. + fn hash(&self, state: &mut H) { + self.label.hash(state) + } +} + +impl> Eq for DetachedWindow {} +impl> PartialEq for DetachedWindow { + /// Only use the [`DetachedWindow`]'s label to compare equality. + fn eq(&self, other: &Self) -> bool { + self.label.eq(&other.label) + } +} + +/// A raw window type that contains fields to access +/// the HWND on Windows, gtk::ApplicationWindow on Linux +pub struct RawWindow<'a> { + #[cfg(windows)] + pub hwnd: isize, + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub gtk_window: &'a gtk::ApplicationWindow, + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub default_vbox: Option<&'a gtk::Box>, + pub _marker: &'a PhantomData<()>, +} diff --git a/crates/tauri-schema-generator/Cargo.toml b/crates/tauri-schema-generator/Cargo.toml new file mode 100644 index 0000000..8a26909 --- /dev/null +++ b/crates/tauri-schema-generator/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tauri-schema-generator" +version = "0.0.0" +edition = "2024" +publish = false + +[build-dependencies] +tauri-utils = { workspace = true, features = ["schema"] } +schemars = { version = "0.8.22", features = ["url", "preserve_order"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +url = { version = "2", features = ["serde"] } + +[lints] +workspace = true diff --git a/crates/tauri-schema-generator/README.md b/crates/tauri-schema-generator/README.md new file mode 100644 index 0000000..af1c3fb --- /dev/null +++ b/crates/tauri-schema-generator/README.md @@ -0,0 +1,3 @@ +# tauri-schema-generator + +This crate is part of the tauri monorepo workspace and its sole purpose is to generate JSON schema for various tauri config files. diff --git a/crates/tauri-schema-generator/build.rs b/crates/tauri-schema-generator/build.rs new file mode 100644 index 0000000..1cc3036 --- /dev/null +++ b/crates/tauri-schema-generator/build.rs @@ -0,0 +1,58 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{error::Error, path::PathBuf}; + +use serde::Deserialize; +use tauri_utils::{ + acl::{Permission, Scopes, capability::Capability}, + config::Config, + write_if_changed, +}; + +macro_rules! schema { + ($name:literal, $path:ty) => { + (concat!($name, ".schema.json"), schemars::schema_for!($path)) + }; +} + +#[derive(Deserialize)] +pub struct VersionMetadata { + tauri: String, +} + +pub fn main() -> Result<(), Box> { + let schemas = [ + schema!("capability", Capability), + schema!("permission", Permission), + schema!("scope", Scopes), + ]; + + let out = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?); + + let schemas_dir = out.join("schemas"); + std::fs::create_dir_all(&schemas_dir)?; + + for (filename, schema) in schemas { + let schema = serde_json::to_string_pretty(&schema)?; + write_if_changed(schemas_dir.join(filename), &schema)?; + } + + // write config schema file + { + let metadata = include_str!("../tauri-cli/metadata-v2.json"); + let tauri_ver = serde_json::from_str::(metadata)?.tauri; + + // set id for generated schema + let (filename, mut config_schema) = schema!("config", Config); + let schema_metadata = config_schema.schema.metadata.as_mut().unwrap(); + schema_metadata.id = Some(format!("https://schema.tauri.app/config/{tauri_ver}")); + + let config_schema = serde_json::to_string_pretty(&config_schema)?; + write_if_changed(schemas_dir.join(filename), &config_schema)?; + write_if_changed(out.join("../tauri-cli/config.schema.json"), config_schema)?; + } + + Ok(()) +} diff --git a/crates/tauri-schema-generator/schemas/capability.schema.json b/crates/tauri-schema-generator/schemas/capability.schema.json new file mode 100644 index 0000000..0769b1b --- /dev/null +++ b/crates/tauri-schema-generator/schemas/capability.schema.json @@ -0,0 +1,234 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Capability", + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\n It controls application windows' and webviews' fine grained access\n to the Tauri core, application, or plugin commands.\n If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\n This can be done to create groups of windows, based on their required system access, which can reduce\n impact of frontend vulnerabilities in less privileged windows.\n Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`.\n A Window can have none, one, or multiple associated capabilities.\n\n ## Example\n\n ```json\n {\n \"identifier\": \"main-user-files-write\",\n \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\",\n \"windows\": [\n \"main\"\n ],\n \"permissions\": [\n \"core:default\",\n \"dialog:open\",\n {\n \"identifier\": \"fs:allow-write-text-file\",\n \"allow\": [{ \"path\": \"$HOME/test.txt\" }]\n },\n ],\n \"platforms\": [\"macOS\",\"windows\"]\n }\n ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n ## Example\n\n `main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\n It should contain a description of what the grouped permissions should allow.\n\n ## Example\n\n This capability allows the `main` window access to `filesystem` write related\n commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\n This setting is optional and defaults to not being set, as our\n default use case is that the content is served from our local application.\n\n :::caution\n Make sure you understand the security implications of providing remote\n sources with local system access.\n :::\n\n ## Example\n\n ```json\n {\n \"urls\": [\"https://*.mydomain.dev\"]\n }\n ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\n If a window label matches any of the patterns in this list,\n the capability will be enabled on all the webviews of that window,\n regardless of the value of [`Self::webviews`].\n\n On multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`]\n for a fine grained access control.\n\n ## Example\n\n `[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\n The capability will be enabled on all the webviews\n whose label matches any of the patterns in this list,\n regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n ## Example\n\n `[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\n Must include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`.\n For commands directly implemented in the application itself only `${permission-name}`\n is required.\n\n ## Example\n\n ```json\n [\n \"core:default\",\n \"shell:allow-open\",\n \"dialog:open\",\n {\n \"identifier\": \"fs:allow-write-text-file\",\n \"allow\": [{ \"path\": \"$HOME/test.txt\" }]\n }\n ]\n ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\n By default all platforms are targeted.\n\n ## Example\n\n `[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + }, + "definitions": { + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n ## Examples\n\n - \"https://*.mydomain.dev\": allows subdomains of mydomain.dev\n - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`]\n or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ] + }, + "Identifier": { + "type": "string" + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/crates/tauri-schema-generator/schemas/permission.schema.json b/crates/tauri-schema-generator/schemas/permission.schema.json new file mode 100644 index 0000000..b13ba48 --- /dev/null +++ b/crates/tauri-schema-generator/schemas/permission.schema.json @@ -0,0 +1,205 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Permission", + "description": "Descriptions of explicit privileges of commands.\n\n It can enable commands to be accessible in the frontend of the application.\n\n If the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.\n Tauri internal convention is to use `

` headings in markdown content\n for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + }, + "definitions": { + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\n If two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\n It can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command.\n The configured scope is passed to the command and will be enforced by the command implementation.\n\n ## Example\n\n ```json\n {\n \"allow\": [{ \"path\": \"$HOME/**\" }],\n \"deny\": [{ \"path\": \"$HOME/secret.txt\" }]\n }\n ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/crates/tauri-schema-generator/schemas/scope.schema.json b/crates/tauri-schema-generator/schemas/scope.schema.json new file mode 100644 index 0000000..cedbf22 --- /dev/null +++ b/crates/tauri-schema-generator/schemas/scope.schema.json @@ -0,0 +1,84 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Scopes", + "description": "An argument for fine grained behavior control of Tauri commands.\n\n It can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command.\n The configured scope is passed to the command and will be enforced by the command implementation.\n\n ## Example\n\n ```json\n {\n \"allow\": [{ \"path\": \"$HOME/**\" }],\n \"deny\": [{ \"path\": \"$HOME/secret.txt\" }]\n }\n ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + }, + "definitions": { + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + } + } +} \ No newline at end of file diff --git a/crates/tauri-schema-generator/src/main.rs b/crates/tauri-schema-generator/src/main.rs new file mode 100644 index 0000000..b7f6eeb --- /dev/null +++ b/crates/tauri-schema-generator/src/main.rs @@ -0,0 +1,12 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Hosts the schema for the Tauri configuration file. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" +)] + +fn main() {} diff --git a/crates/tauri-schema-worker/.cargo/config.toml b/crates/tauri-schema-worker/.cargo/config.toml new file mode 100644 index 0000000..53f6292 --- /dev/null +++ b/crates/tauri-schema-worker/.cargo/config.toml @@ -0,0 +1,5 @@ +[profile.release] +opt-level = 3 +# Fails with 'externref table required for catch wrappers' if we set it to `strip = true` +# see https://github.com/cloudflare/workers-rs/issues/1014 +strip = "debuginfo" diff --git a/crates/tauri-schema-worker/.gitignore b/crates/tauri-schema-worker/.gitignore new file mode 100644 index 0000000..b9515a5 --- /dev/null +++ b/crates/tauri-schema-worker/.gitignore @@ -0,0 +1,4 @@ +/target +/node_modules +/.wrangler +/build diff --git a/crates/tauri-schema-worker/Cargo.toml b/crates/tauri-schema-worker/Cargo.toml new file mode 100644 index 0000000..7c407c2 --- /dev/null +++ b/crates/tauri-schema-worker/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tauri-schema-worker" +version = "0.0.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +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" +semver = { version = "1", features = ["serde"] } +serde = { version = "1", features = ["derive"] } +anyhow = "1" + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(wasm_bindgen_unstable_test_coverage)', +] } + +# https://github.com/rustwasm/wasm-pack/issues/1501 +[package.metadata.wasm-pack.profile.release] +wasm-opt = ["--enable-bulk-memory", "--enable-nontrapping-float-to-int"] diff --git a/crates/tauri-schema-worker/README.md b/crates/tauri-schema-worker/README.md new file mode 100644 index 0000000..288b74c --- /dev/null +++ b/crates/tauri-schema-worker/README.md @@ -0,0 +1,3 @@ +# schema.tauri.app worker + +Source code for `https://schema.tauri.app` cloudflare worker. diff --git a/crates/tauri-schema-worker/package.json b/crates/tauri-schema-worker/package.json new file mode 100644 index 0000000..33b85c0 --- /dev/null +++ b/crates/tauri-schema-worker/package.json @@ -0,0 +1,13 @@ +{ + "name": "tauri-schema-worker", + "version": "0.0.0", + "license": "Apache-2.0 OR MIT", + "private": "true", + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev" + }, + "devDependencies": { + "wrangler": "^4.132.0" + } +} diff --git a/crates/tauri-schema-worker/src/config.rs b/crates/tauri-schema-worker/src/config.rs new file mode 100644 index 0000000..5dc5855 --- /dev/null +++ b/crates/tauri-schema-worker/src/config.rs @@ -0,0 +1,195 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// axum Error is too large +#![allow(clippy::result_large_err)] + +use anyhow::Context; +use axum::{ + Router, + extract::Path, + http::{HeaderValue, StatusCode, header}, + response::{IntoResponse, Result}, + routing::get, +}; +use semver::{Version, VersionReq}; +use serde::Deserialize; +use worker::*; + +#[derive(Deserialize)] +pub struct CrateReleases { + pub versions: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct CrateRelease { + #[serde(alias = "num")] + pub version: Version, + pub yanked: Option, +} + +#[derive(Deserialize)] +pub struct CrateMetadataFull { + #[serde(rename = "crate")] + pub crate_: CrateMetadata, +} + +#[derive(Deserialize)] +pub struct CrateMetadata { + pub max_stable_version: Version, +} + +const USERAGENT: &str = "tauri-schema-worker (contact@tauri.app)"; + +pub fn router() -> Router { + Router::new() + .route("/config", get(stable_schema)) + .route("/config/latest", get(stable_schema)) + .route("/config/stable", get(stable_schema)) + .route("/config/next", get(next_schema)) // pre-releases versions, (rc, alpha and beta) + .route("/config/{version}", get(schema_for_version)) +} + +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 { + 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 { + try_next_schema() + .await + .map(JsonResponse) + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())) + .map_err(Into::into) +} + +#[worker::send] +async fn try_schema_for_version(version: String) -> anyhow::Result { + let version = version.parse::()?; + + let releases = crate_releases("tauri").await?; + + if releases.is_empty() { + return try_stable_schema().await; + } + + let Some(version) = releases.into_iter().find(|r| version.matches(&r.version)) else { + return try_stable_schema().await; + }; + + schema_file_for_version(version.version).await +} + +#[worker::send] +async fn try_stable_schema() -> anyhow::Result { + let max = stable_version("tauri").await?; + schema_file_for_version(max).await +} + +#[worker::send] +async fn try_next_schema() -> anyhow::Result { + let releases = crate_releases("tauri").await?; + let version = releases + .into_iter() + .filter(|r| !r.version.pre.is_empty()) + .map(|r| r.version) + .max() + .context("Couldn't find latest pre-release")?; + schema_file_for_version(version).await +} + +async fn schema_file_for_version(version: Version) -> anyhow::Result { + let cache = Cache::open("schema".to_string()).await; + let cache_key = format!("https://schema.tauri.app/config/{version}"); + if let Some(mut cached) = cache.get(cache_key.clone(), true).await? { + console_log!("Serving schema for {version} from cache"); + return cached.text().await.map_err(Into::into); + } + + console_log!("Fetching schema for {version} from remote"); + + let path = if version.major >= 2 { + "crates/tauri-schema-generator/schemas/config.schema.json" + } else { + "core/tauri-config-schema/schema.json" + }; + let url = format!("https://raw.githubusercontent.com/tauri-apps/tauri/tauri-v{version}/{path}"); + let mut res = Fetch::Request(fetch_req(&url)?).send().await?; + + cache.put(cache_key, res.cloned()?).await?; + + res.text().await.map_err(Into::into) +} + +async fn crate_releases(crate_: &str) -> anyhow::Result> { + let url = format!("https://crates.io/api/v1/crates/{crate_}/versions"); + let mut res = Fetch::Request(fetch_req(&url)?).send().await?; + + let versions: CrateReleases = res.json().await?; + let versions = versions.versions; + + let flt = |r: &CrateRelease| r.yanked == Some(false); + Ok(versions.into_iter().filter(flt).collect()) +} + +async fn stable_version(crate_: &str) -> anyhow::Result { + let url = format!("https://crates.io/api/v1/crates/{crate_}"); + let mut res = Fetch::Request(fetch_req(&url)?).send().await?; + let metadata: CrateMetadataFull = res.json().await?; + Ok(metadata.crate_.max_stable_version) +} + +fn fetch_req(url: &str) -> anyhow::Result { + let headers = Headers::new(); + headers.append(header::USER_AGENT.as_str(), USERAGENT)?; + + worker::Request::new_with_init( + url, + &RequestInit { + method: Method::Get, + headers, + cf: CfProperties { + cache_ttl: Some(86400), + cache_everything: Some(true), + cache_ttl_by_status: Some( + [ + ("200-299".to_string(), 86400), + ("404".to_string(), 1), + ("500-599".to_string(), 0), + ] + .into(), + ), + ..Default::default() + }, + ..Default::default() + }, + ) + .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/src/lib.rs b/crates/tauri-schema-worker/src/lib.rs new file mode 100644 index 0000000..f10397e --- /dev/null +++ b/crates/tauri-schema-worker/src/lib.rs @@ -0,0 +1,27 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use axum::{Router, routing::get}; +use tower_service::Service; +use worker::*; + +mod config; + +#[worker::event(fetch)] +async fn main( + req: HttpRequest, + _env: Env, + _ctx: Context, +) -> worker::Result> { + console_error_panic_hook::set_once(); + Ok(router().call(req).await?) +} + +fn router() -> Router { + Router::new().route("/", get(root)).merge(config::router()) +} + +async fn root() -> &'static str { + "tauri schema worker" +} diff --git a/crates/tauri-schema-worker/wrangler.toml b/crates/tauri-schema-worker/wrangler.toml new file mode 100644 index 0000000..e7e56a6 --- /dev/null +++ b/crates/tauri-schema-worker/wrangler.toml @@ -0,0 +1,16 @@ +# Copyright 2019-2022 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name = "tauri-schema" +main = "build/worker/shim.mjs" +compatibility_date = "2023-08-23" +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.8 && worker-build --release" + +[observability] +enabled = true +head_sampling_rate = 1 diff --git a/crates/tauri-utils/CHANGELOG.md b/crates/tauri-utils/CHANGELOG.md new file mode 100644 index 0000000..0a41224 --- /dev/null +++ b/crates/tauri-utils/CHANGELOG.md @@ -0,0 +1,1089 @@ +# Changelog + +## \[2.9.3] + +### Enhancements + +- [`c2b8f4783`](https://www.github.com/tauri-apps/tauri/commit/c2b8f4783217d4a8a9e22b2d333727f4de2c57b0) ([#15373](https://www.github.com/tauri-apps/tauri/pull/15373) by [@MavenRain](https://www.github.com/tauri-apps/tauri/../../MavenRain)) Improve diagnostics for invalid plugin and permission identifiers. + + The `Identifier` deserializer now wraps the inner error with the offending identifier string so the message reads `invalid plugin or permission identifier '': ...`, surfacing the bad entry without requiring a grep through the file. + + The previous parse failure (`failed to parse JSON: identifiers can only include lowercase ASCII, hyphens which are not leading or trailing, and a single colon if using a prefix at line 16 column 23`) now reads `failed to parse JSON: invalid plugin or permission identifier 'sqlite_proxy:allow-foo': identifiers can only include lowercase ASCII, hyphens which are not leading or trailing, and a single colon if using a prefix at line 16 column 23`. + +### Bug Fixes + +- [`2783e6079`](https://www.github.com/tauri-apps/tauri/commit/2783e60793cf4d911ca89d950047817ee3367dfb) ([#15481](https://www.github.com/tauri-apps/tauri/pull/15481) by [@thanhtoantnt](https://www.github.com/tauri-apps/tauri/../../thanhtoantnt)) Fix `Number::Int` being silently coerced to `Number::Float` on `serde_json` round-trip. + + `From for Value` was checking `as_f64()` first, which succeeds for every integer that fits in an f64, so integer JSON numbers were always deserialized as `Number::Float`. The check order is now `as_i64()` → `as_u64()` (cast to `i64`, wrapping for values above `i64::MAX`) → `as_f64()`, matching serde_json's own visitor convention. + +## \[2.9.2] + +### Bug Fixes + +- [`b5b72ce51`](https://www.github.com/tauri-apps/tauri/commit/b5b72ce51811e9f95b1f7e9a05ea19c8f12ce694) ([#15383](https://www.github.com/tauri-apps/tauri/pull/15383) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix a regression in tauri-utils 2.8.3 that made empty path an invalid resource target, e.g. + + ```json + { + "bundle": { + "resources": { + "README.md": "", + } + } + } + ``` + + (this means `README.md` -> `$RESOURCE/README.md`, note this is a confusing behavior, and will be changed in v3) +- [`3fd8ba2c0`](https://www.github.com/tauri-apps/tauri/commit/3fd8ba2c022717068ff6a154ce12942c3a672232) ([#15388](https://www.github.com/tauri-apps/tauri/pull/15388) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix a regression in tauri-utils 2.8.3 that made an empty directory makes it skip all the following entries, e.g. + + ```json + { + "bundle": { + "resources": [ + "empty-directory", + "README.md" + ] + } + } + ``` + + if `empty-directory` is empty, the `README.md` will not be copied to the resource directory (skipped) + +## \[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 + +- [`7b16dafb1`](https://www.github.com/tauri-apps/tauri/commit/7b16dafb1dc417536ebb62df8eb66154dd97109d) ([#14986](https://www.github.com/tauri-apps/tauri/pull/14986) by [@montyc1999](https://www.github.com/tauri-apps/tauri/../../montyc1999)) Sort csp/plugin/header configs when generating HashMap constructors so that `generate_context!` is deterministic. + + See: https://github.com/tauri-apps/tauri/issues/14978 for more information +- [`c3cbff3f7`](https://www.github.com/tauri-apps/tauri/commit/c3cbff3f7430161715f80f82128b345a6f7140c9) ([#14662](https://www.github.com/tauri-apps/tauri/pull/14662) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix resource path handles `./` path differently (e.g. `./some-folder` should be the same as `some-folder`) + +## \[2.8.2] + +### Enhancements + +- [`2d28e3143`](https://www.github.com/tauri-apps/tauri/commit/2d28e3143ee3d97d7570ea03877aa00a0d6e47d0) ([#14632](https://www.github.com/tauri-apps/tauri/pull/14632) by [@sftse](https://www.github.com/tauri-apps/tauri/../../sftse)) Small code refactors for improved code readability. No user facing changes. + +## \[2.8.1] + +### Bug Fixes + +- [`1573c7240`](https://www.github.com/tauri-apps/tauri/commit/1573c72402352949d1fd3ca5c6fdbee46fe69fbb) ([#14561](https://www.github.com/tauri-apps/tauri/pull/14561) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Fixed an issue that caused schema files to have `\r` characters on Windows. + +## \[2.8.0] + +### New Features + +- [`f5851ee00`](https://www.github.com/tauri-apps/tauri/commit/f5851ee00d6d1f4d560a220ca5a728fedd525092) ([#14089](https://www.github.com/tauri-apps/tauri/pull/14089)) Adds the `scrollBarStyle` option to the window configuration. +- [`3b4fac201`](https://www.github.com/tauri-apps/tauri/commit/3b4fac2017832d426dd07c5e24e26684eda57f7b) ([#14194](https://www.github.com/tauri-apps/tauri/pull/14194)) Add `tauri.conf.json > bundle > android > autoIncrementVersionCode` config option to automatically increment the Android version code. +- [`3d6868d09`](https://www.github.com/tauri-apps/tauri/commit/3d6868d09c323d68a152f3c3f8c7256311bd020a) ([#14128](https://www.github.com/tauri-apps/tauri/pull/14128)) Added `FileAssociation::exported_type` and `FileAssociation::content_types` for better support to defining custom types on macOS. +- [`ed7c9a410`](https://www.github.com/tauri-apps/tauri/commit/ed7c9a4100e08c002212265549d12130d021ad1e) ([#14108](https://www.github.com/tauri-apps/tauri/pull/14108)) Added `bundle > macOS > infoPlist` and `bundle > iOS > infoPlist` configurations to allow defining custom Info.plist extensions. +- [`7b0d4e732`](https://www.github.com/tauri-apps/tauri/commit/7b0d4e73227e42d88732b6d9fe643499dd78ec4e) ([#14265](https://www.github.com/tauri-apps/tauri/pull/14265)) Added `html::normalize_script_for_csp`. +- [`cc8c0b531`](https://www.github.com/tauri-apps/tauri/commit/cc8c0b53171173dbd1d01781a50de1a3ea159031) ([#14031](https://www.github.com/tauri-apps/tauri/pull/14031)) Added support to universal app links on macOS with the `plugins > deep-link > desktop > domains` configuration. + +### Enhancements + +- [`59089723f`](https://www.github.com/tauri-apps/tauri/commit/59089723fc20d66f3f305f2008adeb279bf87462) ([#14091](https://www.github.com/tauri-apps/tauri/pull/14091)) Added a config to set a data_directory relative to the app-specific data dir in JavaScript and `tauri.conf.json`. + +## \[2.7.0] + +### New Features + +- [`91508c0b8`](https://www.github.com/tauri-apps/tauri/commit/91508c0b8d16ec61c7706e93b711c5a85aaffb4a) ([#13881](https://www.github.com/tauri-apps/tauri/pull/13881) by [@pepperoni505](https://www.github.com/tauri-apps/tauri/../../pepperoni505)) Introduces a new configuration option that allows you to specify custom folders to watch for changes when running `tauri dev`. +- [`0c402bfb6`](https://www.github.com/tauri-apps/tauri/commit/0c402bfb6bd0bec24d928fcabe2ffef1f5cff19a) ([#13997](https://www.github.com/tauri-apps/tauri/pull/13997) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Increase default iOS deployment target iOS to 14.0. + +### Enhancements + +- [`9300b59f6`](https://www.github.com/tauri-apps/tauri/commit/9300b59f65156a37f7bf9a629b69da6761ae734a) ([#13787](https://www.github.com/tauri-apps/tauri/pull/13787) by [@JakenHerman](https://www.github.com/tauri-apps/tauri/../../JakenHerman)) Added `fips_compliant` field to `WixConfig` so that it can be configured via `tauri.conf.json` as well. +- [`72b4226ee`](https://www.github.com/tauri-apps/tauri/commit/72b4226ee9932b4dafa4837a49420b2c02d14bb7) ([#13809](https://www.github.com/tauri-apps/tauri/pull/13809) by [@Beanow](https://www.github.com/tauri-apps/tauri/../../Beanow)) Reduced `Debug` format size for binary buffers. + +### Bug Fixes + +- [`1a3d1a024`](https://www.github.com/tauri-apps/tauri/commit/1a3d1a024ebb2a3dd5d15849523a55246d78fda6) ([#13995](https://www.github.com/tauri-apps/tauri/pull/13995) by [@will3942](https://www.github.com/tauri-apps/tauri/../../will3942)) Fix Tauri iOS build with binary XCFramework dependencies, allows extracting binaryTargets that are zipped and also not including XCFrameworks when linking. + +## \[2.6.0] + +### New Features + +- [`232265c70`](https://www.github.com/tauri-apps/tauri/commit/232265c70e1c213bbb3f84b5541ddc07d330fce1) ([#13209](https://www.github.com/tauri-apps/tauri/pull/13209) by [@kandrelczyk](https://www.github.com/tauri-apps/tauri/../../kandrelczyk)) Added `platform::bundle_type`. +- [`33d079392`](https://www.github.com/tauri-apps/tauri/commit/33d079392ac4a5a153b7d8a6d82fefd6f54a2bdf) ([#13811](https://www.github.com/tauri-apps/tauri/pull/13811) by [@mhbagheri-99](https://www.github.com/tauri-apps/tauri/../../mhbagheri-99)) Allow runner configuration to be an object with cmd, cwd, and args properties. The runner can now be configured as `{ "cmd": "my_runner", "cwd": "/path", "args": ["--quiet"] }` while maintaining backwards compatibility with the existing string format. + +## \[2.5.0] + +### New Features + +- [`414619c36`](https://www.github.com/tauri-apps/tauri/commit/414619c36e94e21939534dd72c0438b93da75546) ([#13536](https://www.github.com/tauri-apps/tauri/pull/13536) by [@Tunglies](https://www.github.com/tauri-apps/tauri/../../Tunglies)) Added support for the `bundleName` property in the macOS bundler configuration. This allows specifying the `CFBundleName` value for generated macOS bundles. +- [`09c19932d`](https://www.github.com/tauri-apps/tauri/commit/09c19932d2ddf05f28bcdc73796a966532e7ca1c) ([#13304](https://www.github.com/tauri-apps/tauri/pull/13304) by [@39zde](https://www.github.com/tauri-apps/tauri/../../39zde)) Adds the option to configure the HTTP `Service-Worker-Allowed` response header in `app > security > headers` + +### Bug Fixes + +- [`eb3f0248c`](https://www.github.com/tauri-apps/tauri/commit/eb3f0248c25f1d259cf0fa9448b3c7c2b75b24a7) ([#13646](https://www.github.com/tauri-apps/tauri/pull/13646) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Only write `This default permission set includes the following` to the reference if the default permission set is not empty +- [`c8a30a61d`](https://www.github.com/tauri-apps/tauri/commit/c8a30a61d20552b43afd6f21cb66d18185314148) ([#13476](https://www.github.com/tauri-apps/tauri/pull/13476) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix capability filtering via `tauri.conf.json > app > security > capabilities` not working when generating allowed commands. +- [`b52da29d5`](https://www.github.com/tauri-apps/tauri/commit/b52da29d5dbdb675ddba438a335e6a59f620e536) ([#13429](https://www.github.com/tauri-apps/tauri/pull/13429) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix `mainBinaryName` doesn't work when there's `.` in it + +### What's Changed + +- [`168629646`](https://www.github.com/tauri-apps/tauri/commit/168629646335f24cc7f1c4a61df22688b2198f98) ([#13418](https://www.github.com/tauri-apps/tauri/pull/13418) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Put dynamic ACL into a feature `dynamic-acl`, this is currently enabled by default to align with the previous behaviors, you can disable it through `default-features = false` to reduce the final binary size by not including the ACL references + +### Dependencies + +- [`9c16eefa3`](https://www.github.com/tauri-apps/tauri/commit/9c16eefa319b4697bac1d1019bbb5f93eca63173) ([#13629](https://www.github.com/tauri-apps/tauri/pull/13629) by [@sftse](https://www.github.com/tauri-apps/tauri/../../sftse)) Update html5ever to 0.29 and kuchikiki to version 0.8.8-speedreader. + +### Breaking Changes + +- [`b7cdb3b39`](https://www.github.com/tauri-apps/tauri/commit/b7cdb3b39ef7e84773ce9312535825801350fa20) ([#13410](https://www.github.com/tauri-apps/tauri/pull/13410) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Feature gated the HTML manipulation code in `tauri-utils` behined a flag to reduce compile time + +## \[2.4.0] + +### New Features + +- [`ea36294cb`](https://www.github.com/tauri-apps/tauri/commit/ea36294cbca98f7725c91d1464fd92e77c89698a) ([#13208](https://www.github.com/tauri-apps/tauri/pull/13208)) Added `disableInputAccessoryView: bool` config for iOS. +- [`0aa48fb9e`](https://www.github.com/tauri-apps/tauri/commit/0aa48fb9e4b9d7b5bf3522000a76ebc1836394ed) ([#13030](https://www.github.com/tauri-apps/tauri/pull/13030)) Added `bundleVersion` to iOS and macOS configuration to support specifying a `CFBundleVersion`. +- [`b072e2b29`](https://www.github.com/tauri-apps/tauri/commit/b072e2b2967640ae4fa1af466ae878c156551edd) ([#9687](https://www.github.com/tauri-apps/tauri/pull/9687)) Add `preventOverflow` config option to prevent the window from overflowing the monitor size on creation + +### Bug Fixes + +- [`2dccfab53`](https://www.github.com/tauri-apps/tauri/commit/2dccfab5321fef55d45f3a4c674b6151b1c4424a) ([#13236](https://www.github.com/tauri-apps/tauri/pull/13236)) Fix `fileAssociations` missing `LSHandlerRank` on macOS. + +## \[2.3.1] + +### Enhancements + +- [`a851b6597`](https://www.github.com/tauri-apps/tauri/commit/a851b6597f7e37d12f9e4632945e8466800eb5ff) ([#13057](https://www.github.com/tauri-apps/tauri/pull/13057) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Enhanced the description of generated docs and schema for permission sets to include list of permissions within. + +### Bug Fixes + +- [`4ae14bf2f`](https://www.github.com/tauri-apps/tauri/commit/4ae14bf2f20546c0990c48dc465832ffc46a7247) ([#13093](https://www.github.com/tauri-apps/tauri/pull/13093) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Suppress deprecated warning in `TrayIconConfig`'s codegen + +## \[2.3.0] + +### New Features + +- [`013f8f652`](https://www.github.com/tauri-apps/tauri/commit/013f8f652302f2d49c5ec0a075582033d8b074fb) ([#12890](https://www.github.com/tauri-apps/tauri/pull/12890) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Added `build > removeUnusedCommands` to trigger the build scripts and macros to remove unused commands based on the capabilities you defined. Note this won't be accounting for dynamically added ACLs so make sure to check it when using this. +- [`30f5a1553`](https://www.github.com/tauri-apps/tauri/commit/30f5a1553d3c0ce460c9006764200a9210915a44) ([#12366](https://www.github.com/tauri-apps/tauri/pull/12366) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Added `trafficLightPosition` window configuration to set the traffic light buttons position on macOS. + +### Enhancements + +- [`f981a5ee8`](https://www.github.com/tauri-apps/tauri/commit/f981a5ee8b292b9ea09329f60cecc7f688dda734) ([#12602](https://www.github.com/tauri-apps/tauri/pull/12602) by [@kxxt](https://www.github.com/tauri-apps/tauri/../../kxxt)) Add basic support for linux riscv64 platform. + +### Bug Fixes + +- [`3cc4ad3c3`](https://www.github.com/tauri-apps/tauri/commit/3cc4ad3c381ee081cfcf0df28ea51507ad5c9b95) ([#12884](https://www.github.com/tauri-apps/tauri/pull/12884) by [@oscartbeaumont](https://www.github.com/tauri-apps/tauri/../../oscartbeaumont)) fix: allow double `--` in permissions as command can have double `__` + +### Performance Improvements + +- [`1cd8f55ee`](https://www.github.com/tauri-apps/tauri/commit/1cd8f55eed326d61860fee62ba2d2f4464bdcfcc) ([#13033](https://www.github.com/tauri-apps/tauri/pull/13033) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Don't ship global `bundle.global.js` if `app > withGlobalTauri` is set to false + +## \[2.2.0] + +### Enhancements + +- [`a2d36b8c3`](https://www.github.com/tauri-apps/tauri/commit/a2d36b8c34a8dcfc6736797ca5cd4665faf75e7e) ([#12181](https://www.github.com/tauri-apps/tauri/pull/12181) by [@bastiankistner](https://www.github.com/tauri-apps/tauri/../../bastiankistner)) Add an option to change the default background throttling policy (currently for WebKit only). + +## \[2.1.1] + +### Bug Fixes + +- [`46935212b`](https://www.github.com/tauri-apps/tauri/commit/46935212b61da44dc82dfeb803fceebf5659f7b7) ([#11658](https://www.github.com/tauri-apps/tauri/pull/11658) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix `.json5` capability files not recognized even with `config-json5` feature enabled + +## \[2.1.0] + +### New Features + +- [`fabc2f283`](https://www.github.com/tauri-apps/tauri/commit/fabc2f283e38b62c721326e44645d47138418cbc) ([#11485](https://www.github.com/tauri-apps/tauri/pull/11485) by [@39zde](https://www.github.com/tauri-apps/tauri/../../39zde)) Adds a new configuration option `app > security > headers` to define headers that will be added to every http response from tauri to the web view. This doesn't include IPC messages and error responses. +- [`4d545ab3c`](https://www.github.com/tauri-apps/tauri/commit/4d545ab3ca228c8a21b966b709f84a0da2864479) ([#11486](https://www.github.com/tauri-apps/tauri/pull/11486) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Added `Window::set_background_color` and `WindowBuilder::background_color`. +- [`cbc095ec5`](https://www.github.com/tauri-apps/tauri/commit/cbc095ec5fe7de29b5c9265576d4e071ec159c1c) ([#11451](https://www.github.com/tauri-apps/tauri/pull/11451) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `app > windows > devtools` config option and when creating the webview from JS, to enable or disable devtools for a specific webview. +- [`058c0db72`](https://www.github.com/tauri-apps/tauri/commit/058c0db72f43fbe1574d0db654560e693755cd7e) ([#11584](https://www.github.com/tauri-apps/tauri/pull/11584) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `bundle > linux > rpm > compression` config option to control RPM bundle compression type and level. +- [`f37e97d41`](https://www.github.com/tauri-apps/tauri/commit/f37e97d410c4a219e99f97692da05ca9d8e0ba3a) ([#11477](https://www.github.com/tauri-apps/tauri/pull/11477) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `app > windows > useHttpsScheme` config option to choose whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android +- [`2a75c64b5`](https://www.github.com/tauri-apps/tauri/commit/2a75c64b5431284e7340e8743d4ea56a62c75466) ([#11469](https://www.github.com/tauri-apps/tauri/pull/11469) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Added `app > windows > windowClassname` config option to specify the name of the window class on Windows. + +### Enhancements + +- [`c33bbf457`](https://www.github.com/tauri-apps/tauri/commit/c33bbf45740274b6918ea6c647f366fb6008e459) ([#11575](https://www.github.com/tauri-apps/tauri/pull/11575) by [@kornelski](https://www.github.com/tauri-apps/tauri/../../kornelski)) Include the path in ACL I/O errors. + +### Bug Fixes + +- [`378142914`](https://www.github.com/tauri-apps/tauri/commit/37814291475814b4a24cc77b6fa457ec9ba7a779) ([#11429](https://www.github.com/tauri-apps/tauri/pull/11429) by [@griffi-gh](https://www.github.com/tauri-apps/tauri/../../griffi-gh)) Enhance resource directory resolution to support running on distros like NixOS. + +## \[2.0.2] + +### New Features + +- Add `bundler > windows > wix > version` to manually specify a wix-compatible version. + +## \[2.0.1] + +### What's Changed + +- [`0ab2b3306`](https://www.github.com/tauri-apps/tauri/commit/0ab2b330644b6419f6cee1d5377bfb5cdda2ccf9) ([#11205](https://www.github.com/tauri-apps/tauri/pull/11205) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7. + +## \[2.0.0] + +### What's Changed + +- [`382ed482b`](https://www.github.com/tauri-apps/tauri/commit/382ed482bd08157c39e62f9a0aaad8802f1092cb) Bump MSRV to 1.78. +- [`637285790`](https://www.github.com/tauri-apps/tauri/commit/6372857905ae9c0aedb7f482ddf6cf9f9836c9f2) Promote to v2 stable! + +## \[2.0.0-rc.13] + +### New Features + +- [`a247170e1`](https://www.github.com/tauri-apps/tauri/commit/a247170e1f620a9b012274b11cfe51e90327d6e9) ([#11056](https://www.github.com/tauri-apps/tauri/pull/11056) by [@SpikeHD](https://www.github.com/tauri-apps/tauri/../../SpikeHD)) Expose the ability to enabled browser extensions in WebView2 on Windows. +- [`f57a729cd`](https://www.github.com/tauri-apps/tauri/commit/f57a729cd8f7e10d8daf0b9d5b85f9c7ad530496) ([#11039](https://www.github.com/tauri-apps/tauri/pull/11039) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `upgradeCode` in `wix` configuration to set an upgrade code for your MSI installer. This is recommended to be set if you plan to change your `productName`. + +### Bug Fixes + +- [`1efa5e718`](https://www.github.com/tauri-apps/tauri/commit/1efa5e7184009537b688a395596c696173ae0231) ([#11099](https://www.github.com/tauri-apps/tauri/pull/11099) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Rerun build script if the platform-specific configuration file changes. + +## \[2.0.0-rc.12] + +### New Features + +- [`ad294d274`](https://www.github.com/tauri-apps/tauri/commit/ad294d274dd81d2ef91ed73af9163b6e9b8eb964) ([#11032](https://www.github.com/tauri-apps/tauri/pull/11032) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `app > windows > create` option to choose whether to create this window at app startup or not. + +## \[2.0.0-rc.11] + +### New Features + +- [`35bd9dd3d`](https://www.github.com/tauri-apps/tauri/commit/35bd9dd3dc3d8972bbc4aa5f4a6c6fa14354e9bf) ([#10977](https://www.github.com/tauri-apps/tauri/pull/10977) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `mainBinaryName` config option to set the file name for the main binary. + +## \[2.0.0-rc.10] + +### Bug Fixes + +- [`0a47bf043`](https://www.github.com/tauri-apps/tauri/commit/0a47bf04302ca8502d3da21b3bc27818720fe34a) ([#10946](https://www.github.com/tauri-apps/tauri/pull/10946) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Fixed an issue that made the `identifier` in `tauri.conf.json` optional while it was actually required. + +## \[2.0.0-rc.9] + +### Dependencies + +- [`d9c8d3cc8`](https://www.github.com/tauri-apps/tauri/commit/d9c8d3cc8d5ca67cd767ffc7a521f801b41ce201) ([#10902](https://www.github.com/tauri-apps/tauri/pull/10902) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Update infer to 0.16, tray icon to 0.17, urlpattern to 0.3, image to 0.25 + +### Breaking Changes + +- [`faa259bac`](https://www.github.com/tauri-apps/tauri/commit/faa259bacf1ace670af763982c6903190faf163a) ([#10907](https://www.github.com/tauri-apps/tauri/pull/10907) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) The `Assets::iter` function now must return a iterator with `Item = (Cow<'_, str>, Cow<'_, [u8]>)` to be more flexible on contexts where the assets are not `'static`. + +## \[2.0.0-rc.8] + +### Enhancements + +- [`f0acf504a`](https://www.github.com/tauri-apps/tauri/commit/f0acf504a2a972c063188a00143d8bf2b887691d) ([#10858](https://www.github.com/tauri-apps/tauri/pull/10858) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Validate duplicate capability identifier. + +## \[2.0.0-rc.7] + +### New Features + +- [`58dda44a5`](https://www.github.com/tauri-apps/tauri/commit/58dda44a59b915f091602cdfc53385a148469793) ([#10339](https://www.github.com/tauri-apps/tauri/pull/10339) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Add a new option `minimumWebview2Version` for Windows NSIS installer to trigger a webview2 update if the user's webview2 is older than this version + +### Bug Fixes + +- [`03f2a5098`](https://www.github.com/tauri-apps/tauri/commit/03f2a50981b8c01b1c196811fce9d93f1bf0820d) ([#10718](https://www.github.com/tauri-apps/tauri/pull/10718) by [@rdlabo](https://www.github.com/tauri-apps/tauri/../../rdlabo)) Update swift-rs fixing a plugin build when native dependencies are used. + +### Breaking Changes + +- [`073bb4f45`](https://www.github.com/tauri-apps/tauri/commit/073bb4f459a923541b94970dfa7e087bccaa2cfd) ([#10772](https://www.github.com/tauri-apps/tauri/pull/10772) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Removed the deprecated `webview_fixed_runtime_path` config option, use the `webview_install_mode` instead. + +## \[2.0.0-rc.6] + +### Bug Fixes + +- [`9bcff3cd7`](https://www.github.com/tauri-apps/tauri/commit/9bcff3cd7997fe13425a21b577f93317831f77fa) ([#10703](https://www.github.com/tauri-apps/tauri/pull/10703) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Properly remove isolation script on Android. + +### What's Changed + +- [`f4d5241b3`](https://www.github.com/tauri-apps/tauri/commit/f4d5241b377d0f7a1b58100ee19f7843384634ac) ([#10731](https://www.github.com/tauri-apps/tauri/pull/10731) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Update documentation icon path. + +## \[2.0.0-rc.5] + +### Bug Fixes + +- [`da381e07f`](https://www.github.com/tauri-apps/tauri/commit/da381e07f3770988fe6d0859a02331b87cc6723f) ([#10696](https://www.github.com/tauri-apps/tauri/pull/10696) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Implemented `resource_dir` on Android, which returns a URI that needs to be resolved using [AssetManager::open](https://developer.android.com/reference/android/content/res/AssetManager#open\(java.lang.String,%20int\)). This will be handled by the file system plugin. +- [`da381e07f`](https://www.github.com/tauri-apps/tauri/commit/da381e07f3770988fe6d0859a02331b87cc6723f) ([#10696](https://www.github.com/tauri-apps/tauri/pull/10696) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix `resource_dir` on iOS. + +## \[2.0.0-rc.4] + +### New Features + +- [`8d148a9e2`](https://www.github.com/tauri-apps/tauri/commit/8d148a9e2566edebfea2d75f32df7c9396d765a4) ([#10634](https://www.github.com/tauri-apps/tauri/pull/10634) by [@anatawa12](https://www.github.com/tauri-apps/tauri/../../anatawa12)) Custom sign command with object notation for whitespaces in the command path and arguments. + +### Bug Fixes + +- [`5c335ae9a`](https://www.github.com/tauri-apps/tauri/commit/5c335ae9ad88e46c2135a557390f6e808c9a6088) ([#10648](https://www.github.com/tauri-apps/tauri/pull/10648) by [@Flakebi](https://www.github.com/tauri-apps/tauri/../../Flakebi)) Prevent build script from rerunning unnecessarily by only writing files when the content changes. + +## \[2.0.0-rc.3] + +### Enhancements + +- [`0bb7b0f35`](https://www.github.com/tauri-apps/tauri/commit/0bb7b0f352960fb5111a40157c0953d19e76f1fd) ([#10559](https://www.github.com/tauri-apps/tauri/pull/10559) by [@Norbiros](https://www.github.com/tauri-apps/tauri/../../Norbiros)) Return autogenerated permissions from `autogenerate_command_permissions`. + +### Bug Fixes + +- [`9e891933d`](https://www.github.com/tauri-apps/tauri/commit/9e891933d8ac7a67e37770a149d0a5dd385ee625) ([#10293](https://www.github.com/tauri-apps/tauri/pull/10293) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Fix `ResourcePaths` iterator returning an unexpected result for mapped resources, for example `"../resources/user.json": "resources/user.json"` generates this resource `resources/user.json/user.json` where it should generate just `resources/user.json`. +- [`9fe846615`](https://www.github.com/tauri-apps/tauri/commit/9fe846615b7c4f310f07897ded881c239e3df30a) ([#10547](https://www.github.com/tauri-apps/tauri/pull/10547) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix plugin permissions documentation heading for permissions table. + +### Dependencies + +- [`0afee5ed8`](https://www.github.com/tauri-apps/tauri/commit/0afee5ed80265c95c4581e173c4886677cfed593) ([#10436](https://www.github.com/tauri-apps/tauri/pull/10436) by [@nyurik](https://www.github.com/tauri-apps/tauri/../../nyurik)) Updated brotli to v6. + +## \[2.0.0-rc.2] + +### Bug Fixes + +- [`f5dfc0280`](https://www.github.com/tauri-apps/tauri/commit/f5dfc02800dbd3bdee671b032454c49ac7102fb4) ([#10533](https://www.github.com/tauri-apps/tauri/pull/10533) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Fixed an issue causing `tauri ios init` to fail if `iOS.minimumSystemVersion` was not configured explicitly. + +## \[2.0.0-rc.1] + +### New Features + +- [`8dc81b6cc`](https://www.github.com/tauri-apps/tauri/commit/8dc81b6cc2b8235b11f74a971d6aa3a5df5e9f68) ([#10496](https://www.github.com/tauri-apps/tauri/pull/10496) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Added `bundle > ios > template` configuration option for custom Xcode project YML Handlebars template using XcodeGen. +- [`02c00abc6`](https://www.github.com/tauri-apps/tauri/commit/02c00abc63cf86e9bf9179cbb143d5145a9397b6) ([#10495](https://www.github.com/tauri-apps/tauri/pull/10495) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Added `bundle > ios > minimumSystemVersion` configuration option. + +### Bug Fixes + +- [`7e810cb2a`](https://www.github.com/tauri-apps/tauri/commit/7e810cb2a3fd934017ae973e737864dfa4bdf64e) ([#10485](https://www.github.com/tauri-apps/tauri/pull/10485) by [@anatawa12](https://www.github.com/tauri-apps/tauri/../../anatawa12)) Fixed an issue where permission files will be generated with ':' in the file path. + +## \[2.0.0-rc.0] + +### New Features + +- [`a5bfbaa62`](https://www.github.com/tauri-apps/tauri/commit/a5bfbaa62b8cd0aacbb33f730d4e30b43c461fe1)([#9962](https://www.github.com/tauri-apps/tauri/pull/9962)) Added `bundle > iOS > frameworks` configuration to define a list of frameworks that are linked to the Xcode project when it is generated. + +### Enhancements + +- [`7aeac39e7`](https://www.github.com/tauri-apps/tauri/commit/7aeac39e7fb97dc57ca278f1c097058275c20aa2) ([#10397](https://www.github.com/tauri-apps/tauri/pull/10397)) Make the set of gtk application id optional, to allow more then one instance of the app running at the same time. + +### Bug Fixes + +- [`498f405ca`](https://www.github.com/tauri-apps/tauri/commit/498f405ca80440447823dd3c9cd53c0f79d655b5) ([#10404](https://www.github.com/tauri-apps/tauri/pull/10404)) Fixed an issue where configuration parsing errors always displayed 'tauri.conf.json' as the file path, even when using 'Tauri.toml' or 'tauri.conf.json5'. + + The error messages now correctly shows the actual config file being used. + +### Security fixes + +- [`426d14bb4`](https://www.github.com/tauri-apps/tauri/commit/426d14bb4164290d93b5a0f61e925cb2dfc4aafa) ([#10423](https://www.github.com/tauri-apps/tauri/pull/10423)) Explicitly check that the main frame's origin is the sender of Isolation Payloads + +## \[2.0.0-beta.19] + +### New Features + +- [`4c239729c`](https://www.github.com/tauri-apps/tauri/commit/4c239729c3e1b899ecbc6793c3682848e8de1729) ([#10167](https://www.github.com/tauri-apps/tauri/pull/10167) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `RawIsolationPayload::content_type` method. + +## \[2.0.0-beta.18] + +### New Features + +- [`fafc238f7`](https://www.github.com/tauri-apps/tauri/commit/fafc238f7288548975ca7d3e5207b925c0295c91) ([#9977](https://www.github.com/tauri-apps/tauri/pull/9977)) Add `bundle > homepage` option, if unset, it will fallback to `homepage` defined in `Cargo.toml`. +- [`656a64974`](https://www.github.com/tauri-apps/tauri/commit/656a64974468bc207bf39537e02ae179bdee9b83) ([#9318](https://www.github.com/tauri-apps/tauri/pull/9318)) Added a configuration option to disable hardened runtime on macOS codesign. +- [`5b769948a`](https://www.github.com/tauri-apps/tauri/commit/5b769948a81cac333f64c870a470ba6525bd5cd3) ([#9959](https://www.github.com/tauri-apps/tauri/pull/9959)) Add `include_image` macro to help embedding instances of `Image` struct at compile-time in rust to be used with window, menu or tray icons. +- [`3ab170917`](https://www.github.com/tauri-apps/tauri/commit/3ab170917ed535fc9013f0a9255631fb34493e18) ([#9932](https://www.github.com/tauri-apps/tauri/pull/9932)) Add an option to disable NSIS compression `bundle > nsis > compression: "none"` +- [`f21029b1b`](https://www.github.com/tauri-apps/tauri/commit/f21029b1bc25f5cb987e1a25de94c2d364e3e462) ([#9994](https://www.github.com/tauri-apps/tauri/pull/9994)) Add `bundle > nsis > startMenuFolder` option to customize start menu folder for NSIS installer + +### Enhancements + +- [`878198777`](https://www.github.com/tauri-apps/tauri/commit/878198777ef693efdbd394cb4be4b234e8a7ed3d) ([#9999](https://www.github.com/tauri-apps/tauri/pull/9999)) Mark ACL `permissions` array with unique items + +### Breaking Changes + +- [`3ab170917`](https://www.github.com/tauri-apps/tauri/commit/3ab170917ed535fc9013f0a9255631fb34493e18) ([#9932](https://www.github.com/tauri-apps/tauri/pull/9932)) Changed `NsisSettings::compression` field from `Option` to just `NsisCompression` +- [`911242f09`](https://www.github.com/tauri-apps/tauri/commit/911242f0928e0a2add3595fa9de27850fb875fa6) ([#9883](https://www.github.com/tauri-apps/tauri/pull/9883)) Move updater target from `bundle > targets` to a separate field `bundle > createUpdaterArtifacts` +- [`3ab170917`](https://www.github.com/tauri-apps/tauri/commit/3ab170917ed535fc9013f0a9255631fb34493e18) ([#9932](https://www.github.com/tauri-apps/tauri/pull/9932)) Changed `NsisConfig::compression` field from `Option` to just `NsisCompression` + +## \[2.0.0-beta.17] + +### New Features + +- [`8a1ae2dea`](https://www.github.com/tauri-apps/tauri/commit/8a1ae2deaf3086e531ada25b1627f900e2e421fb)([#9843](https://www.github.com/tauri-apps/tauri/pull/9843)) Added an option to use a Xcode project for the iOS plugin instead of a plain SwiftPM project. +- [`5462e5cad`](https://www.github.com/tauri-apps/tauri/commit/5462e5cadc73c1b9083d852061d7c7f982cfbe53)([#9731](https://www.github.com/tauri-apps/tauri/pull/9731)) Add `installer_hooks` NSIS configuration field +- [`d6d3efbd1`](https://www.github.com/tauri-apps/tauri/commit/d6d3efbd125489cb46642b6d013cdc1eb7fc1a66)([#9865](https://www.github.com/tauri-apps/tauri/pull/9865)) Add `sign_command` in `WindowsConfig` + +### Breaking Changes + +- [`fc1543c65`](https://www.github.com/tauri-apps/tauri/commit/fc1543c65e736622bed93543dcc6504c43e200bb)([#9864](https://www.github.com/tauri-apps/tauri/pull/9864)) Removed `skip_webview_install` (`skipWebviewInstall`) option from config, which has been deprecated for a while now and planned to be removed in v2. Use `webview_install_mode` (`webviewInstallMode`) instead. +- [`265c23886`](https://www.github.com/tauri-apps/tauri/commit/265c23886ee5efbcc6d7188ff5c84cb32fa82aea)([#9375](https://www.github.com/tauri-apps/tauri/pull/9375)) Removed `Config::binary_name` and `PackageInfo::package_name` + +## \[2.0.0-beta.16] + +### Bug Fixes + +- [`be95d8d37`](https://www.github.com/tauri-apps/tauri/commit/be95d8d37c4b1a420c0d28a83b7efa40ab0b0ab5)([#9782](https://www.github.com/tauri-apps/tauri/pull/9782)) Fixes the `ToTokens` implementation for `Capability`. + +## \[2.0.0-beta.15] + +### Bug Fixes + +- [`a5205f179`](https://www.github.com/tauri-apps/tauri/commit/a5205f179e577cce5c05b710b597da8f4e1d0780)([#9691](https://www.github.com/tauri-apps/tauri/pull/9691)) Fixes the ToTokens implementation of the window configuration `proxy_url` field. + +## \[2.0.0-beta.14] + +### Bug Fixes + +- [`3fbc1703f`](https://www.github.com/tauri-apps/tauri/commit/3fbc1703f107dfb2c5a75e848805dcf60d449eb1)([#9676](https://www.github.com/tauri-apps/tauri/pull/9676)) Fixes `schemars` compilation issue. + +## \[2.0.0-beta.13] + +### Bug Fixes + +- [`a1e0e268f`](https://www.github.com/tauri-apps/tauri/commit/a1e0e268f02f7e2934bec48de4cac0dc00529a2b)([#9477](https://www.github.com/tauri-apps/tauri/pull/9477)) Replace `tauri:` prefix with `tauri-` for temporary permission file names + +## \[2.0.0-beta.12] + +### New Features + +- [`36b4c1249`](https://www.github.com/tauri-apps/tauri/commit/36b4c12497fbe636066f4848c6877b3ab6cc892e)([#9331](https://www.github.com/tauri-apps/tauri/pull/9331)) Added support for `provides`, `conflicts` and `replaces` (`obsoletes` for RPM) options for `bundler > deb` and `bundler > rpm` configs. + +## \[2.0.0-beta.11] + +### New Features + +- [`259d84529`](https://www.github.com/tauri-apps/tauri/commit/259d845290dde40639537258b2810567910f47f3)([#9209](https://www.github.com/tauri-apps/tauri/pull/9209)) Added `preInstallScript`, `postInstallScript`, `preRemoveScript` and `postRemoveScript` options for `bundler > deb` and `bundler > rpm` configs. + +### Enhancements + +- [`7c334cb18`](https://www.github.com/tauri-apps/tauri/commit/7c334cb1851ab034a3cfb472dd99dfc61ad3ca7f)([#9327](https://www.github.com/tauri-apps/tauri/pull/9327)) Make the isolation pattern encrypt key unextractable. +- [`a804a70a7`](https://www.github.com/tauri-apps/tauri/commit/a804a70a7aa1dc40fa9043206ad2265c6a5a437b)([#9328](https://www.github.com/tauri-apps/tauri/pull/9328)) The isolation iframe script now removes itself after execution. + +### Breaking Changes + +- [`06833f4fa`](https://www.github.com/tauri-apps/tauri/commit/06833f4fa8e63ecc55fe3fc874a9e397e77a5709)([#9100](https://www.github.com/tauri-apps/tauri/pull/9100)) Rename `FileDrop` to `DragDrop` on structs, enums and enum variants. Also renamed `file_drop` to `drag_drop` on fields and function names. + +## \[2.0.0-beta.10] + +### New Features + +- [`e227fe02f`](https://www.github.com/tauri-apps/tauri/commit/e227fe02f986e145c0731a64693e1c830a9eb5b0)([#9156](https://www.github.com/tauri-apps/tauri/pull/9156)) Added the `plugin` module. + +### Enhancements + +- [`7213b9e47`](https://www.github.com/tauri-apps/tauri/commit/7213b9e47242bef814aa7257e0bf84631bf5fe7e)([#9124](https://www.github.com/tauri-apps/tauri/pull/9124)) Fallback to an empty permission set if the plugin did not define its `default` permissions. + +## \[2.0.0-beta.9] + +### Breaking Changes + +- [`490a6b424`](https://www.github.com/tauri-apps/tauri/commit/490a6b424e81714524150aef96fbf6cf7004b940)([#9147](https://www.github.com/tauri-apps/tauri/pull/9147)) Removed the `assets::Assets` trait which is now part of the `tauri` crate. + +## \[2.0.0-beta.8] + +### Enhancements + +- [`3e472d0af`](https://www.github.com/tauri-apps/tauri/commit/3e472d0afcd67545dd6d9f18d304580a3b2759a8)([#9115](https://www.github.com/tauri-apps/tauri/pull/9115)) Changed the permission and capability platforms to be optional. + +### Breaking Changes + +- [`4ef17d083`](https://www.github.com/tauri-apps/tauri/commit/4ef17d08336a2e0df4a7ef9adea746d7419710b6)([#9116](https://www.github.com/tauri-apps/tauri/pull/9116)) The ACL configuration for remote URLs now uses the URLPattern standard instead of glob patterns. + +## \[2.0.0-beta.7] + +### Bug Fixes + +- [`86fa339de`](https://www.github.com/tauri-apps/tauri/commit/86fa339de7b176efafa9b3e89f94dcad5fcd03da)([#9071](https://www.github.com/tauri-apps/tauri/pull/9071)) Fix compile time error in context generation when using `app.windows.windowEffects.color` +- [`6c0683224`](https://www.github.com/tauri-apps/tauri/commit/6c068322460300e9d56a4fac5b018d4c437daa9e)([#9068](https://www.github.com/tauri-apps/tauri/pull/9068)) Fixes scope resolution grouping scopes for all windows. +- [`c68218b36`](https://www.github.com/tauri-apps/tauri/commit/c68218b362c417b62e56c7a2b5b32c13fe035a83)([#8990](https://www.github.com/tauri-apps/tauri/pull/8990)) Fix `BundleTarget::to_vec` returning an empty vec for `BundleTarget::All` variant. +- [`c68218b36`](https://www.github.com/tauri-apps/tauri/commit/c68218b362c417b62e56c7a2b5b32c13fe035a83)([#8990](https://www.github.com/tauri-apps/tauri/pull/8990)) Add `BundleType::all` method to return all possible `BundleType` variants. + +### Breaking Changes + +- [`9aa0d6e95`](https://www.github.com/tauri-apps/tauri/commit/9aa0d6e959269a9d99ff474e7f12bd397ea75fcd)([#9069](https://www.github.com/tauri-apps/tauri/pull/9069)) Removed `debug_eprintln!` and `consume_unused_variable` macros. +- [`bb23511ea`](https://www.github.com/tauri-apps/tauri/commit/bb23511ea80bcaffbdebf057301e463fff268c90)([#9079](https://www.github.com/tauri-apps/tauri/pull/9079)) Changed `CapabiltyFile::List` enum variant to be a tuple-struct and added `CapabiltyFile::NamedList`. This allows more flexibility when parsing capabilties from JSON files. + +## \[2.0.0-beta.6] + +### New Features + +- [`d7f56fef`](https://www.github.com/tauri-apps/tauri/commit/d7f56fef85cac3af4e2dbac1eac40e5567b1f160)([#9014](https://www.github.com/tauri-apps/tauri/pull/9014)) Allow defining a permission that only applies to a set of target platforms via the `platforms` configuration option. + +### Enhancements + +- [`04440edc`](https://www.github.com/tauri-apps/tauri/commit/04440edce870f9d06055616034941d79443d5a87)([#9019](https://www.github.com/tauri-apps/tauri/pull/9019)) Changed plugin markdown docs generation to table format. + +### Breaking Changes + +- [`3657ad82`](https://www.github.com/tauri-apps/tauri/commit/3657ad82f88ce528551d032d521c52eed3f396b4)([#9008](https://www.github.com/tauri-apps/tauri/pull/9008)) Allow defining permissions for the application commands via `tauri_build::Attributes::app_manifest`. + +## \[2.0.0-beta.5] + +### Enhancements + +- [`bc5b5e67`](https://www.github.com/tauri-apps/tauri/commit/bc5b5e671a546512f823f1c157421b4c3311dfc0)([#8984](https://www.github.com/tauri-apps/tauri/pull/8984)) Do not include a CSP tag in the application HTML and rely on the custom protocol response header instead. + +## \[2.0.0-beta.4] + +### Breaking Changes + +- [`a76fb118`](https://www.github.com/tauri-apps/tauri/commit/a76fb118ce2de22e1bdb4216bf0ac01dfc3e5799)([#8950](https://www.github.com/tauri-apps/tauri/pull/8950)) Changed the capability format to allow configuring both `remote: { urls: Vec }` and `local: bool (default: true)` instead of choosing one on the `context` field. + +## \[2.0.0-beta.3] + +### Breaking Changes + +- [`361ec37f`](https://www.github.com/tauri-apps/tauri/commit/361ec37fd4a5caa5b6630b9563ef079f53c6c336)([#8932](https://www.github.com/tauri-apps/tauri/pull/8932)) Moved `ProgressBarState` from `tauri-utils` to the `tauri::window` module and removed the `unity_uri` field. + +## \[2.0.0-beta.2] + +### Enhancements + +- [`0cb0a15c`](https://www.github.com/tauri-apps/tauri/commit/0cb0a15ce22af3d649cf219ac04188c14c5f4905)([#8789](https://www.github.com/tauri-apps/tauri/pull/8789)) Add `webviews` array on the capability for usage on multiwebview contexts. +- [`83a68deb`](https://www.github.com/tauri-apps/tauri/commit/83a68deb5676d39cd4728d2e140f6b46d5f787ed)([#8797](https://www.github.com/tauri-apps/tauri/pull/8797)) Added a new configuration option `tauri.conf.json > app > security > capabilities` to reference existing capabilities and inline new ones. If it is empty, all capabilities are still included preserving the current behavior. +- [`8d16a80d`](https://www.github.com/tauri-apps/tauri/commit/8d16a80d2fb2468667e7987d0cc99dbc7e3b9d0a)([#8802](https://www.github.com/tauri-apps/tauri/pull/8802)) The `Context` struct now includes the runtime authority instead of the resolved ACL. This does not impact most applications. +- [`28fb036c`](https://www.github.com/tauri-apps/tauri/commit/28fb036ce476c6f22815c35385f923135212c6f3)([#8852](https://www.github.com/tauri-apps/tauri/pull/8852)) Enhance resource directory resolution on development. +- [`dd7571a7`](https://www.github.com/tauri-apps/tauri/commit/dd7571a7808676c8063a4983b9c6687dfaf03a09)([#8815](https://www.github.com/tauri-apps/tauri/pull/8815)) Do not generate JSON schema and markdown reference file if the plugin does not define any permissions and delete those files if they exist. +- [`5618f6d2`](https://www.github.com/tauri-apps/tauri/commit/5618f6d2ffc9ebf40710145538b06bebfa55f878)([#8856](https://www.github.com/tauri-apps/tauri/pull/8856)) Relax requirements on plugin's identifiers to be alphanumeric and `-` instead of only lower alpha and `-`. +- [`8d16a80d`](https://www.github.com/tauri-apps/tauri/commit/8d16a80d2fb2468667e7987d0cc99dbc7e3b9d0a)([#8802](https://www.github.com/tauri-apps/tauri/pull/8802)) Refactored the capability types and resolution algorithm. + +### Bug Fixes + +- [`ae0fe47c`](https://www.github.com/tauri-apps/tauri/commit/ae0fe47c4c35fa87c77acf42af32ef3f0615cb08)([#8774](https://www.github.com/tauri-apps/tauri/pull/8774)) Fix compile error when `tauri.conf.json` had `bundle > license` set. + +### Breaking Changes + +- [`f284f9c5`](https://www.github.com/tauri-apps/tauri/commit/f284f9c545deeb77d15b6e8b1d0d05f49c40634c)([#8898](https://www.github.com/tauri-apps/tauri/pull/8898)) Changed the capability `remote` configuration to take a list of `urls` instead of `domains` for more flexibility. + +## \[2.0.0-beta.1] + +### Enhancements + +- [`4e101f80`](https://www.github.com/tauri-apps/tauri/commit/4e101f801657e7d01ce8c22f9c6468067d0caab2)([#8756](https://www.github.com/tauri-apps/tauri/pull/8756)) Moved the capability JSON schema to the `src-tauri/gen` folder so it's easier to track changes on the `capabilities` folder. + +### Bug Fixes + +- [`4e101f80`](https://www.github.com/tauri-apps/tauri/commit/4e101f801657e7d01ce8c22f9c6468067d0caab2)([#8756](https://www.github.com/tauri-apps/tauri/pull/8756)) Rerun build script when a new permission is added. + +## \[2.0.0-beta.0] + +### New Features + +- [`74a2a603`](https://www.github.com/tauri-apps/tauri/commit/74a2a6036a5e57462f161d728cbd8a6f121028ca)([#8661](https://www.github.com/tauri-apps/tauri/pull/8661)) Implement access control list for IPC usage. +- [`9eaeb5a8`](https://www.github.com/tauri-apps/tauri/commit/9eaeb5a8cd95ae24b5e66205bdc2763cb7f965ce)([#8622](https://www.github.com/tauri-apps/tauri/pull/8622)) Add `parent` option for window config. +- [`58fe2e81`](https://www.github.com/tauri-apps/tauri/commit/58fe2e812a85b9f4eba105286a63f271ea637836)([#8670](https://www.github.com/tauri-apps/tauri/pull/8670)) Add `WebviewUrl::CustomProtocol` enum variant. + +### What's Changed + +- [`6639a579`](https://www.github.com/tauri-apps/tauri/commit/6639a579c76d45210f33a72d37e21d4c5a9d334b)([#8441](https://www.github.com/tauri-apps/tauri/pull/8441)) Added the `WindowConfig::proxy_url` `WebviewBuilder::proxy_url() / WebviewWindowBuilder::proxy_url()` options when creating a webview. + +### Breaking Changes + +- [`8de308d1`](https://www.github.com/tauri-apps/tauri/commit/8de308d1bf6a855d7a26af58bd0e744938ba47d8)([#8723](https://www.github.com/tauri-apps/tauri/pull/8723)) Restructured Tauri config per [RFC#5](https://github.com/tauri-apps/rfcs/blob/f3e82a6b0c5390401e855850d47dc7b7d9afd684/texts/0005-tauri-config-restructure.md): + + - Moved `package.productName`, `package.version` and `tauri.bundle.identifier` fields to the top-level. + - Removed `package` object. + - Renamed `tauri` object to `app`. + - Moved `tauri.bundle` object to the top-level. + - Renamed `build.distDir` field to `frontendDist`. + - Renamed `build.devPath` field to `devUrl` and will no longer accepts paths, it will only accept URLs. + - Moved `tauri.pattern` to `app.security.pattern`. + - Removed `tauri.bundle.updater` object, and its fields have been moved to the updater plugin under `plugins.updater` object. + - Moved `build.withGlobalTauri` to `app.withGlobalTauri`. + - Moved `tauri.bundle.dmg` object to `bundle.macOS.dmg`. + - Moved `tauri.bundle.deb` object to `bundle.linux.deb`. + - Moved `tauri.bundle.appimage` object to `bundle.linux.appimage`. + - Removed all license fields from each bundle configuration object and instead added `bundle.license` and `bundle.licenseFile`. + - Renamed `AppUrl` to `FrontendDist` and refactored its variants to be more explicit. +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) Renamed `config::WindowUrl` to `config::WebviewUrl`. +- [`a093682d`](https://www.github.com/tauri-apps/tauri/commit/a093682d2df7169b024bb4f736c7f1fd2ea8b327)([#8621](https://www.github.com/tauri-apps/tauri/pull/8621)) Changed `error` field in `ConfigError::FormatToml` to be boxed `Box` to reduce the enum `ConfigError` size in memory. +- [`58fe2e81`](https://www.github.com/tauri-apps/tauri/commit/58fe2e812a85b9f4eba105286a63f271ea637836)([#8670](https://www.github.com/tauri-apps/tauri/pull/8670)) Changed `dist_dir` and `dev_path` config options to be optional. + +## \[2.0.0-alpha.13] + +### Bug Fixes + +- [`9b230de7`](https://www.github.com/tauri-apps/tauri/commit/9b230de7bc6690c2733f5324d50b999af1f7a6ef)([#8407](https://www.github.com/tauri-apps/tauri/pull/8407)) Fix compile error when parsing config that includes float values. + +## \[2.0.0-alpha.12] + +### New Features + +- [\`\`](https://www.github.com/tauri-apps/tauri/commit/undefined) Add bundle DMG configuration options. + +## \[2.0.0-alpha.11] + +### Breaking Changes + +- [`5e84e92e`](https://www.github.com/tauri-apps/tauri/commit/5e84e92e99376f24b730f8eba002239379b593e1)([#8243](https://www.github.com/tauri-apps/tauri/pull/8243)) Changed `platform::windows_version` to return a `(u32, u32, u32)` instead of `Option<(u32, u32, u32)>` + +## \[2.0.0-alpha.10] + +### Enhancements + +- [`c6c59cf2`](https://www.github.com/tauri-apps/tauri/commit/c6c59cf2373258b626b00a26f4de4331765dd487) Pull changes from Tauri 1.5 release. + +### Dependencies + +- [`c7c2507d`](https://www.github.com/tauri-apps/tauri/commit/c7c2507da16a9beb71bf06745fe7ac1325ab7c2a)([#8035](https://www.github.com/tauri-apps/tauri/pull/8035)) Update `windows` to version `0.51` and `webview2-com` to version `0.27` + +## \[2.0.0-alpha.9] + +### New Features + +- [`c085adda`](https://www.github.com/tauri-apps/tauri/commit/c085addab58ba851398373c6fd13f9cb026d71e8)([#8009](https://www.github.com/tauri-apps/tauri/pull/8009)) Added `set_progress_bar` to `Window`. +- [`c1ec0f15`](https://www.github.com/tauri-apps/tauri/commit/c1ec0f155118527361dd5645d920becbc8afd569)([#7933](https://www.github.com/tauri-apps/tauri/pull/7933)) Added the `always_on_bottom` option to the window configuration. +- [`880266a7`](https://www.github.com/tauri-apps/tauri/commit/880266a7f697e1fe58d685de3bb6836ce5251e92)([#8031](https://www.github.com/tauri-apps/tauri/pull/8031)) Bump the MSRV to 1.70. +- [`ed32257d`](https://www.github.com/tauri-apps/tauri/commit/ed32257d044f90b5eb15053efd1667125def2d2b)([#7794](https://www.github.com/tauri-apps/tauri/pull/7794)) On Windows, add `WindowEffect::Tabbed`,`WindowEffect::TabbedDark` and `WindowEffect::TabbedLight` + +### Breaking Changes + +- [`ebcc21e4`](https://www.github.com/tauri-apps/tauri/commit/ebcc21e4b95f4e8c27639fb1bca545b432f52d5e)([#8057](https://www.github.com/tauri-apps/tauri/pull/8057)) Renamed the beforeDevCommand, beforeBuildCommand and beforeBundleCommand hooks environment variables from `TAURI_PLATFORM, TAURI_ARCH, TAURI_FAMILY, TAURI_PLATFORM_VERSION, TAURI_PLATFORM_TYPE and TAURI_DEBUG` to `TAURI_ENV_PLATFORM, TAURI_ENV_ARCH, TAURI_ENV_FAMILY, TAURI_ENV_PLATFORM_VERSION, TAURI_ENV_PLATFORM_TYPE and TAURI_ENV_DEBUG` to differentiate the prefix with other CLI environment variables. + +## \[2.0.0-alpha.8] + +### Enhancements + +- Add an option to specify `id` for the tray icon in the tauri configuration file. + +### Breaking Changes + +- [`100d9ede`](https://www.github.com/tauri-apps/tauri/commit/100d9ede35995d9db21d2087dd5606adfafb89a5)([#7802](https://www.github.com/tauri-apps/tauri/pull/7802)) Follow file name conventions set by desktop for mobile Tauri configuration files. Added `target` argument on most `config::parse` methods. + +## \[2.0.0-alpha.7] + +### New Features + +- [`4db363a0`](https://www.github.com/tauri-apps/tauri/commit/4db363a03c182349f8491f46ced258d84723b11f)([#6589](https://www.github.com/tauri-apps/tauri/pull/6589)) Added `visible_on_all_workspaces` configuration option to `WindowBuilder`, `Window`, and `WindowConfig`. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) Add option to specify a tooltip text for the tray icon in the config. +- [`74b1f4fc`](https://www.github.com/tauri-apps/tauri/commit/74b1f4fc6625d5b4f9b86f70e4eebd6551c61809)([#7384](https://www.github.com/tauri-apps/tauri/pull/7384)) Add `WindowEffect::MicaDark` and `WindowEffect::MicaLight` +- [`3b98141a`](https://www.github.com/tauri-apps/tauri/commit/3b98141aa26f74c641a4090874247b97079bd58a)([#3736](https://www.github.com/tauri-apps/tauri/pull/3736)) Add a configuration object for file associations under `BundleConfig`. + +### Enhancements + +- [`fbeb5b91`](https://www.github.com/tauri-apps/tauri/commit/fbeb5b9185baeda19e865228179e3e44c165f1d9)([#7170](https://www.github.com/tauri-apps/tauri/pull/7170)) Use custom protocols on the IPC implementation to enhance performance. + +### Security fixes + +- [`43c6285e`](https://www.github.com/tauri-apps/tauri/commit/43c6285e9006fb84066461d57fe09ea8db76d636)([#7359](https://www.github.com/tauri-apps/tauri/pull/7359)) Changed HTML implementation from unmaintained `kuchiki` to `kuchikiki`. + +### Breaking Changes + +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) `systemTray` config option has been renamed to `trayIcon`. + +## \[2.0.0-alpha.6] + +### New Features + +- [`e0f0dce2`](https://www.github.com/tauri-apps/tauri/commit/e0f0dce220730e2822fc202463aedf0166145de7)([#6442](https://www.github.com/tauri-apps/tauri/pull/6442)) Added the `window_effects` option to the window configuration. + +## \[2.0.0-alpha.5] + +- [`9a79dc08`](https://www.github.com/tauri-apps/tauri/commit/9a79dc085870e0c1a5df13481ff271b8c6cc3b78)([#6947](https://www.github.com/tauri-apps/tauri/pull/6947)) Remove `enable_tauri_api` from the IPC scope. +- [`09376af5`](https://www.github.com/tauri-apps/tauri/commit/09376af59424cc27803fa2820d2ac0d4cdc90a6d)([#6704](https://www.github.com/tauri-apps/tauri/pull/6704)) Moved the `cli` feature to its own plugin in the plugins-workspace repository. +- [`e1e85dc2`](https://www.github.com/tauri-apps/tauri/commit/e1e85dc2a5f656fc37867e278cae8042037740ac)([#6925](https://www.github.com/tauri-apps/tauri/pull/6925)) Moved the `protocol` scope configuration to the `asset_protocol` field in `SecurityConfig`. +- [`e1e85dc2`](https://www.github.com/tauri-apps/tauri/commit/e1e85dc2a5f656fc37867e278cae8042037740ac)([#6925](https://www.github.com/tauri-apps/tauri/pull/6925)) Moved the updater configuration to the `BundleConfig`. +- [`b072daa3`](https://www.github.com/tauri-apps/tauri/commit/b072daa3bd3e38b808466666619ddb885052c5b2)([#6919](https://www.github.com/tauri-apps/tauri/pull/6919)) Moved the `updater` feature to its own plugin in the plugins-workspace repository. +- [`3188f376`](https://www.github.com/tauri-apps/tauri/commit/3188f3764978c6d1452ee31d5a91469691e95094)([#6883](https://www.github.com/tauri-apps/tauri/pull/6883)) Bump the MSRV to 1.65. +- [`e1e85dc2`](https://www.github.com/tauri-apps/tauri/commit/e1e85dc2a5f656fc37867e278cae8042037740ac)([#6925](https://www.github.com/tauri-apps/tauri/pull/6925)) Removed the allowlist configuration. +- [`2d5378bf`](https://www.github.com/tauri-apps/tauri/commit/2d5378bfc1ba817ee2f331b41738a90e5997e5e8)([#6717](https://www.github.com/tauri-apps/tauri/pull/6717)) Remove the updater's dialog option. + +## \[2.0.0-alpha.4] + +- Added `android` configuration object under `tauri > bundle`. + - [db4c9dc6](https://www.github.com/tauri-apps/tauri/commit/db4c9dc655e07ee2184fe04571f500f7910890cd) feat(core): add option to configure Android's minimum SDK version ([#6651](https://www.github.com/tauri-apps/tauri/pull/6651)) on 2023-04-07 + +## \[2.0.0-alpha.3] + +- Pull changes from Tauri 1.3 release. + - [](https://www.github.com/tauri-apps/tauri/commit/undefined) on undefined + +## \[2.0.0-alpha.2] + +- Added the `shadow` option to the window configuration and `set_shadow` option to the `window` allow list. + - [a81750d7](https://www.github.com/tauri-apps/tauri/commit/a81750d779bc72f0fdb7de90b7fbddfd8049b328) feat(core): add shadow APIs ([#6206](https://www.github.com/tauri-apps/tauri/pull/6206)) on 2023-02-08 + +## \[2.0.0-alpha.1] + +- Bump the MSRV to 1.64. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Added `crate_name` field on `PackageInfo`. + - [630a7f4b](https://www.github.com/tauri-apps/tauri/commit/630a7f4b18cef169bfd48673609306fec434e397) refactor: remove mobile log initialization, ref [#6049](https://www.github.com/tauri-apps/tauri/pull/6049) ([#6081](https://www.github.com/tauri-apps/tauri/pull/6081)) on 2023-01-17 + +## \[2.0.0-alpha.0] + +- Parse `android` and `ios` Tauri configuration files. + - [b3a3afc7](https://www.github.com/tauri-apps/tauri/commit/b3a3afc7de8de4021d73559288f5192732a706cf) feat(core): detect android and ios platform configuration files ([#4997](https://www.github.com/tauri-apps/tauri/pull/4997)) on 2022-08-22 +- First mobile alpha release! + - [fa3a1098](https://www.github.com/tauri-apps/tauri/commit/fa3a10988a03aed1b66fb17d893b1a9adb90f7cd) feat(ci): prepare 2.0.0-alpha.0 ([#5786](https://www.github.com/tauri-apps/tauri/pull/5786)) on 2022-12-08 + +## \[1.5.3] + +### New features + +- [`7aa30dec`](https://www.github.com/tauri-apps/tauri/commit/7aa30dec85a17c3d3faaf3841b93e10991b991b0)([#8620](https://www.github.com/tauri-apps/tauri/pull/8620)) Add `priority`, `section` and `changelog` options in Debian config. + +## \[1.5.2] + +### Bug Fixes + +- [`9b230de7`](https://www.github.com/tauri-apps/tauri/commit/9b230de7bc6690c2733f5324d50b999af1f7a6ef)([#8407](https://www.github.com/tauri-apps/tauri/pull/8407)) Fix compile error when parsing config that includes float values. + +## \[1.5.3] + +### New Features + +- [`b3e53e72`](https://www.github.com/tauri-apps/tauri/commit/b3e53e7243311a2659b7569dddc20c56ac9f9d8e)([#8288](https://www.github.com/tauri-apps/tauri/pull/8288)) Added `Assets::iter` to iterate on all embedded assets. + +## \[1.5.0] + +### New Features + +- [`4dd4893d`](https://www.github.com/tauri-apps/tauri/commit/4dd4893d7d166ac3a3b6dc2e3bd2540326352a78)([#5950](https://www.github.com/tauri-apps/tauri/pull/5950)) Allow specifying resources as a map specifying source and target paths. + +### Enhancements + +- [`9aa34ada`](https://www.github.com/tauri-apps/tauri/commit/9aa34ada5769dbefa7dfe5f7a6288b3d20b294e4)([#7645](https://www.github.com/tauri-apps/tauri/pull/7645)) Add setting to switch to `http://.localhost/` for custom protocols on Windows. + +### Bug Fixes + +- [`a6b52e44`](https://www.github.com/tauri-apps/tauri/commit/a6b52e44f22844009e273fb0250368d7a463f095)([#6519](https://www.github.com/tauri-apps/tauri/pull/6519)) Fix `io::read_line` not including the new line character `\n`. + +### Security fixes + +- [`eeff1784`](https://www.github.com/tauri-apps/tauri/commit/eeff1784e1ffa568e4ba024e17dd611f8e086784)([#7367](https://www.github.com/tauri-apps/tauri/pull/7367)) Changed HTML implementation from unmaintained `kuchiki` to `kuchikiki`. + +## \[1.4.0] + +### New Features + +- [`acc36fe1`](https://www.github.com/tauri-apps/tauri/commit/acc36fe1176cc8aa9063bde932abeb94796c5c72)([#6158](https://www.github.com/tauri-apps/tauri/pull/6158)) Add option to configure `require_literal_leading_dot` on `fs` and `asset` protocol scopes. +- [`35cd751a`](https://www.github.com/tauri-apps/tauri/commit/35cd751adc6fef1f792696fa0cfb471b0bf99374)([#5176](https://www.github.com/tauri-apps/tauri/pull/5176)) Added the `desktop_template` option on `tauri.conf.json > tauri > bundle > deb`. +- [`c4d6fb4b`](https://www.github.com/tauri-apps/tauri/commit/c4d6fb4b1ea8acf02707a9fe5dcab47c1c5bae7b)([#2353](https://www.github.com/tauri-apps/tauri/pull/2353)) Added the `maximizable`, `minimizable` and `closable` options to the window configuration. +- [`3cb7a3e6`](https://www.github.com/tauri-apps/tauri/commit/3cb7a3e642bb10ee90dc1d24daa48b8c8c15c9ce)([#6997](https://www.github.com/tauri-apps/tauri/pull/6997)) Add `MimeType::parse_with_fallback` and `MimeType::parse_from_uri_with_fallback` +- [`29488205`](https://www.github.com/tauri-apps/tauri/commit/2948820579d20dfaa0861c2f0a58bd7737a7ffd1)([#6867](https://www.github.com/tauri-apps/tauri/pull/6867)) Allow specifying custom language files of Tauri's custom messages for the NSIS installer +- [`e092f799`](https://www.github.com/tauri-apps/tauri/commit/e092f799469ff32c7d1595d0f07d06fd2dab5c29)([#6887](https://www.github.com/tauri-apps/tauri/pull/6887)) Add `nsis > template` option to specify custom NSIS installer template. +- [`cd3846c8`](https://www.github.com/tauri-apps/tauri/commit/cd3846c8ce61ab2879b3911e831525e6242aaab2)([#6955](https://www.github.com/tauri-apps/tauri/pull/6955)) Add `WindowsUpdateInstallMode::nsis_args` +- [`85e77fb7`](https://www.github.com/tauri-apps/tauri/commit/85e77fb797ec17882f55d0944578d929fc6c9c1f)([#6762](https://www.github.com/tauri-apps/tauri/pull/6762)) Correctly determine MIME type of `.txt` files. + +## \[1.3.0] + +- Added the `additional_browser_args` option to the window configuration. + - [3dc38b15](https://www.github.com/tauri-apps/tauri/commit/3dc38b150ea8c59c8ba67fd586f921016928f47c) feat(core): expose additional_browser_args to window config (fix: [#5757](https://www.github.com/tauri-apps/tauri/pull/5757)) ([#5799](https://www.github.com/tauri-apps/tauri/pull/5799)) on 2022-12-14 +- Added the `content_protected` option to the window configuration. + - [4ab5545b](https://www.github.com/tauri-apps/tauri/commit/4ab5545b7a831c549f3c65e74de487ede3ab7ce5) feat: add content protection api, closes [#5132](https://www.github.com/tauri-apps/tauri/pull/5132) ([#5513](https://www.github.com/tauri-apps/tauri/pull/5513)) on 2022-12-13 +- Correctly determine mime type of `.less`, `.sass` and `.styl` files. + - [5fdf8dcb](https://www.github.com/tauri-apps/tauri/commit/5fdf8dcb8ed171d06121dceb32078a7e4f86cc64) fix(core): mime type of .less, .sass and .styl files ([#6316](https://www.github.com/tauri-apps/tauri/pull/6316)) on 2023-02-19 +- Bump minimum supported Rust version to 1.60. + - [5fdc616d](https://www.github.com/tauri-apps/tauri/commit/5fdc616df9bea633810dcb814ac615911d77222c) feat: Use the zbus-backed of notify-rust ([#6332](https://www.github.com/tauri-apps/tauri/pull/6332)) on 2023-03-31 +- Add initial support for building `nsis` bundles on non-Windows platforms. + - [60e6f6c3](https://www.github.com/tauri-apps/tauri/commit/60e6f6c3f1605f3064b5bb177992530ff788ccf0) feat(bundler): Add support for creating NSIS bundles on unix hosts ([#5788](https://www.github.com/tauri-apps/tauri/pull/5788)) on 2023-01-19 +- Add `nsis` bundle target + - [c94e1326](https://www.github.com/tauri-apps/tauri/commit/c94e1326a7c0767a13128a8b1d327a00156ece12) feat(bundler): add `nsis`, closes [#4450](https://www.github.com/tauri-apps/tauri/pull/4450), closes [#2319](https://www.github.com/tauri-apps/tauri/pull/2319) ([#4674](https://www.github.com/tauri-apps/tauri/pull/4674)) on 2023-01-03 +- Added configuration to specify remote URLs allowed to access the IPC. + - [ee71c31f](https://www.github.com/tauri-apps/tauri/commit/ee71c31fd09cc5427da6d29d37c003a914547696) feat(core): allow configuring remote domains with IPC access, closes [#5088](https://www.github.com/tauri-apps/tauri/pull/5088) ([#5918](https://www.github.com/tauri-apps/tauri/pull/5918)) on 2023-04-11 + +## \[1.2.1] + +- Fix `allowlist > app > show/hide` always disabled when `allowlist > app > all: false`. + - [bb251087](https://www.github.com/tauri-apps/tauri/commit/bb2510876d0bdff736d36bf3a465cdbe4ad2b90c) fix(core): extend allowlist with `app`'s allowlist, closes [#5650](https://www.github.com/tauri-apps/tauri/pull/5650) ([#5652](https://www.github.com/tauri-apps/tauri/pull/5652)) on 2022-11-18 + +## \[1.2.0] + +- Validate `package > productName` in the tauri config and produce errors if it contains one of the following characters `/\:*?\"<>|` + - [b9316a64](https://www.github.com/tauri-apps/tauri/commit/b9316a64eaa9348c79efafb8b94960d9b4d5b27a) fix(cli): validate `productName` in config, closes [#5233](https://www.github.com/tauri-apps/tauri/pull/5233) ([#5262](https://www.github.com/tauri-apps/tauri/pull/5262)) on 2022-09-28 +- Properly serialize HTML template tags. + - [aec5537d](https://www.github.com/tauri-apps/tauri/commit/aec5537de0205f62b2ae5c89da04d21930a6fc2e) fix(codegen): serialize template tags, closes [#4410](https://www.github.com/tauri-apps/tauri/pull/4410) ([#5247](https://www.github.com/tauri-apps/tauri/pull/5247)) on 2022-09-28 +- `PatternKind::Isolation` is now defined even without the `isolation` feature. + - [a178f95d](https://www.github.com/tauri-apps/tauri/commit/a178f95d68b773779b40235a3a22115a5e36aa6a) feat: config schema generator ([#5193](https://www.github.com/tauri-apps/tauri/pull/5193)) on 2022-10-28 +- Added the `app` allowlist module. + - [39bf895b](https://www.github.com/tauri-apps/tauri/commit/39bf895b73ec6b53f5758815396ba85dda6b9c67) feat(macOS): Add application `show` and `hide` methods ([#3689](https://www.github.com/tauri-apps/tauri/pull/3689)) on 2022-10-03 +- - [7d9aa398](https://www.github.com/tauri-apps/tauri/commit/7d9aa3987efce2d697179ffc33646d086c68030c) feat: bump MSRV to 1.59 ([#5296](https://www.github.com/tauri-apps/tauri/pull/5296)) on 2022-09-28 +- Add `tauri.conf.json > bundle > publisher` field to specify the app publisher. + - [628285c1](https://www.github.com/tauri-apps/tauri/commit/628285c1cf43f03ed62378f3b6cc0c991317526f) feat(bundler): add `publisher` field, closes [#5273](https://www.github.com/tauri-apps/tauri/pull/5273) ([#5283](https://www.github.com/tauri-apps/tauri/pull/5283)) on 2022-09-28 +- Canonicalize the return value of `platform::resource_dir`. + - [a06dc699](https://www.github.com/tauri-apps/tauri/commit/a06dc6993148f10ff7623c9dcc81f313dd960ad0) fix(core): canonicalize resource dir to fix scope check, closes [#5196](https://www.github.com/tauri-apps/tauri/pull/5196) ([#5218](https://www.github.com/tauri-apps/tauri/pull/5218)) on 2022-09-29 +- Added `title` option on the system tray configuration (macOS only). + - [8f1ace77](https://www.github.com/tauri-apps/tauri/commit/8f1ace77956ac3477826ceb059a191e55b3fff93) feat: expose `set_title` for MacOS tray ([#5182](https://www.github.com/tauri-apps/tauri/pull/5182)) on 2022-09-30 +- Added the `user_agent` option to the window configuration. + - [a6c94119](https://www.github.com/tauri-apps/tauri/commit/a6c94119d8545d509723b147c273ca5edfe3729f) feat(core): expose user_agent to window config ([#5317](https://www.github.com/tauri-apps/tauri/pull/5317)) on 2022-10-02 +- Add `mime_type` module. + - [54c337e0](https://www.github.com/tauri-apps/tauri/commit/54c337e06f3bc624c4780cf002bc54790f446c90) feat(cli): hotreload support for frontend static files, closes [#2173](https://www.github.com/tauri-apps/tauri/pull/2173) ([#5256](https://www.github.com/tauri-apps/tauri/pull/5256)) on 2022-09-28 + +## \[1.1.1] + +- Add missing allowlist config for `set_cursor_grab`, `set_cursor_visible`, `set_cursor_icon` and `set_cursor_position` APIs. + - [c764408d](https://www.github.com/tauri-apps/tauri/commit/c764408da7fae123edd41115bda42fa75a4731d2) fix: Add missing allowlist config for cursor apis, closes [#5207](https://www.github.com/tauri-apps/tauri/pull/5207) ([#5211](https://www.github.com/tauri-apps/tauri/pull/5211)) on 2022-09-16 + +## \[1.1.0] + +- Allow adding `build > beforeBundleCommand` in tauri.conf.json to run a shell command before the bundling phase. + - [57ab9847](https://www.github.com/tauri-apps/tauri/commit/57ab9847eb2d8c9a5da584b873b7c072e9ee26bf) feat(cli): add `beforeBundleCommand`, closes [#4879](https://www.github.com/tauri-apps/tauri/pull/4879) ([#4893](https://www.github.com/tauri-apps/tauri/pull/4893)) on 2022-08-09 +- Change `before_dev_command` and `before_build_command` config value to allow configuring the current working directory. + - [d6f7d3cf](https://www.github.com/tauri-apps/tauri/commit/d6f7d3cfe8a7ec8d68c8341016c4e0a3103da587) Add cwd option to `before` commands, add wait option to dev [#4740](https://www.github.com/tauri-apps/tauri/pull/4740) [#3551](https://www.github.com/tauri-apps/tauri/pull/3551) ([#4834](https://www.github.com/tauri-apps/tauri/pull/4834)) on 2022-08-02 +- Allow configuring the `before_dev_command` to force the CLI to wait for the command to finish before proceeding. + - [d6f7d3cf](https://www.github.com/tauri-apps/tauri/commit/d6f7d3cfe8a7ec8d68c8341016c4e0a3103da587) Add cwd option to `before` commands, add wait option to dev [#4740](https://www.github.com/tauri-apps/tauri/pull/4740) [#3551](https://www.github.com/tauri-apps/tauri/pull/3551) ([#4834](https://www.github.com/tauri-apps/tauri/pull/4834)) on 2022-08-02 +- Added support to configuration files in TOML format (Tauri.toml file). + - [ae83d008](https://www.github.com/tauri-apps/tauri/commit/ae83d008f9e1b89bfc8dddaca42aa5c1fbc36f6d) feat: add support to TOML config file `Tauri.toml`, closes [#4806](https://www.github.com/tauri-apps/tauri/pull/4806) ([#4813](https://www.github.com/tauri-apps/tauri/pull/4813)) on 2022-08-02 +- Refactored the `config::parse` module. + - [ae83d008](https://www.github.com/tauri-apps/tauri/commit/ae83d008f9e1b89bfc8dddaca42aa5c1fbc36f6d) feat: add support to TOML config file `Tauri.toml`, closes [#4806](https://www.github.com/tauri-apps/tauri/pull/4806) ([#4813](https://www.github.com/tauri-apps/tauri/pull/4813)) on 2022-08-02 +- Update windows to 0.39.0 and webview2-com to 0.19.1. + - [e6d9b670](https://www.github.com/tauri-apps/tauri/commit/e6d9b670b0b314ed667b0e164f2c8d27048e678f) refactor: remove unneeded focus code ([#5065](https://www.github.com/tauri-apps/tauri/pull/5065)) on 2022-09-03 + +## \[1.0.3] + +- Added `menu_on_left_click: bool` to the `SystemTrayConfig`. + - [f8a3becb](https://www.github.com/tauri-apps/tauri/commit/f8a3becb287942db7f7b551b5db6aeb5a2e939ee) feat(core): add option to disable tray menu on left click, closes [#4584](https://www.github.com/tauri-apps/tauri/pull/4584) ([#4587](https://www.github.com/tauri-apps/tauri/pull/4587)) on 2022-07-05 +- Added `config::parse::read_platform` and `config::parse::get_platform_config_filename`. + - [8e3e7fc6](https://www.github.com/tauri-apps/tauri/commit/8e3e7fc64641afc7a6833bc93205e6f525562545) feat(cli): improve bundle identifier validation, closes [#4589](https://www.github.com/tauri-apps/tauri/pull/4589) ([#4596](https://www.github.com/tauri-apps/tauri/pull/4596)) on 2022-07-05 + +## \[1.0.2] + +- Expose `platform::windows_version` function. + - [bf764e83](https://www.github.com/tauri-apps/tauri/commit/bf764e83e01e7443e6cc54572001e1c98c357465) feat(utils): expose `windows_version` function ([#4534](https://www.github.com/tauri-apps/tauri/pull/4534)) on 2022-06-30 + +## \[1.0.1] + +- Changed the `BundleConfig::targets` to a `BundleTarget` enum to enhance generated documentation. + - [31c15cd2](https://www.github.com/tauri-apps/tauri/commit/31c15cd2bd94dbe39fb94982a15cbe02ac5d8925) docs(config): enhance documentation for bundle targets, closes [#3251](https://www.github.com/tauri-apps/tauri/pull/3251) ([#4418](https://www.github.com/tauri-apps/tauri/pull/4418)) on 2022-06-21 +- Added `platform::is_windows_7`. + - [57039fb2](https://www.github.com/tauri-apps/tauri/commit/57039fb2166571de85271b014a8711a29f06be1a) fix(core): add windows 7 notification support ([#4491](https://www.github.com/tauri-apps/tauri/pull/4491)) on 2022-06-28 +- Suppress unused variable warning in release builds. + - [45981851](https://www.github.com/tauri-apps/tauri/commit/45981851e35119266c1a079e1ff27a39f1fdfaed) chore(lint): unused variable warnings for release builds ([#4411](https://www.github.com/tauri-apps/tauri/pull/4411)) on 2022-06-22 +- Added webview install mode options. + - [2ca762d2](https://www.github.com/tauri-apps/tauri/commit/2ca762d207030a892a6d128b519e150e2d733468) feat(bundler): extend webview2 installation options, closes [#2882](https://www.github.com/tauri-apps/tauri/pull/2882) [#2452](https://www.github.com/tauri-apps/tauri/pull/2452) ([#4466](https://www.github.com/tauri-apps/tauri/pull/4466)) on 2022-06-26 + +## \[1.0.0] + +- Upgrade to `stable`! + - [f4bb30cc](https://www.github.com/tauri-apps/tauri/commit/f4bb30cc73d6ba9b9ef19ef004dc5e8e6bb901d3) feat(covector): prepare for v1 ([#4351](https://www.github.com/tauri-apps/tauri/pull/4351)) on 2022-06-15 + +## \[1.0.0-rc.9] + +- Added a config flag to bundle the media framework used by webkit2gtk `tauri.conf.json > tauri > bundle > appimage > bundleMediaFramework`. + - [d335fae9](https://www.github.com/tauri-apps/tauri/commit/d335fae92cdcbb0ee18aad4e54558914afa3e778) feat(bundler): bundle additional gstreamer files, closes [#4092](https://www.github.com/tauri-apps/tauri/pull/4092) ([#4271](https://www.github.com/tauri-apps/tauri/pull/4271)) on 2022-06-10 + +## \[1.0.0-rc.8] + +- **Breaking change:** `PackageInfo::version` is now a `semver::Version` instead of a `String`. + - [2badbd2d](https://www.github.com/tauri-apps/tauri/commit/2badbd2d7ed51bf33c1b547b4c837b600574bd4a) refactor: force semver versions, change updater `should_install` sig ([#4215](https://www.github.com/tauri-apps/tauri/pull/4215)) on 2022-05-25 + - [a7388e23](https://www.github.com/tauri-apps/tauri/commit/a7388e23c3b9019d48b078cae00a75c74d74d11b) fix(ci): adjust change file to include tauri-utils and tauri-codegen on 2022-05-27 + +## \[1.0.0-rc.7] + +- Allow configuring the display options for the MSI execution allowing quieter updates. + - [9f2c3413](https://www.github.com/tauri-apps/tauri/commit/9f2c34131952ea83c3f8e383bc3cec7e1450429f) feat(core): configure msiexec display options, closes [#3951](https://www.github.com/tauri-apps/tauri/pull/3951) ([#4061](https://www.github.com/tauri-apps/tauri/pull/4061)) on 2022-05-15 + +## \[1.0.0-rc.6] + +- Added `$schema` support to `tauri.conf.json`. + - [715cbde3](https://www.github.com/tauri-apps/tauri/commit/715cbde3842a916c4ebeab2cab348e1774b5c192) feat(config): add `$schema` to `tauri.conf.json`, closes [#3464](https://www.github.com/tauri-apps/tauri/pull/3464) ([#4031](https://www.github.com/tauri-apps/tauri/pull/4031)) on 2022-05-03 +- The `dangerous_allow_asset_csp_modification` configuration value has been changed to allow a list of CSP directives to disable. + - [164078c0](https://www.github.com/tauri-apps/tauri/commit/164078c0b719ccbc12e956fecf8a7d4a3c5044e1) feat: allow limiting dangerousDisableAssetCspModification, closes [#3831](https://www.github.com/tauri-apps/tauri/pull/3831) ([#4021](https://www.github.com/tauri-apps/tauri/pull/4021)) on 2022-05-02 + +## \[1.0.0-rc.5] + +- Added the `io` module with the `read_line` method. + - [b5622882](https://www.github.com/tauri-apps/tauri/commit/b5622882cf3748e1e5a90915f415c0cd922aaaf8) fix(cli): exit on non-compilation Cargo errors, closes [#3930](https://www.github.com/tauri-apps/tauri/pull/3930) ([#3942](https://www.github.com/tauri-apps/tauri/pull/3942)) on 2022-04-22 +- **Breaking change:** Removed the `useBootstrapper` option. Use https://github.com/tauri-apps/fix-path-env-rs instead. + - [6a5ff08c](https://www.github.com/tauri-apps/tauri/commit/6a5ff08ce9052b656aa40accedfd4315825164a3) refactor: remove bootstrapper, closes [#3786](https://www.github.com/tauri-apps/tauri/pull/3786) ([#3832](https://www.github.com/tauri-apps/tauri/pull/3832)) on 2022-03-31 + +## \[1.0.0-rc.4] + +- Added an option to disable the CSP injection of distributable assets nonces and hashes. + - [f6e32ee1](https://www.github.com/tauri-apps/tauri/commit/f6e32ee1880eb364ed76beb937c9d12e14d54910) feat(core): add dangerous option to disable compile time CSP injection ([#3775](https://www.github.com/tauri-apps/tauri/pull/3775)) on 2022-03-28 + +- Use the default value for `MacConfig.minimumSystemVersion` if the value is set to an empty string. + - [c81534eb](https://www.github.com/tauri-apps/tauri/commit/c81534ebd873c358e0346c7949aeb171803149a5) feat(cli): use default macOS minimum system version when it is empty ([#3658](https://www.github.com/tauri-apps/tauri/pull/3658)) on 2022-03-13 + +- Replace multiple dependencies who's C code compiled concurrently and caused + the other ones to bloat compile time significantly. + +- `zstd` -> `brotli` + +- `blake3` -> a vendored version of the blake3 reference + +- `ring` -> `getrandom` + +See https://github.com/tauri-apps/tauri/pull/3773 for more information about +these specific choices. + +- [8661e3e2](https://www.github.com/tauri-apps/tauri/commit/8661e3e24d96c399bfbcdee5d8e9d6beba2265a7) replace dependencies with long build times when used together (closes [#3571](https://www.github.com/tauri-apps/tauri/pull/3571)) ([#3773](https://www.github.com/tauri-apps/tauri/pull/3773)) on 2022-03-27 + +## \[1.0.0-rc.3] + +- Use `is_symlink` API compatible with Rust v1.57 instead of std/fs/struct.Metadata.html#method.is_symlink. + - [73388119](https://www.github.com/tauri-apps/tauri/commit/73388119e653e7902b19beef2ab6d7c5f8a7b83a) use older symlink check function ([#3579](https://www.github.com/tauri-apps/tauri/pull/3579)) on 2022-03-01 + +## \[1.0.0-rc.2] + +- Changed the default value for `tauri > bundle > macOS > minimumSystemVersion` to `10.13`. + - [fce344b9](https://www.github.com/tauri-apps/tauri/commit/fce344b90b7227f8f5514853c2f885fb24d3648e) feat(core): set default value for `minimum_system_version` to 10.13 ([#3497](https://www.github.com/tauri-apps/tauri/pull/3497)) on 2022-02-17 + +## \[1.0.0-rc.1] + +- Change default value for the `freezePrototype` configuration to `false`. + - [3a4c0160](https://www.github.com/tauri-apps/tauri/commit/3a4c01606184be762adee055ddac803de0d28527) fix(core): change default `freezePrototype` to false, closes [#3416](https://www.github.com/tauri-apps/tauri/pull/3416) [#3406](https://www.github.com/tauri-apps/tauri/pull/3406) ([#3423](https://www.github.com/tauri-apps/tauri/pull/3423)) on 2022-02-12 + +## \[1.0.0-rc.0] + +- The `allowlist` configuration now includes a `clipboard` object, controlling the exposure of the `writeText` and `readText` APIs. + - [d660cab3](https://www.github.com/tauri-apps/tauri/commit/d660cab38d7d703e8b2bb85a3e9462d9e28b086b) feat: enhance allowlist configuration \[TRI-027] ([#11](https://www.github.com/tauri-apps/tauri/pull/11)) on 2022-01-09 +- The dialog allowlist now includes flags for the `message`, `ask` and `confirm` APIs. + - [d660cab3](https://www.github.com/tauri-apps/tauri/commit/d660cab38d7d703e8b2bb85a3e9462d9e28b086b) feat: enhance allowlist configuration \[TRI-027] ([#11](https://www.github.com/tauri-apps/tauri/pull/11)) on 2022-01-09 +- The `allowlist` configuration now includes a `process` object, controlling the exposure of the `relaunch` and `exit` APIs. + - [d660cab3](https://www.github.com/tauri-apps/tauri/commit/d660cab38d7d703e8b2bb85a3e9462d9e28b086b) feat: enhance allowlist configuration \[TRI-027] ([#11](https://www.github.com/tauri-apps/tauri/pull/11)) on 2022-01-09 +- The `window` allowlist now includes options to enable all window modification APIs: `center`, `close`, `create`, `hide`, `maximize`, `minimize`, `print`, `requestUserAttention`, `setAlwaysOnTop`, `setDecorations`, `setFocus`, `setFullscreen`, `setIcon`, `setMaxSize`, `setMinSize`, `setPosition`, `setResizable`, `setSize`, `setSkipTaskbar`, `setTitle`, `show`, `startDragging`, `unmaximize` and `unminimize`. + - [d660cab3](https://www.github.com/tauri-apps/tauri/commit/d660cab38d7d703e8b2bb85a3e9462d9e28b086b) feat: enhance allowlist configuration \[TRI-027] ([#11](https://www.github.com/tauri-apps/tauri/pull/11)) on 2022-01-09 +- Added `asset` allowlist configuration, which enables the `asset` protocol and defines it access scope. + - [7920ff14](https://www.github.com/tauri-apps/tauri/commit/7920ff14e6424079c48ea5645d9aa13e7a272b87) feat: scope the `fs` API and the `asset` protocol \[TRI-026] \[TRI-010] \[TRI-011] ([#10](https://www.github.com/tauri-apps/tauri/pull/10)) on 2022-01-09 +- Change `CliArg` numeric types from `u64` to `usize`. + - [1f988535](https://www.github.com/tauri-apps/tauri/commit/1f98853573a837dd0cfc2161b206a5033ec2da5e) chore(deps) Update Tauri Core ([#2480](https://www.github.com/tauri-apps/tauri/pull/2480)) on 2021-08-24 +- Apply `nonce` to `script` and `style` tags and set them on the `CSP` (`script-src` and `style-src` fetch directives). + - [cf54dcf9](https://www.github.com/tauri-apps/tauri/commit/cf54dcf9c81730e42c9171daa9c8aa474c95b522) feat: improve `CSP` security with nonces and hashes, add `devCsp` \[TRI-004] ([#8](https://www.github.com/tauri-apps/tauri/pull/8)) on 2022-01-09 +- The path returned from `tauri::api::process::current_binary` is now cached when loading the binary. + - [7c3db7a3](https://www.github.com/tauri-apps/tauri/commit/7c3db7a3811fd4de3e71c78cfd00894fa51ab786) cache current binary path much sooner ([#45](https://www.github.com/tauri-apps/tauri/pull/45)) on 2022-02-01 +- Added `dev_csp` to the `security` configuration object. + - [cf54dcf9](https://www.github.com/tauri-apps/tauri/commit/cf54dcf9c81730e42c9171daa9c8aa474c95b522) feat: improve `CSP` security with nonces and hashes, add `devCsp` \[TRI-004] ([#8](https://www.github.com/tauri-apps/tauri/pull/8)) on 2022-01-09 +- Fixes resource directory resolution on Linux. + - [1a28904b](https://www.github.com/tauri-apps/tauri/commit/1a28904b8ebea92e143d5dc21ebd209e9edec531) fix(core): resource path resolution on Linux, closes [#2493](https://www.github.com/tauri-apps/tauri/pull/2493) on 2021-08-22 +- Allow using a fixed version for the Webview2 runtime via the `tauri > bundle > windows > webviewFixedRuntimePath` config option. + - [85df94f2](https://www.github.com/tauri-apps/tauri/commit/85df94f2b0d40255812b42c5e32a70c4b45392df) feat(core): config for fixed webview2 runtime version path ([#27](https://www.github.com/tauri-apps/tauri/pull/27)) on 2021-11-02 +- The updater `pubkey` is now a required field for security reasons. Sign your updates with the `tauri signer` command. + - [d95cc831](https://www.github.com/tauri-apps/tauri/commit/d95cc83105dda52df7514e30e54f3676cdb374ee) feat: enforce updater public key \[TRI-015] ([#42](https://www.github.com/tauri-apps/tauri/pull/42)) on 2022-01-09 +- Added the `isolation` pattern. + - [d5d6d2ab](https://www.github.com/tauri-apps/tauri/commit/d5d6d2abc17cd89c3a079d2ce01581193469dbc0) Isolation Pattern ([#43](https://www.github.com/tauri-apps/tauri/pull/43)) Co-authored-by: Ngo Iok Ui (Wu Yu Wei) Co-authored-by: Lucas Fernandes Nogueira on 2022-01-17 +- Adds support for using JSON5 format for the `tauri.conf.json` file, along with also supporting the `.json5` extension. + +Here is the logic flow that determines if JSON or JSON5 will be used to parse the config: + +1. Check if `tauri.conf.json` exists + a. Parse it with `serde_json` + b. Parse it with `json5` if `serde_json` fails + c. Return original `serde_json` error if all above steps failed +2. Check if `tauri.conf.json5` exists + a. Parse it with `json5` + b. Return error if all above steps failed +3. Return error if all above steps failed + +- [995de57a](https://www.github.com/tauri-apps/tauri/commit/995de57a76cf51215277673e526d7ec32b86b564) Add seamless support for using JSON5 in the config file ([#47](https://www.github.com/tauri-apps/tauri/pull/47)) on 2022-02-03 +- Move the copying of resources and sidecars from `cli.rs` to `tauri-build` so using the Cargo CLI directly processes the files for the application execution in development. + - [5eb72c24](https://www.github.com/tauri-apps/tauri/commit/5eb72c24deddf5a01093bea96b90c0d8806afc3f) refactor: copy resources and sidecars on the Cargo build script ([#3357](https://www.github.com/tauri-apps/tauri/pull/3357)) on 2022-02-08 +- **Breaking change**\* Remove default webview window when `tauri.conf.json > tauri > windows` is not set. + - [c119060e](https://www.github.com/tauri-apps/tauri/commit/c119060e3d9a5a824639fb6b3c45a87e7a62e4e2) refactor(core): empty default value for config > tauri > windows ([#3380](https://www.github.com/tauri-apps/tauri/pull/3380)) on 2022-02-10 +- The minimum Rust version is now `1.56`. + - [a9dfc015](https://www.github.com/tauri-apps/tauri/commit/a9dfc015505afe91281c2027954ffcc588b1a59c) feat: update to edition 2021 and set minimum rust to 1.56 ([#2789](https://www.github.com/tauri-apps/tauri/pull/2789)) on 2021-10-22 +- Adds `scope` glob array config under `tauri > allowlist > fs`. + Adds `assetScope` glob array config under `tauri > allowlist > protocol`. + Adds `scope` URL array config under `tauri > allowlist > http`. + - [7920ff14](https://www.github.com/tauri-apps/tauri/commit/7920ff14e6424079c48ea5645d9aa13e7a272b87) feat: scope the `fs` API and the `asset` protocol \[TRI-026] \[TRI-010] \[TRI-011] ([#10](https://www.github.com/tauri-apps/tauri/pull/10)) on 2022-01-09 + - [0ad1c651](https://www.github.com/tauri-apps/tauri/commit/0ad1c6515f696fadefddbf133a9561836b3d5934) feat(core): add `http` allowlist scope \[TRI-008] ([#24](https://www.github.com/tauri-apps/tauri/pull/24)) on 2021-10-29 +- The `shell` allowlist now includes a `sidecar` flag, which enables the use of the `shell` API to execute sidecars. + - [eed01728](https://www.github.com/tauri-apps/tauri/commit/eed017287fed2ade689af4268e8b63b9c9f2e585) feat(core): add `shell > sidecar` allowlist and `process` feature flag \[TRI-037] ([#18](https://www.github.com/tauri-apps/tauri/pull/18)) on 2021-10-24 +- Force updater endpoint URL to use `https` on release builds. + - [c077f449](https://www.github.com/tauri-apps/tauri/commit/c077f449270cffbf7956b1af81e1fb237ebf564a) feat: force endpoint URL to use https on release \[TRI-015] ([#41](https://www.github.com/tauri-apps/tauri/pull/41)) on 2022-01-09 + +## \[1.0.0-beta.3] + +- Fixes minimum window height being used as maximum height. + - [e3f99165](https://www.github.com/tauri-apps/tauri/commit/e3f9916526b226866137cb663e5cafab2b6a0e01) fix(core) minHeight being used as maxHeight ([#2247](https://www.github.com/tauri-apps/tauri/pull/2247)) on 2021-07-19 +- Implement `Debug` on public API structs and enums. + - [fa9341ba](https://www.github.com/tauri-apps/tauri/commit/fa9341ba18ba227735341530900714dba0f27291) feat(core): implement `Debug` on public API structs/enums, closes [#2292](https://www.github.com/tauri-apps/tauri/pull/2292) ([#2387](https://www.github.com/tauri-apps/tauri/pull/2387)) on 2021-08-11 +- Keep original value on `config > package > productName` on Linux (previously converted to kebab-case). + - [3f039cb8](https://www.github.com/tauri-apps/tauri/commit/3f039cb8a308b0f18deaa37d7cfb1cc50d308d0e) fix: keep original `productName` for .desktop `Name` field, closes [#2295](https://www.github.com/tauri-apps/tauri/pull/2295) ([#2384](https://www.github.com/tauri-apps/tauri/pull/2384)) on 2021-08-10 +- Inject the invoke key on regular `` tags. + - [d0142e87](https://www.github.com/tauri-apps/tauri/commit/d0142e87ddf5231fd46e2cbe4769bb16f3fe01e9) fix(core): invoke key injection on regular JS scripts, closes [#2342](https://www.github.com/tauri-apps/tauri/pull/2342) ([#2344](https://www.github.com/tauri-apps/tauri/pull/2344)) on 2021-08-03 + +## \[1.0.0-beta.2] + +- Inject invoke key on `script` tags with `type="module"`. + - [f03eea9c](https://www.github.com/tauri-apps/tauri/commit/f03eea9c9b964709532afbc4d1dd343b3fd96081) feat(core): inject invoke key on `"#; + + 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!( + 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 0000000..d17ee8d --- /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(), + "".to_string() + ); + } + + #[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/io.rs b/crates/tauri-utils/src/io.rs new file mode 100644 index 0000000..be4fd0d --- /dev/null +++ b/crates/tauri-utils/src/io.rs @@ -0,0 +1,47 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! IO helpers. + +use std::io::BufRead; + +/// Read all bytes until a newline (the `0xA` byte) or a carriage return (`\r`) is reached, and append them to the provided buffer. +/// +/// Adapted from . +pub fn read_line(r: &mut R, buf: &mut Vec) -> std::io::Result { + let mut read = 0; + loop { + let (done, used) = { + let available = match r.fill_buf() { + Ok(n) => n, + Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + + Err(e) => return Err(e), + }; + match memchr::memchr(b'\n', available) { + Some(i) => { + let end = i + 1; + buf.extend_from_slice(&available[..end]); + (true, end) + } + None => match memchr::memchr(b'\r', available) { + Some(i) => { + let end = i + 1; + buf.extend_from_slice(&available[..end]); + (true, end) + } + None => { + buf.extend_from_slice(available); + (false, available.len()) + } + }, + } + }; + r.consume(used); + read += used; + if done || used == 0 { + return Ok(read); + } + } +} diff --git a/crates/tauri-utils/src/lib.rs b/crates/tauri-utils/src/lib.rs new file mode 100644 index 0000000..ff388d5 --- /dev/null +++ b/crates/tauri-utils/src/lib.rs @@ -0,0 +1,407 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! This crate contains common code that is reused in many places and offers useful utilities like parsing configuration files, detecting platform triples, injecting the CSP, and managing assets. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" +)] +#![warn(missing_docs, rust_2018_idioms)] +#![allow(clippy::deprecated_semver)] + +use std::{ + ffi::OsString, + fmt::Display, + path::{Path, PathBuf}, +}; + +use semver::Version; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +pub mod acl; +pub mod assets; +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; +pub mod plugin; +/// Prepare application resources and sidecars. +#[cfg(feature = "resources")] +pub mod resources; +#[cfg(any(feature = "build", feature = "build-2"))] +pub mod tokens; + +#[cfg(any(feature = "build", feature = "build-2"))] +pub mod build; + +/// Application pattern. +pub mod pattern; + +/// `tauri::App` package information. +#[derive(Debug, Clone)] +pub struct PackageInfo { + /// App name + pub name: String, + /// App version + pub version: Version, + /// The crate authors. + pub authors: &'static str, + /// The crate description. + pub description: &'static str, + /// The crate name. + pub crate_name: &'static str, +} + +#[allow(deprecated)] +mod window_effects { + use super::*; + + #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)] + #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] + #[serde(rename_all = "camelCase")] + #[non_exhaustive] + /// Platform-specific window effects + pub enum WindowEffect { + /// A default material appropriate for the view's effectiveAppearance. **macOS 10.14-** + #[deprecated( + since = "macOS 10.14", + note = "You should instead choose an appropriate semantic material." + )] + AppearanceBased, + /// **macOS 10.14-** + #[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")] + Light, + /// **macOS 10.14-** + #[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")] + Dark, + /// **macOS 10.14-** + #[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")] + MediumLight, + /// **macOS 10.14-** + #[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")] + UltraDark, + /// **macOS 10.10+** + Titlebar, + /// **macOS 10.10+** + Selection, + /// **macOS 10.11+** + Menu, + /// **macOS 10.11+** + Popover, + /// **macOS 10.11+** + Sidebar, + /// **macOS 10.14+** + HeaderView, + /// **macOS 10.14+** + Sheet, + /// **macOS 10.14+** + WindowBackground, + /// **macOS 10.14+** + HudWindow, + /// **macOS 10.14+** + FullScreenUI, + /// **macOS 10.14+** + Tooltip, + /// **macOS 10.14+** + ContentBackground, + /// **macOS 10.14+** + UnderWindowBackground, + /// **macOS 10.14+** + UnderPageBackground, + /// **macOS 26.0+** + LiquidGlassRegular, + /// **macOS 26.0+** + LiquidGlassClear, + /// Mica effect that matches the system dark preference **Windows 11 Only** + Mica, + /// Mica effect with dark mode but only if dark mode is enabled on the system **Windows 11 Only** + MicaDark, + /// Mica effect with light mode **Windows 11 Only** + MicaLight, + /// Tabbed effect that matches the system dark preference **Windows 11 Only** + Tabbed, + /// Tabbed effect with dark mode but only if dark mode is enabled on the system **Windows 11 Only** + TabbedDark, + /// Tabbed effect with light mode **Windows 11 Only** + TabbedLight, + /// **Windows 7/10/11(22H1) Only** + /// + /// ## Notes + /// + /// This effect has bad performance when resizing/dragging the window on Windows 11 build 22621. + Blur, + /// **Windows 10/11 Only** + /// + /// ## Notes + /// + /// This effect has bad performance when resizing/dragging the window on Windows 10 v1903+ and Windows 11 build 22000. + Acrylic, + } + + /// Window effect state **macOS only** + /// + /// + #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)] + #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] + #[serde(rename_all = "camelCase")] + pub enum WindowEffectState { + /// Make window effect state follow the window's active state + FollowsWindowActiveState, + /// Make window effect state always active + Active, + /// Make window effect state always inactive + Inactive, + } +} + +pub use window_effects::{WindowEffect, WindowEffectState}; + +/// How the window title bar should be displayed on macOS. +#[derive(Debug, Clone, PartialEq, Eq, Copy, Default)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[non_exhaustive] +pub enum TitleBarStyle { + /// A normal title bar. + #[default] + Visible, + /// Makes the title bar transparent, so the window background color is shown instead. + /// + /// Useful if you don't need to have actual HTML under the title bar. This lets you avoid the caveats of using `TitleBarStyle::Overlay`. Will be more useful when Tauri lets you set a custom window background color. + Transparent, + /// Shows the title bar as a transparent overlay over the window's content. + /// + /// Keep in mind: + /// - The height of the title bar is different on different OS versions, which can lead to window the controls and title not being where you don't expect. + /// - You need to define a custom drag region to make your window draggable, however due to a limitation you can't drag the window when it's not in focus . + /// - The color of the window title depends on the system theme. + Overlay, +} + +impl Serialize for TitleBarStyle { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +impl<'de> Deserialize<'de> for TitleBarStyle { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(match s.to_lowercase().as_str() { + "transparent" => Self::Transparent, + "overlay" => Self::Overlay, + _ => Self::Visible, + }) + } +} + +impl Display for TitleBarStyle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Visible => "Visible", + Self::Transparent => "Transparent", + Self::Overlay => "Overlay", + } + ) + } +} + +/// System theme. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[non_exhaustive] +pub enum Theme { + /// Light theme. + Light, + /// Dark theme. + Dark, +} + +impl Serialize for Theme { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +impl<'de> Deserialize<'de> for Theme { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(match s.to_lowercase().as_str() { + "dark" => Self::Dark, + _ => Self::Light, + }) + } +} + +impl Display for Theme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Light => "light", + Self::Dark => "dark", + } + ) + } +} + +/// Information about environment variables. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Env { + /// The APPIMAGE environment variable. + #[cfg(target_os = "linux")] + pub appimage: Option, + /// The APPDIR environment variable. + #[cfg(target_os = "linux")] + pub appdir: Option, + /// The command line arguments of the current process. + pub args_os: Vec, +} + +#[allow(clippy::derivable_impls)] +impl Default for Env { + fn default() -> Self { + let args_os = std::env::args_os().collect(); + #[cfg(target_os = "linux")] + { + let env = Self { + #[cfg(target_os = "linux")] + appimage: std::env::var_os("APPIMAGE"), + #[cfg(target_os = "linux")] + appdir: std::env::var_os("APPDIR"), + args_os, + }; + if env.appimage.is_some() || env.appdir.is_some() { + // validate that we're actually running on an AppImage + // an AppImage is mounted to `/$TEMPDIR/.mount_${appPrefix}${hash}` + // see + // note that it is safe to use `std::env::current_exe` here since we just loaded an AppImage. + let is_temp = std::env::current_exe() + .map(|p| { + p.display() + .to_string() + .starts_with(&format!("{}/.mount_", std::env::temp_dir().display())) + }) + .unwrap_or(true); + + if !is_temp { + log::warn!( + "`APPDIR` or `APPIMAGE` environment variable found but this application was not detected as an AppImage; this might be a security issue." + ); + } + } + env + } + #[cfg(not(target_os = "linux"))] + { + Self { args_os } + } + } +} + +/// The result type of `tauri-utils`. +pub type Result = std::result::Result; + +/// The error type of `tauri-utils`. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum Error { + /// Target triple architecture error + #[error("Unable to determine target-architecture")] + Architecture, + /// Target triple OS error + #[error("Unable to determine target-os")] + Os, + /// Target triple environment error + #[error("Unable to determine target-environment")] + Environment, + /// Tried to get resource on an unsupported platform + #[error("Unsupported platform for reading resources")] + UnsupportedPlatform, + /// Get parent process error + #[error("Could not get parent process")] + ParentProcess, + /// Get parent process PID error + #[error("Could not get parent PID")] + ParentPid, + /// Get child process error + #[error("Could not get child process")] + ChildProcess, + /// IO error + #[error("{0}")] + Io(#[from] std::io::Error), + /// Invalid pattern. + #[error("invalid pattern `{0}`. Expected either `brownfield` or `isolation`.")] + InvalidPattern(String), + /// Invalid glob pattern. + #[cfg(feature = "resources")] + #[error("{0}")] + GlobPattern(#[from] glob::PatternError), + /// Failed to use glob pattern. + #[cfg(feature = "resources")] + #[error("`{0}`")] + Glob(#[from] glob::GlobError), + /// Glob pattern did not find any results. + #[cfg(feature = "resources")] + #[error("glob pattern {0} path not found or didn't match any files.")] + GlobPathNotFound(String), + /// Error walking directory. + #[cfg(feature = "resources")] + #[error("{0}")] + WalkdirError(#[from] walkdir::Error), + /// Not allowed to walk dir. + #[cfg(feature = "resources")] + #[error( + "could not walk directory `{0}`, try changing `allow_walk` to true on the `ResourcePaths` constructor." + )] + NotAllowedToWalkDir(std::path::PathBuf), + /// Resource path doesn't exist + #[cfg(feature = "resources")] + #[error("resource path `{0}` doesn't exist")] + ResourcePathNotFound(std::path::PathBuf), +} + +/// Reconstructs a path from its components using the platform separator then converts it to String and removes UNC prefixes on Windows if it exists. +pub fn display_path>(p: P) -> String { + dunce::simplified(&p.as_ref().components().collect::()) + .display() + .to_string() +} + +/// Write the file only if the content of the existing file (if any) is different. +/// +/// This will always write unless the file exists with identical content. +pub fn write_if_changed(path: P, content: C) -> std::io::Result<()> +where + P: AsRef, + C: AsRef<[u8]>, +{ + if std::fs::read(&path).is_ok_and(|existing| existing == content.as_ref()) { + return Ok(()); + } + + std::fs::write(path, content) +} diff --git a/crates/tauri-utils/src/mime_type.rs b/crates/tauri-utils/src/mime_type.rs new file mode 100644 index 0000000..a4d001f --- /dev/null +++ b/crates/tauri-utils/src/mime_type.rs @@ -0,0 +1,154 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Determine a mime type from a URI or file contents. + +use std::fmt; + +const MIMETYPE_PLAIN: &str = "text/plain"; + +/// [Web Compatible MimeTypes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#important_mime_types_for_web_developers) +#[allow(missing_docs)] +pub enum MimeType { + Css, + Csv, + Html, + Ico, + Js, + Json, + Jsonld, + Mp4, + OctetStream, + Rtf, + Svg, + Txt, +} + +impl std::fmt::Display for MimeType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mime = match self { + MimeType::Css => "text/css", + MimeType::Csv => "text/csv", + MimeType::Html => "text/html", + MimeType::Ico => "image/vnd.microsoft.icon", + MimeType::Js => "text/javascript", + MimeType::Json => "application/json", + MimeType::Jsonld => "application/ld+json", + MimeType::Mp4 => "video/mp4", + MimeType::OctetStream => "application/octet-stream", + MimeType::Rtf => "application/rtf", + MimeType::Svg => "image/svg+xml", + MimeType::Txt => MIMETYPE_PLAIN, + }; + write!(f, "{mime}") + } +} + +impl MimeType { + /// parse a URI suffix to convert text/plain mimeType to their actual web compatible mimeType. + pub fn parse_from_uri(uri: &str) -> MimeType { + Self::parse_from_uri_with_fallback(uri, Self::Html) + } + + /// parse a URI suffix to convert text/plain mimeType to their actual web compatible mimeType with specified fallback for unknown file extensions. + pub fn parse_from_uri_with_fallback(uri: &str, fallback: MimeType) -> MimeType { + let suffix = uri.split('.').next_back(); + match suffix { + Some("bin") => Self::OctetStream, + Some("css" | "less" | "sass" | "styl") => Self::Css, + Some("csv") => Self::Csv, + Some("html") => Self::Html, + Some("ico") => Self::Ico, + Some("js") => Self::Js, + Some("json") => Self::Json, + Some("jsonld") => Self::Jsonld, + Some("mjs") => Self::Js, + Some("mp4") => Self::Mp4, + Some("rtf") => Self::Rtf, + Some("svg") => Self::Svg, + Some("txt") => Self::Txt, + // Assume HTML when a TLD is found for eg. `wry:://tauri.app` | `wry://hello.com` + Some(_) => fallback, + // using octet stream according to this: + // + None => Self::OctetStream, + } + } + + /// infer mimetype from content (or) URI if needed. + pub fn parse(content: &[u8], uri: &str) -> String { + Self::parse_with_fallback(content, uri, Self::Html) + } + /// infer mimetype from content (or) URI if needed with specified fallback for unknown file extensions. + pub fn parse_with_fallback(content: &[u8], uri: &str, fallback: MimeType) -> String { + let mime = if uri.ends_with(".svg") { + // when reading svg, we can't use `infer` + None + } else { + infer::get(content).map(|info| info.mime_type()) + }; + + match mime { + Some(mime) if mime == MIMETYPE_PLAIN => { + Self::parse_from_uri_with_fallback(uri, fallback).to_string() + } + None => Self::parse_from_uri_with_fallback(uri, fallback).to_string(), + Some(mime) => mime.to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_parse_mimetype_from_uri() { + let css = MimeType::parse_from_uri( + "https://unpkg.com/browse/bootstrap@4.1.0/dist/css/bootstrap-grid.css", + ) + .to_string(); + assert_eq!(css, "text/css".to_string()); + + let csv: String = MimeType::parse_from_uri("https://example.com/random.csv").to_string(); + assert_eq!(csv, "text/csv".to_string()); + + let ico: String = + MimeType::parse_from_uri("https://icons.duckduckgo.com/ip3/microsoft.com.ico").to_string(); + assert_eq!(ico, String::from("image/vnd.microsoft.icon")); + + let html: String = MimeType::parse_from_uri("https://tauri.app/index.html").to_string(); + assert_eq!(html, String::from("text/html")); + + let js: String = + MimeType::parse_from_uri("https://unpkg.com/react@17.0.1/umd/react.production.min.js") + .to_string(); + assert_eq!(js, "text/javascript".to_string()); + + let json: String = + MimeType::parse_from_uri("https://unpkg.com/browse/react@17.0.1/build-info.json").to_string(); + assert_eq!(json, String::from("application/json")); + + let jsonld: String = MimeType::parse_from_uri("https:/example.com/hello.jsonld").to_string(); + assert_eq!(jsonld, String::from("application/ld+json")); + + let mjs: String = MimeType::parse_from_uri("https://example.com/bundled.mjs").to_string(); + assert_eq!(mjs, String::from("text/javascript")); + + let mp4: String = MimeType::parse_from_uri("https://example.com/video.mp4").to_string(); + assert_eq!(mp4, String::from("video/mp4")); + + let rtf: String = MimeType::parse_from_uri("https://example.com/document.rtf").to_string(); + assert_eq!(rtf, String::from("application/rtf")); + + let svg: String = MimeType::parse_from_uri("https://example.com/picture.svg").to_string(); + assert_eq!(svg, String::from("image/svg+xml")); + + let txt: String = MimeType::parse_from_uri("https://example.com/file.txt").to_string(); + assert_eq!(txt, String::from("text/plain")); + + let custom_scheme = MimeType::parse_from_uri("wry://tauri.app").to_string(); + assert_eq!(custom_scheme, String::from("text/html")); + } +} diff --git a/crates/tauri-utils/src/pattern/isolation.js b/crates/tauri-utils/src/pattern/isolation.js new file mode 100644 index 0000000..586d080 --- /dev/null +++ b/crates/tauri-utils/src/pattern/isolation.js @@ -0,0 +1,154 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * IMPORTANT: See ipc.js for the main frame implementation. + * main frame -> isolation frame = isolation payload + * isolation frame -> main frame = isolation message + */ + +;(async function () { + /** + * Sends the message to the isolation frame. + * @param {any} message + */ + function sendMessage(message) { + window.parent.postMessage(message, '*') + } + + /** + * @type {string} - The main frame origin. + */ + const origin = __TEMPLATE_origin__ + + /** + * @type {Uint8Array} - Injected by Tauri during runtime + */ + const aesGcmKeyRaw = new Uint8Array(__TEMPLATE_runtime_aes_gcm_key__) + + /** + * @type {CryptoKey} + */ + const aesGcmKey = await window.crypto.subtle.importKey( + 'raw', + aesGcmKeyRaw, + 'AES-GCM', + false, + ['encrypt'] + ) + + /** + * @param {object} data + * @return {Promise<{nonce: number[], payload: number[]}>} + */ + async function encrypt(payload) { + const algorithm = Object.create(null) + algorithm.name = 'AES-GCM' + algorithm.iv = window.crypto.getRandomValues(new Uint8Array(12)) + + const { contentType, data } = __RAW_process_ipc_message_fn__(payload) + + const message = + typeof data === 'string' + ? new TextEncoder().encode(data) + : ArrayBuffer.isView(data) || data instanceof ArrayBuffer + ? data + : new Uint8Array(data) + + return window.crypto.subtle + .encrypt(algorithm, aesGcmKey, message) + .then((payload) => { + const result = Object.create(null) + result.nonce = Array.from(new Uint8Array(algorithm.iv)) + result.payload = Array.from(new Uint8Array(payload)) + result.contentType = contentType + return result + }) + } + + /** + * Detects if a message event is a valid isolation message. + * + * @param {MessageEvent} event - a message event that is expected to be an isolation message + * @return {boolean} - if the event was a valid isolation message + */ + function isIsolationMessage(data) { + if (typeof data === 'object' && typeof data.payload === 'object') { + const keys = data.payload ? Object.keys(data.payload) : [] + return ( + keys.length > 0 + && keys.every( + (key) => key === 'nonce' || key === 'payload' || key === 'contentType' + ) + ) + } + return false + } + + /** + * Detect if a message event is a valid isolation payload. + * + * @param {MessageEvent} event - a message event that is expected to be an isolation payload + * @return boolean + */ + function isIsolationPayload(data) { + return ( + typeof data === 'object' + && 'callback' in data + && 'error' in data + && !isIsolationMessage(data) + ) + } + + /** + * Handle incoming payload events. + * @param {MessageEvent} event + */ + async function payloadHandler(event) { + if (event.origin !== origin || !isIsolationPayload(event.data)) { + return + } + + let data = event.data + + if (typeof window.__TAURI_ISOLATION_HOOK__ === 'function') { + // await even if it's not async so that we can support async ones + data = await window.__TAURI_ISOLATION_HOOK__(data) + } + + const message = Object.create(null) + message.cmd = data.cmd + message.callback = data.callback + message.error = data.error + message.options = data.options + message.payload = await encrypt(data.payload) + sendMessage(message) + } + + window.addEventListener('message', payloadHandler, false) + + /** + * @type {number} - How many milliseconds to wait between ready checks + */ + const readyIntervalMs = 50 + + /** + * Wait until this Isolation context is ready to receive messages, and let the main frame know. + */ + function waitUntilReady() { + // consider either a function or an explicitly set null value as the ready signal + if ( + typeof window.__TAURI_ISOLATION_HOOK__ === 'function' + || window.__TAURI_ISOLATION_HOOK__ === null + ) { + sendMessage('__TAURI_ISOLATION_READY__') + } else { + setTimeout(waitUntilReady, readyIntervalMs) + } + } + + setTimeout(waitUntilReady, readyIntervalMs) +})() + +document.currentScript?.remove() diff --git a/crates/tauri-utils/src/pattern/isolation.rs b/crates/tauri-utils/src/pattern/isolation.rs new file mode 100644 index 0000000..223cca6 --- /dev/null +++ b/crates/tauri-utils/src/pattern/isolation.rs @@ -0,0 +1,171 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::array::TryFromSliceError; +use std::borrow::Cow; +use std::fmt::{Debug, Formatter}; +use std::string::FromUtf8Error; + +use aes_gcm::aead::Aead; +use aes_gcm::{Aes256Gcm, KeyInit, Nonce}; +use getrandom::Error as CsprngError; +use serialize_to_javascript::{Template, default_template}; + +/// The style for the isolation iframe. +pub const IFRAME_STYLE: &str = "#__tauri_isolation__ { display: none !important }"; + +/// Errors that can occur during Isolation keys generation. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum Error { + /// Something went wrong with the CSPRNG. + #[error("CSPRNG error")] + Csprng(#[from] CsprngError), + + /// Something went wrong with decrypting an AES-GCM payload + #[error("AES-GCM")] + Aes, + + /// Nonce was not 96 bits + #[error("Nonce: {0}")] + NonceSize(#[from] TryFromSliceError), + + /// Payload was not valid utf8 + #[error("{0}")] + Utf8(#[from] FromUtf8Error), + + /// Invalid json format + #[error("{0}")] + Json(#[from] serde_json::Error), +} + +/// A formatted AES-GCM cipher instance along with the key used to initialize it. +#[derive(Clone)] +pub struct AesGcmPair { + raw: [u8; 32], + key: Aes256Gcm, +} + +impl Debug for AesGcmPair { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "AesGcmPair(...)") + } +} + +impl AesGcmPair { + fn new() -> Result { + let mut raw = [0u8; 32]; + getrandom::fill(&mut raw)?; + let key = aes_gcm::Key::::from_slice(&raw); + Ok(Self { + raw, + key: Aes256Gcm::new(key), + }) + } + + /// The raw value used to create the AES-GCM key + pub fn raw(&self) -> &[u8; 32] { + &self.raw + } + + /// The formatted AES-GCM key + pub fn key(&self) -> &Aes256Gcm { + &self.key + } + + #[doc(hidden)] + pub fn encrypt(&self, nonce: &[u8; 12], payload: &[u8]) -> Result, Error> { + self + .key + .encrypt(nonce.into(), payload) + .map_err(|_| self::Error::Aes) + } +} + +/// All cryptographic keys required for Isolation encryption +#[derive(Debug, Clone)] +pub struct Keys { + /// AES-GCM key + aes_gcm: AesGcmPair, +} + +impl Keys { + /// Securely generate required keys for Isolation encryption. + pub fn new() -> Result { + AesGcmPair::new().map(|aes_gcm| Self { aes_gcm }) + } + + /// The AES-GCM data (and raw data). + pub fn aes_gcm(&self) -> &AesGcmPair { + &self.aes_gcm + } + + /// Decrypts a message using the generated keys. + pub fn decrypt(&self, raw: RawIsolationPayload<'_>) -> Result, Error> { + let RawIsolationPayload { nonce, payload, .. } = raw; + let nonce: [u8; 12] = nonce.as_ref().try_into()?; + self + .aes_gcm + .key + .decrypt(Nonce::from_slice(&nonce), payload.as_ref()) + .map_err(|_| self::Error::Aes) + } +} + +/// Raw representation of +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RawIsolationPayload<'a> { + nonce: Cow<'a, [u8]>, + payload: Cow<'a, [u8]>, + content_type: Cow<'a, str>, +} + +impl<'a> RawIsolationPayload<'a> { + /// Content type of this payload. + pub fn content_type(&self) -> &Cow<'a, str> { + &self.content_type + } +} + +impl<'a> TryFrom<&'a Vec> for RawIsolationPayload<'a> { + type Error = Error; + + fn try_from(value: &'a Vec) -> Result { + serde_json::from_slice(value).map_err(Into::into) + } +} + +/// The Isolation JavaScript template meant to be injected during codegen. +/// +/// Note: This struct is not considered part of the stable API +#[derive(Template)] +#[default_template("isolation.js")] +pub struct IsolationJavascriptCodegen { + // this template intentionally does not include the runtime field +} + +/// The Isolation JavaScript template meant to be injected during runtime. +/// +/// Note: This struct is not considered part of the stable API +#[derive(Template)] +#[default_template("isolation.js")] +pub struct IsolationJavascriptRuntime<'a> { + /// The key used on the Rust backend and the Isolation Javascript + pub runtime_aes_gcm_key: &'a [u8; 32], + /// The origin the isolation application is expecting messages from. + pub origin: String, + /// The function that processes the IPC message. + #[raw] + pub process_ipc_message_fn: &'a str, +} + +#[cfg(test)] +mod test { + #[test] + fn create_keys() -> Result<(), Box> { + let _ = super::Keys::new()?; + Ok(()) + } +} diff --git a/crates/tauri-utils/src/pattern/mod.rs b/crates/tauri-utils/src/pattern/mod.rs new file mode 100644 index 0000000..97ca495 --- /dev/null +++ b/crates/tauri-utils/src/pattern/mod.rs @@ -0,0 +1,7 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/// Handling the Tauri "Isolation" Pattern. +#[cfg(feature = "isolation")] +pub mod isolation; diff --git a/crates/tauri-utils/src/platform.rs b/crates/tauri-utils/src/platform.rs new file mode 100644 index 0000000..9a148d2 --- /dev/null +++ b/crates/tauri-utils/src/platform.rs @@ -0,0 +1,442 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Platform helper functions. + +use std::{fmt::Display, path::PathBuf}; + +use serde::{Deserialize, Serialize}; + +use crate::{Env, PackageInfo, config::BundleType}; + +mod starting_binary; + +/// URI prefix of a Tauri asset. +/// +/// This is referenced in the Tauri Android library, +/// which resolves these assets to a file descriptor. +#[cfg(target_os = "android")] +pub const ANDROID_ASSET_PROTOCOL_URI_PREFIX: &str = "asset://localhost/"; + +/// Resource id of the application icon that `tauri-build` embeds into Windows executables +/// and that `tauri::image::Image::from_app_icon_resource` reads back. +/// +/// `32512` has no special meaning here: it was picked because we misunderstood +/// `IDI_APPLICATION` (`MAKEINTRESOURCE(32512)`) to be the id an application icon must use, +/// which is not the case. See . +pub const WINDOWS_APP_ICON_RESOURCE_ID: u16 = 32512; + +/// Platform target. +#[derive(PartialEq, Eq, Copy, Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub enum Target { + /// MacOS. + #[serde(rename = "macOS")] + MacOS, + /// Windows. + Windows, + /// Linux. + Linux, + /// Android. + Android, + /// iOS. + #[serde(rename = "iOS")] + Ios, +} + +impl Display for Target { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::MacOS => "macOS", + Self::Windows => "windows", + Self::Linux => "linux", + Self::Android => "android", + Self::Ios => "iOS", + } + ) + } +} + +impl Target { + /// Parses the target from the given target triple. + pub fn from_triple(target: &str) -> Self { + if target.contains("darwin") { + Self::MacOS + } else if target.contains("windows") { + Self::Windows + } else if target.contains("android") { + Self::Android + } else if target.contains("ios") { + Self::Ios + } else { + Self::Linux + } + } + + /// Gets the current build target. + pub fn current() -> Self { + if cfg!(target_os = "macos") { + Self::MacOS + } else if cfg!(target_os = "windows") { + Self::Windows + } else if cfg!(target_os = "ios") { + Self::Ios + } else if cfg!(target_os = "android") { + Self::Android + } else { + Self::Linux + } + } + + /// Whether the target is mobile or not. + pub fn is_mobile(&self) -> bool { + matches!(self, Target::Android | Target::Ios) + } + + /// Whether the target is desktop or not. + pub fn is_desktop(&self) -> bool { + !self.is_mobile() + } +} + +/// Retrieves the currently running binary's path, taking into account security considerations. +/// +/// The path is cached as soon as possible (before even `main` runs) and that value is returned +/// repeatedly instead of fetching the path every time. It is possible for the path to not be found, +/// or explicitly disabled (see following macOS specific behavior). +/// +/// # Platform-specific behavior +/// +/// On `macOS`, this function will return an error if the original path contained any symlinks +/// due to less protection on macOS regarding symlinks. This behavior can be disabled by setting the +/// `process-relaunch-dangerous-allow-symlink-macos` feature, although it is *highly discouraged*. +/// +/// # Security +/// +/// If the above platform-specific behavior does **not** take place, this function uses the +/// following resolution. +/// +/// We canonicalize the path we received from [`std::env::current_exe`] to resolve any soft links. +/// This avoids the usual issue of needing the file to exist at the passed path because a valid +/// current executable result for our purpose should always exist. Notably, +/// [`std::env::current_exe`] also has a security section that goes over a theoretical attack using +/// hard links. Let's cover some specific topics that relate to different ways an attacker might +/// try to trick this function into returning the wrong binary path. +/// +/// ## Symlinks ("Soft Links") +/// +/// [`std::path::Path::canonicalize`] is used to resolve symbolic links to the original path, +/// including nested symbolic links (`link2 -> link1 -> bin`). On macOS, any results that include +/// a symlink are rejected by default due to lesser symlink protections. This can be disabled, +/// **although discouraged**, with the `process-relaunch-dangerous-allow-symlink-macos` feature. +/// +/// ## Hard Links +/// +/// A [Hard Link] is a named entry that points to a file in the file system. +/// On most systems, this is what you would think of as a "file". The term is +/// used on filesystems that allow multiple entries to point to the same file. +/// The linked [Hard Link] Wikipedia page provides a decent overview. +/// +/// In short, unless the attacker was able to create the link with elevated +/// permissions, it should generally not be possible for them to hard link +/// to a file they do not have permissions to - with exception to possible +/// operating system exploits. +/// +/// There are also some platform-specific information about this below. +/// +/// ### Windows +/// +/// Windows requires a permission to be set for the user to create a symlink +/// or a hard link, regardless of ownership status of the target. Elevated +/// permissions users have the ability to create them. +/// +/// ### macOS +/// +/// macOS allows for the creation of symlinks and hard links to any file. +/// Accessing through those links will fail if the user who owns the links +/// does not have the proper permissions on the original file. +/// +/// ### Linux +/// +/// Linux allows for the creation of symlinks to any file. Accessing the +/// symlink will fail if the user who owns the symlink does not have the +/// proper permissions on the original file. +/// +/// Linux additionally provides a kernel hardening feature since version +/// 3.6 (30 September 2012). Most distributions since then have enabled +/// the protection (setting `fs.protected_hardlinks = 1`) by default, which +/// means that a vast majority of desktop Linux users should have it enabled. +/// **The feature prevents the creation of hardlinks that the user does not own +/// or have read/write access to.** [See the patch that enabled this]. +/// +/// [Hard Link]: https://en.wikipedia.org/wiki/Hard_link +/// [See the patch that enabled this]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=800179c9b8a1e796e441674776d11cd4c05d61d7 +pub fn current_exe() -> std::io::Result { + self::starting_binary::STARTING_BINARY.cloned() +} + +/// Try to determine the current target triple. +/// +/// Returns a target triple (e.g. `x86_64-unknown-linux-gnu` or `i686-pc-windows-msvc`) or an +/// `Error::Config` if the current config cannot be determined or is not some combination of the +/// following values: +/// `linux, mac, windows` -- `i686, x86, armv7` -- `gnu, musl, msvc` +/// +/// * Errors: +/// * Unexpected system config +pub fn target_triple() -> crate::Result { + let arch = if cfg!(target_arch = "x86") { + "i686" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else if cfg!(target_arch = "arm") { + "armv7" + } else if cfg!(target_arch = "aarch64") { + "aarch64" + } else if cfg!(target_arch = "riscv64") { + "riscv64" + } else { + return Err(crate::Error::Architecture); + }; + + let os = if cfg!(target_os = "linux") { + "unknown-linux" + } else if cfg!(target_os = "macos") { + "apple-darwin" + } else if cfg!(target_os = "windows") { + "pc-windows" + } else if cfg!(target_os = "freebsd") { + "unknown-freebsd" + } else { + return Err(crate::Error::Os); + }; + + let os = if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") { + String::from(os) + } else { + let env = if cfg!(target_env = "gnu") { + "gnu" + } else if cfg!(target_env = "musl") { + "musl" + } else if cfg!(target_env = "msvc") { + "msvc" + } else { + return Err(crate::Error::Environment); + }; + + format!("{os}-{env}") + }; + + Ok(format!("{arch}-{os}")) +} + +#[cfg(all(not(test), not(target_os = "android")))] +fn is_cargo_output_directory(path: &std::path::Path) -> bool { + path.join(".cargo-lock").exists() +} + +#[cfg(test)] +const CARGO_OUTPUT_DIRECTORIES: &[&str] = &["debug", "release", "custom-profile"]; + +#[cfg(test)] +fn is_cargo_output_directory(path: &std::path::Path) -> bool { + let Some(last_component) = path.components().next_back() else { + return false; + }; + CARGO_OUTPUT_DIRECTORIES + .iter() + .any(|dirname| &last_component.as_os_str() == dirname) +} + +/// Computes the resource directory of the current environment. +/// +/// ## Platform-specific +/// +/// - **Windows:** Resolves to the directory that contains the main executable. +/// - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to +/// the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`. +/// If not running in an AppImage, the path is `/usr/lib/${exe_name}`. +/// When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`. +/// - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app). +/// - **iOS:** Resolves to `${exe_dir}/assets`. +/// - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path, +/// we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/), +/// with that, you can read the files through [`FsExt::fs`](https://docs.rs/tauri-plugin-fs/latest/tauri_plugin_fs/trait.FsExt.html#tymethod.fs) +/// like this: `app.fs().read_to_string(app.path().resource_dir().unwrap().join("resource"));` +pub fn resource_dir(package_info: &PackageInfo, env: &Env) -> crate::Result { + #[cfg(target_os = "android")] + return resource_dir_android(package_info, env); + #[cfg(not(target_os = "android"))] + { + let exe = current_exe()?; + resource_dir_from(exe, package_info, env) + } +} + +#[cfg(target_os = "android")] +fn resource_dir_android(_package_info: &PackageInfo, _env: &Env) -> crate::Result { + Ok(PathBuf::from(ANDROID_ASSET_PROTOCOL_URI_PREFIX)) +} + +#[cfg(not(target_os = "android"))] +#[allow(unused_variables)] +fn resource_dir_from>( + exe: P, + package_info: &PackageInfo, + env: &Env, +) -> crate::Result { + let exe_dir = exe.as_ref().parent().expect("failed to get exe directory"); + let curr_dir = exe_dir.display().to_string(); + + let parts: Vec<&str> = curr_dir.split(std::path::MAIN_SEPARATOR).collect(); + let len = parts.len(); + + // Check if running from the Cargo output directory, which means it's an executable in a development machine + // We check if the binary is inside a `target` folder which can be either `target/$profile` or `target/$triple/$profile` + // and see if there's a .cargo-lock file along the executable + // This ensures the check is safer so it doesn't affect apps in production + // Windows also includes the resources in the executable folder so we check that too + if cfg!(target_os = "windows") + || ((len >= 2 && parts[len - 2] == "target") || (len >= 3 && parts[len - 3] == "target")) + && is_cargo_output_directory(exe_dir) + { + return Ok(exe_dir.to_path_buf()); + } + + #[allow(unused_mut, unused_assignments)] + let mut res = Err(crate::Error::UnsupportedPlatform); + + #[cfg(target_os = "linux")] + { + // (canonicalize checks for existence, so there's no need for an extra check) + res = if let Ok(bundle_dir) = exe_dir + .join(format!("../lib/{}", package_info.name)) + .canonicalize() + { + Ok(bundle_dir) + } else if let Some(appdir) = &env.appdir { + let appdir: &std::path::Path = appdir.as_ref(); + Ok(PathBuf::from(format!( + "{}/usr/lib/{}", + appdir.display(), + package_info.name + ))) + } else { + // running bundle + Ok(PathBuf::from(format!("/usr/lib/{}", package_info.name))) + }; + } + + #[cfg(target_os = "macos")] + { + res = exe_dir + .join("../Resources") + .canonicalize() + .map_err(Into::into); + } + + #[cfg(target_os = "ios")] + { + res = exe_dir.join("assets").canonicalize().map_err(Into::into); + } + + res +} + +// Variable holding the type of bundle the executable is stored in. This is modified by binary +// patching during build +#[used] +// Marked as `mut` because it could get optimized away without it, +// see https://github.com/tauri-apps/tauri/pull/13812 +static mut __TAURI_BUNDLE_TYPE: &str = "__TAURI_BUNDLE_TYPE_VAR_UNK"; + +/// Get the type of the bundle current binary is packaged in. +/// If the bundle type is unknown, it returns [`Option::None`]. +pub fn bundle_type() -> Option { + unsafe { + match __TAURI_BUNDLE_TYPE { + "__TAURI_BUNDLE_TYPE_VAR_DEB" => Some(BundleType::Deb), + "__TAURI_BUNDLE_TYPE_VAR_RPM" => Some(BundleType::Rpm), + "__TAURI_BUNDLE_TYPE_VAR_APP" => Some(BundleType::AppImage), + "__TAURI_BUNDLE_TYPE_VAR_MSI" => Some(BundleType::Msi), + "__TAURI_BUNDLE_TYPE_VAR_NSS" => Some(BundleType::Nsis), + _ => { + if cfg!(target_os = "macos") { + Some(BundleType::App) + } else { + None + } + } + } + } +} + +#[cfg(any(feature = "build", feature = "build-2"))] +mod build { + use proc_macro2::TokenStream; + use quote::{ToTokens, TokenStreamExt, quote}; + + use super::*; + + impl ToTokens for Target { + fn to_tokens(&self, tokens: &mut TokenStream) { + let prefix = quote! { ::tauri::utils::platform::Target }; + + tokens.append_all(match self { + Self::MacOS => quote! { #prefix::MacOS }, + Self::Linux => quote! { #prefix::Linux }, + Self::Windows => quote! { #prefix::Windows }, + Self::Android => quote! { #prefix::Android }, + Self::Ios => quote! { #prefix::Ios }, + }); + } + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use crate::{Env, PackageInfo}; + + #[test] + #[cfg(not(target_os = "android"))] + fn resolve_resource_dir() { + let package_info = PackageInfo { + name: "MyApp".into(), + version: "1.0.0".parse().unwrap(), + authors: "", + description: "", + crate_name: "my-app", + }; + let env = Env::default(); + + let path = PathBuf::from("/path/to/target/aarch64-apple-darwin/debug/app"); + let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap(); + assert_eq!(resource_dir, path.parent().unwrap()); + + let path = PathBuf::from("/path/to/target/custom-profile/app"); + let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap(); + assert_eq!(resource_dir, path.parent().unwrap()); + + let path = PathBuf::from("/path/to/target/release/app"); + let resource_dir = super::resource_dir_from(&path, &package_info, &env).unwrap(); + assert_eq!(resource_dir, path.parent().unwrap()); + + let path = PathBuf::from("/path/to/target/unknown-profile/app"); + #[allow(clippy::needless_borrows_for_generic_args)] + let resource_dir = super::resource_dir_from(&path, &package_info, &env); + #[cfg(target_os = "macos")] + assert!(resource_dir.is_err()); + #[cfg(target_os = "linux")] + assert_eq!(resource_dir.unwrap(), PathBuf::from("/usr/lib/MyApp")); + #[cfg(windows)] + assert_eq!(resource_dir.unwrap(), path.parent().unwrap()); + } +} diff --git a/crates/tauri-utils/src/platform/starting_binary.rs b/crates/tauri-utils/src/platform/starting_binary.rs new file mode 100644 index 0000000..000e258 --- /dev/null +++ b/crates/tauri-utils/src/platform/starting_binary.rs @@ -0,0 +1,84 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use ctor::declarative::ctor; +use std::{ + io::{Error, ErrorKind, Result}, + path::{Path, PathBuf}, +}; + +ctor! { + /// A cached version of the current binary using [`ctor`] to cache it before even `main` runs. + #[ctor(unsafe)] + 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); + +impl StartingBinary { + /// Find the starting executable as safely as possible. + fn new() -> Self { + // see notes on current_exe() for security implications + let dangerous_path = match std::env::current_exe() { + Ok(dangerous_path) => dangerous_path, + error @ Err(_) => return Self(error), + }; + + // note: this only checks symlinks on problematic platforms, see implementation below + if let Some(symlink) = Self::has_symlink(&dangerous_path) { + return Self(Err(Error::new( + ErrorKind::InvalidData, + format!( + "StartingBinary found current_exe() that contains a symlink on a non-allowed platform: {}", + symlink.display() + ), + ))); + } + + // we canonicalize the path to resolve any symlinks to the real exe path + Self(dangerous_path.canonicalize()) + } + + /// A clone of the [`PathBuf`] found to be the starting path. + /// + /// Because [`Error`] is not clone-able, it is recreated instead. + pub(super) fn cloned(&self) -> Result { + // false positive + #[allow(clippy::useless_asref)] + self + .0 + .as_ref() + .map(Clone::clone) + .map_err(|e| Error::new(e.kind(), e.to_string())) + } + + /// We only care about checking this on macOS currently, as it has the least symlink protections. + #[cfg(any( + not(target_os = "macos"), + feature = "process-relaunch-dangerous-allow-symlink-macos" + ))] + fn has_symlink(_: &Path) -> Option<&Path> { + None + } + + /// We only care about checking this on macOS currently, as it has the least symlink protections. + #[cfg(all( + target_os = "macos", + not(feature = "process-relaunch-dangerous-allow-symlink-macos") + ))] + fn has_symlink(path: &Path) -> Option<&Path> { + path.ancestors().find(|ancestor| { + matches!( + ancestor + .symlink_metadata() + .as_ref() + .map(std::fs::Metadata::file_type) + .as_ref() + .map(std::fs::FileType::is_symlink), + Ok(true) + ) + }) + } +} diff --git a/crates/tauri-utils/src/plugin.rs b/crates/tauri-utils/src/plugin.rs new file mode 100644 index 0000000..9fe35be --- /dev/null +++ b/crates/tauri-utils/src/plugin.rs @@ -0,0 +1,88 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Compile-time and runtime types for Tauri plugins. +#[cfg(any(feature = "build", feature = "build-2"))] +pub use build::*; + +#[cfg(any(feature = "build", feature = "build-2"))] +mod build { + use std::{ + env::vars_os, + fs, + path::{Path, PathBuf}, + }; + + const GLOBAL_API_SCRIPT_PATH_KEY: &str = "GLOBAL_API_SCRIPT_PATH"; + /// Known file name of the file that contains an array with the path of all API scripts defined with [`define_global_api_script_path`]. + pub const GLOBAL_API_SCRIPT_FILE_LIST_PATH: &str = "__global-api-script.js"; + + /// Defines the path to the global API script using Cargo instructions. + pub fn define_global_api_script_path(path: &Path) { + println!( + "cargo:{GLOBAL_API_SCRIPT_PATH_KEY}={}", + path + .canonicalize() + .expect("failed to canonicalize global API script path") + .display() + ) + } + + /// Collects the path of all the global API scripts defined with [`define_global_api_script_path`] + /// and saves them to the out dir with filename [`GLOBAL_API_SCRIPT_FILE_LIST_PATH`]. + /// + /// `tauri_global_scripts` is only used in Tauri's monorepo for the examples to work + /// since they don't have a build script to run `tauri-build` and pull in the deps env vars + pub fn save_global_api_scripts_paths(out_dir: &Path, mut tauri_global_scripts: Option) { + let mut scripts = Vec::new(); + + for (key, value) in vars_os() { + let key = key.to_string_lossy(); + + if key == format!("DEP_TAURI_{GLOBAL_API_SCRIPT_PATH_KEY}") { + tauri_global_scripts = Some(PathBuf::from(value)); + } else if key.starts_with("DEP_") && key.ends_with(GLOBAL_API_SCRIPT_PATH_KEY) { + let script_path = PathBuf::from(value); + scripts.push(script_path); + } + } + + if let Some(tauri_global_scripts) = tauri_global_scripts { + scripts.insert(0, tauri_global_scripts); + } + + fs::write( + out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH), + serde_json::to_string(&scripts).expect("failed to serialize global API script paths"), + ) + .expect("failed to write global API script"); + } + + /// Read global api scripts from [`GLOBAL_API_SCRIPT_FILE_LIST_PATH`] + pub fn read_global_api_scripts(out_dir: &Path) -> Option> { + let global_scripts_path = out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH); + if !global_scripts_path.exists() { + return None; + } + + let global_scripts_str = fs::read_to_string(global_scripts_path) + .expect("failed to read plugin global API script paths"); + let global_scripts = serde_json::from_str::>(&global_scripts_str) + .expect("failed to parse plugin global API script paths"); + + Some( + global_scripts + .into_iter() + .map(|p| { + fs::read_to_string(&p).unwrap_or_else(|e| { + panic!( + "failed to read plugin global API script {}: {e}", + p.display() + ) + }) + }) + .collect(), + ) + } +} diff --git a/crates/tauri-utils/src/resources.rs b/crates/tauri-utils/src/resources.rs new file mode 100644 index 0000000..692c5cd --- /dev/null +++ b/crates/tauri-utils/src/resources.rs @@ -0,0 +1,724 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::HashMap, + path::{Component, Path, PathBuf}, +}; + +use walkdir::WalkDir; + +use crate::platform::Target as TargetPlatform; + +/// Given a path (absolute or relative) to a resource file, returns the +/// relative path from the bundle resources directory where that resource +/// should be stored. +pub fn resource_relpath(path: &Path) -> PathBuf { + let mut dest = PathBuf::new(); + for component in path.components() { + match component { + Component::Prefix(_) => {} + Component::RootDir => dest.push("_root_"), + Component::CurDir => {} + Component::ParentDir => dest.push("_up_"), + Component::Normal(string) => dest.push(string), + } + } + dest +} + +fn normalize(path: &Path) -> PathBuf { + let mut dest = PathBuf::new(); + for component in path.components() { + match component { + Component::Prefix(_) => {} + Component::RootDir => dest.push("/"), + Component::CurDir => {} + Component::ParentDir => dest.push(".."), + Component::Normal(string) => dest.push(string), + } + } + dest +} + +/// Parses the external binaries to bundle, adding the target triple suffix to each of them. +pub fn external_binaries( + external_binaries: &[String], + target_triple: &str, + target_platform: &TargetPlatform, +) -> Vec { + let mut paths = Vec::new(); + for curr_path in external_binaries { + let extension = if matches!(target_platform, TargetPlatform::Windows) { + ".exe" + } else { + "" + }; + paths.push(format!("{curr_path}-{target_triple}{extension}")); + } + paths +} + +/// Information for a resource. +#[derive(Debug)] +pub struct Resource { + path: PathBuf, + target: PathBuf, +} + +impl Resource { + /// The path of the resource. + pub fn path(&self) -> &Path { + &self.path + } + + /// The target location of the resource. + pub fn target(&self) -> &Path { + &self.target + } +} + +#[derive(Debug)] +enum PatternIter<'a> { + Slice(std::slice::Iter<'a, String>), + Map(std::collections::hash_map::Iter<'a, String, String>), +} + +/// A helper to iterate through resources. +pub struct ResourcePaths<'a> { + iter: ResourcePathsIter<'a>, +} + +impl<'a> ResourcePaths<'a> { + /// Creates a new ResourcePaths from a slice of patterns to iterate + pub fn new(patterns: &'a [String], allow_walk: bool) -> ResourcePaths<'a> { + ResourcePaths { + iter: ResourcePathsIter { + pattern_iter: PatternIter::Slice(patterns.iter()), + allow_walk, + current_dest: None, + current_iter: None, + rerun_if_changed: Vec::new(), + }, + } + } + + /// Creates a new ResourcePaths from a slice of patterns to iterate + pub fn from_map(patterns: &'a HashMap, allow_walk: bool) -> ResourcePaths<'a> { + ResourcePaths { + iter: ResourcePathsIter { + pattern_iter: PatternIter::Map(patterns.iter()), + allow_walk, + current_dest: None, + current_iter: None, + rerun_if_changed: Vec::new(), + }, + } + } + + /// Returns the resource iterator that yields the source and target paths. + /// Needed when using [`Self::from_map`]. + pub fn iter(self) -> ResourcePathsIter<'a> { + self.iter + } +} + +/// Iterator of a [`ResourcePaths`]. +#[derive(Debug)] +pub struct ResourcePathsIter<'a> { + /// the patterns to iterate. + pattern_iter: PatternIter<'a>, + /// whether the resource paths allows directories or not. + allow_walk: bool, + + /// The value of map when [`Self::pattern_iter`] is a [`PatternIter::Map`], + /// used for determining [`Resource::target`] + current_dest: Option, + /// The iter for the current pattern. The cycle goes like this: + /// [`ResourcePaths::next`] -> [`Self::next`] -> [`Self::pattern_iter::next`] -> [`Self::current_iter::next`] + current_iter: Option, + /// Paths that were walked or globbed while iterating. Build scripts + /// should emit a `rerun-if-changed` for each so that adding or removing a + /// file inside a resource directory re-triggers the resource copy. + rerun_if_changed: Vec, +} + +#[derive(Debug)] +enum ResourcePathsInnerIter { + Walk { + iter: walkdir::IntoIter, + /// The key of map when [`ResourcePathsIter::pattern_iter`] is a [`PatternIter::Map`], + /// used for determining [`Resource::target`] + current_pattern: Option, + }, + Glob { + iter: glob::Paths, + }, +} + +impl Iterator for ResourcePathsInnerIter { + type Item = crate::Result; + + fn next(&mut self) -> Option> { + match self { + ResourcePathsInnerIter::Walk { iter, .. } => Some( + iter + .next()? + .map(|entry| entry.into_path()) + .map_err(Into::into), + ), + ResourcePathsInnerIter::Glob { iter } => Some(iter.next()?.map_err(Into::into)), + } + } +} + +impl ResourcePathsIter<'_> { + /// Paths that were walked or globbed while iterating. + /// + /// A build script should emit a `cargo:rerun-if-changed` for each of these + /// after iterating, so that adding or removing a file inside a resource + /// directory re-runs the script and copies the new files. Only meaningful + /// once iteration has produced the entries (i.e. after the iterator is + /// exhausted). + pub fn rerun_if_changed(&self) -> &[PathBuf] { + &self.rerun_if_changed + } + + fn next_current_iter(&mut self) -> Option> { + let current_iter = self.current_iter.as_mut().unwrap(); + let entry = current_iter.next()?; + + Some(match entry { + Ok(entry) => { + // Skip directories + if entry.is_dir() { + self.next_current_iter()? + } else { + self.resource_from_path(normalize(&entry)) + } + } + Err(error) => Err(error), + }) + } + + fn resource_from_path(&self, path: PathBuf) -> crate::Result { + if !path.exists() { + return Err(crate::Error::ResourcePathNotFound(path)); + } + + Ok(Resource { + target: if let Some(dest) = &self.current_dest { + match &self.current_iter { + Some(current_iter) => match current_iter { + // if processing a directory, preserve directory structure under current_dest + ResourcePathsInnerIter::Walk { + current_pattern, .. + } => { + if let Some(pattern) = current_pattern { + dest.join(path.strip_prefix(pattern).unwrap_or(&path)) + } else { + dest.join(&path) + } + } + // if processing a glob and current_dest is not empty + // we put all globbed paths under current_dest + // preserving the file name as it is + ResourcePathsInnerIter::Glob { .. } => dest.join(path.file_name().unwrap()), + }, + None => { + if dest.components().count() == 0 { + // if current_dest is empty while processing a file pattern + // we preserve the file name as it is + // + // e.g. `{ "README.md": "" }` is `README.md` -> `$RESOURCE/README.md` + // + // TODO: This behavior is a confusing special case, + // remove this in v3 or make other cases like this work + // > `{ "README.md": "./folder/" }` is `README.md` -> `$RESOURCE/folder/README.md` (this gives `$RESOURCE/folder` today) + PathBuf::from(path.file_name().unwrap()) + } else { + dest.clone() + } + } + } + } else { + // If [`ResourcePathsIter::pattern_iter`] is a [`PatternIter::Slice`] + resource_relpath(&path) + }, + path, + }) + } + + fn next_pattern(&mut self) -> Option> { + self.current_dest = None; + self.current_iter = None; + + let pattern = match &mut self.pattern_iter { + PatternIter::Slice(iter) => iter.next()?, + PatternIter::Map(iter) => { + let (pattern, dest) = iter.next()?; + self.current_dest = Some(resource_relpath(Path::new(dest))); + pattern + } + }; + + if pattern.contains('*') { + // Watch the fixed directory prefix of the glob (everything before the + // first wildcard component) so new files matching the glob are noticed. + let mut base = PathBuf::new(); + for component in Path::new(pattern).components() { + if component.as_os_str().to_string_lossy().contains('*') { + break; + } + base.push(component); + } + if base.as_os_str().is_empty() { + base.push("."); + } + self.rerun_if_changed.push(base); + + self.current_iter = match glob::glob(pattern) { + Ok(glob) => Some(ResourcePathsInnerIter::Glob { iter: glob }), + Err(error) => return Some(Err(error.into())), + }; + match self.next_current_iter() { + Some(r) => Some(r), + None => { + self.current_iter = None; + Some(Err(crate::Error::GlobPathNotFound(pattern.clone()))) + } + } + } else { + let path = normalize(Path::new(pattern)); + self.rerun_if_changed.push(path.clone()); + if path.is_dir() { + if !self.allow_walk { + return Some(Err(crate::Error::NotAllowedToWalkDir(path))); + } + self.current_iter = Some(ResourcePathsInnerIter::Walk { + iter: WalkDir::new(&path).into_iter(), + current_pattern: if matches!(self.pattern_iter, PatternIter::Map(_)) { + Some(path) + } else { + None + }, + }); + // If the directory is empty, skip and continue to the next pattern + self.next_current_iter().or_else(|| self.next_pattern()) + } else { + Some(self.resource_from_path(path)) + } + } + } +} + +impl Iterator for ResourcePaths<'_> { + type Item = crate::Result; + + fn next(&mut self) -> Option> { + self.iter.next().map(|r| r.map(|res| res.path)) + } +} + +impl Iterator for ResourcePathsIter<'_> { + type Item = crate::Result; + + fn next(&mut self) -> Option> { + if self.current_iter.is_some() { + match self.next_current_iter() { + Some(r) => return Some(r), + None => self.current_iter = None, + } + } + + self.next_pattern() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use std::fs; + use std::path::Path; + + impl PartialEq for Resource { + fn eq(&self, other: &Self) -> bool { + self.path == other.path && self.target == other.target + } + } + + fn expected_resources(resources: &[(&str, &str)]) -> Vec { + resources + .iter() + .map(|(path, target)| Resource { + path: Path::new(path).components().collect(), + target: Path::new(target).components().collect(), + }) + .collect() + } + + fn setup_test_dirs() { + let mut random = [0; 1]; + getrandom::fill(&mut random).unwrap(); + + let temp = std::env::temp_dir(); + let temp = temp.join(format!("tauri_resource_paths_iter_test_{}", random[0])); + + let _ = fs::remove_dir_all(&temp); + fs::create_dir_all(&temp).unwrap(); + + std::env::set_current_dir(&temp).unwrap(); + + let paths = [ + "src-tauri/tauri.conf.json", + "src-tauri/some-other-json.json", + "src-tauri/Cargo.toml", + "src-tauri/Tauri.toml", + "src-tauri/build.rs", + "src-tauri/some-folder/some-file.txt", + "src/assets/javascript.svg", + "src/assets/tauri.svg", + "src/assets/rust.svg", + "src/assets/lang/en.json", + "src/assets/lang/ar.json", + "src/sounds/lang/es.wav", + "src/sounds/lang/fr.wav", + "src/textures/ground/earth.tex", + "src/textures/ground/sand.tex", + "src/textures/water.tex", + "src/textures/fire.tex", + "src/tiles/sky/grey.tile", + "src/tiles/sky/yellow.tile", + "src/tiles/grass.tile", + "src/tiles/stones.tile", + "src/index.html", + "src/style.css", + "src/script.js", + "src/dir/another-dir/file1.txt", + "src/dir/another-dir2/file2.txt", + ]; + + for path in paths { + let path = Path::new(path); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, "").unwrap(); + } + fs::create_dir_all("empty-directory").unwrap(); + } + + fn resources_map(literal: &[(&str, &str)]) -> HashMap { + literal + .iter() + .map(|(from, to)| (from.to_string(), to.to_string())) + .collect() + } + + #[test] + #[serial_test::serial(resources)] + fn resource_paths_iter_slice_allow_walk() { + setup_test_dirs(); + + let dir = std::env::current_dir().unwrap().join("src-tauri"); + let _ = std::env::set_current_dir(dir); + + let resources = ResourcePaths::new( + &[ + // `empty-directory` should not affect anything + "../empty-directory".into(), + "../src/script.js".into(), + "../src/assets".into(), + "../src/index.html".into(), + "../src/sounds".into(), + // Should be the same as `../src/textures/` or `../src/textures` + "../src/textures/**/*".into(), + "*.toml".into(), + "*.conf.json".into(), + ], + true, + ) + .iter() + .flatten() + .collect::>(); + + let expected = expected_resources(&[ + // From `../src/script.js` + ("../src/script.js", "_up_/src/script.js"), + // From `../src/assets` + ( + "../src/assets/javascript.svg", + "_up_/src/assets/javascript.svg", + ), + ("../src/assets/tauri.svg", "_up_/src/assets/tauri.svg"), + ("../src/assets/rust.svg", "_up_/src/assets/rust.svg"), + ("../src/assets/lang/en.json", "_up_/src/assets/lang/en.json"), + ("../src/assets/lang/ar.json", "_up_/src/assets/lang/ar.json"), + // From `../src/index.html` + ("../src/index.html", "_up_/src/index.html"), + // From `../src/sounds` + ("../src/sounds/lang/es.wav", "_up_/src/sounds/lang/es.wav"), + ("../src/sounds/lang/fr.wav", "_up_/src/sounds/lang/fr.wav"), + // From `../src/textures/**/*` + ( + "../src/textures/ground/earth.tex", + "_up_/src/textures/ground/earth.tex", + ), + ( + "../src/textures/ground/sand.tex", + "_up_/src/textures/ground/sand.tex", + ), + ("../src/textures/water.tex", "_up_/src/textures/water.tex"), + ("../src/textures/fire.tex", "_up_/src/textures/fire.tex"), + // From `*.toml` + ("Cargo.toml", "Cargo.toml"), + ("Tauri.toml", "Tauri.toml"), + // From `*.conf.json` + ("tauri.conf.json", "tauri.conf.json"), + ]); + + assert_eq!(resources.len(), expected.len()); + for resource in expected { + if !resources.contains(&resource) { + panic!("{resource:?} was expected but not found in {resources:?}"); + } + } + } + + #[test] + #[serial_test::serial(resources)] + fn resource_paths_iter_rerun_if_changed() { + setup_test_dirs(); + + let dir = std::env::current_dir().unwrap().join("src-tauri"); + let _ = std::env::set_current_dir(dir); + + let patterns = [ + "../src/script.js".into(), + "../src/assets".into(), + "../src/textures/**/*".into(), + "*.toml".into(), + ]; + let mut resources = ResourcePaths::new(&patterns, true).iter(); + + for resource in resources.by_ref() { + resource.unwrap(); + } + + assert_eq!( + resources.rerun_if_changed(), + &[ + normalize(Path::new("../src/script.js")), + normalize(Path::new("../src/assets")), + normalize(Path::new("../src/textures")), + PathBuf::from("."), + ] + ); + } + + #[test] + #[serial_test::serial(resources)] + fn resource_paths_iter_slice_no_walk() { + setup_test_dirs(); + + let dir = std::env::current_dir().unwrap().join("src-tauri"); + let _ = std::env::set_current_dir(dir); + + let resources = ResourcePaths::new( + &[ + "../src/script.js".into(), + "../src/assets".into(), + "../src/index.html".into(), + "../src/sounds".into(), + "*.toml".into(), + "*.conf.json".into(), + ], + false, + ) + .iter() + .flatten() + .collect::>(); + + let expected = expected_resources(&[ + ("../src/script.js", "_up_/src/script.js"), + ("../src/index.html", "_up_/src/index.html"), + ("Cargo.toml", "Cargo.toml"), + ("Tauri.toml", "Tauri.toml"), + ("tauri.conf.json", "tauri.conf.json"), + ]); + + assert_eq!(resources.len(), expected.len()); + for resource in expected { + if !resources.contains(&resource) { + panic!("{resource:?} was expected but not found in {resources:?}"); + } + } + } + + #[test] + #[serial_test::serial(resources)] + fn resource_paths_iter_map_allow_walk() { + setup_test_dirs(); + + let dir = std::env::current_dir().unwrap().join("src-tauri"); + let _ = std::env::set_current_dir(dir); + + let resources = ResourcePaths::from_map( + &resources_map(&[ + ("../src/script.js", "main.js"), + ("../src/assets", ""), + ("../src/index.html", "frontend/index.html"), + ("../src/sounds", "voices"), + ("../src/textures/*", "textures"), + ("../src/tiles/**/*", "tiles"), + ("*.toml", ""), + ("*.conf.json", "json"), + ("./some-folder/", "some-target-folder/"), + ("../non-existent-file", "asd"), // invalid case + ("../non/*", "asd"), // invalid case + ]), + true, + ) + .iter() + .flatten() + .collect::>(); + + let expected = expected_resources(&[ + ("../src/script.js", "main.js"), + ("../src/assets/javascript.svg", "javascript.svg"), + ("../src/assets/tauri.svg", "tauri.svg"), + ("../src/assets/rust.svg", "rust.svg"), + ("../src/assets/lang/en.json", "lang/en.json"), + ("../src/assets/lang/ar.json", "lang/ar.json"), + ("../src/index.html", "frontend/index.html"), + ("../src/sounds/lang/es.wav", "voices/lang/es.wav"), + ("../src/sounds/lang/fr.wav", "voices/lang/fr.wav"), + ("../src/textures/water.tex", "textures/water.tex"), + ("../src/textures/fire.tex", "textures/fire.tex"), + ("../src/tiles/grass.tile", "tiles/grass.tile"), + ("../src/tiles/stones.tile", "tiles/stones.tile"), + ("../src/tiles/sky/grey.tile", "tiles/grey.tile"), + ("../src/tiles/sky/yellow.tile", "tiles/yellow.tile"), + ("Cargo.toml", "Cargo.toml"), + ("Tauri.toml", "Tauri.toml"), + ("tauri.conf.json", "json/tauri.conf.json"), + ( + "some-folder/some-file.txt", + "some-target-folder/some-file.txt", + ), + ]); + + assert_eq!(resources.len(), expected.len()); + for resource in expected { + if !resources.contains(&resource) { + panic!("{resource:?} was expected but not found in {resources:?}"); + } + } + } + + #[test] + #[serial_test::serial(resources)] + fn resource_paths_iter_map_no_walk() { + setup_test_dirs(); + + let dir = std::env::current_dir().unwrap().join("src-tauri"); + let _ = std::env::set_current_dir(dir); + + let resources = ResourcePaths::from_map( + &resources_map(&[ + ("../src/script.js", "main.js"), + ("../src/assets", ""), + ("../src/index.html", "frontend/index.html"), + ("../src/sounds", "voices"), + ("*.toml", ""), + ("*.conf.json", "json"), + ]), + false, + ) + .iter() + .flatten() + .collect::>(); + + let expected = expected_resources(&[ + ("../src/script.js", "main.js"), + ("../src/index.html", "frontend/index.html"), + ("Cargo.toml", "Cargo.toml"), + ("Tauri.toml", "Tauri.toml"), + ("tauri.conf.json", "json/tauri.conf.json"), + ]); + + assert_eq!(resources.len(), expected.len()); + for resource in expected { + if !resources.contains(&resource) { + panic!("{resource:?} was expected but not found in {resources:?}"); + } + } + } + + #[test] + #[serial_test::serial(resources)] + fn resource_paths_errors() { + setup_test_dirs(); + + let dir = std::env::current_dir().unwrap().join("src-tauri"); + let _ = std::env::set_current_dir(dir); + + let resources = ResourcePaths::from_map( + &resources_map(&[ + ("../non-existent-file", "file"), + ("../non-existent-dir", "dir"), + // exists but not allowed to walk + ("../src", "dir2"), + // doesn't exist but it is a glob and will return an error + ("../non-existent-glob-dir/*", "glob"), + // exists but only contains directories and will not produce any values + ("../src/dir/*", "dir3"), + ]), + false, + ) + .iter() + .collect::>(); + + assert_eq!(resources.len(), 5); + + assert!(resources.iter().all(|r| r.is_err())); + + // hashmap order is not guaranteed so we check the error variant exists and how many + assert!( + resources + .iter() + .any(|r| matches!(r, Err(crate::Error::ResourcePathNotFound(_)))) + ); + assert_eq!( + resources + .iter() + .filter(|r| matches!(r, Err(crate::Error::ResourcePathNotFound(_)))) + .count(), + 2 + ); + assert!( + resources + .iter() + .any(|r| matches!(r, Err(crate::Error::NotAllowedToWalkDir(_)))) + ); + assert_eq!( + resources + .iter() + .filter(|r| matches!(r, Err(crate::Error::NotAllowedToWalkDir(_)))) + .count(), + 1 + ); + assert!( + resources + .iter() + .any(|r| matches!(r, Err(crate::Error::GlobPathNotFound(_)))) + ); + assert_eq!( + resources + .iter() + .filter(|r| matches!(r, Err(crate::Error::GlobPathNotFound(_)))) + .count(), + 2 + ); + } +} diff --git a/crates/tauri-utils/src/tokens.rs b/crates/tauri-utils/src/tokens.rs new file mode 100644 index 0000000..6558621 --- /dev/null +++ b/crates/tauri-utils/src/tokens.rs @@ -0,0 +1,175 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Utilities to implement [`ToTokens`] for a type. + +use std::path::Path; + +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; +use serde_json::Value as JsonValue; +use url::Url; + +/// Write a `TokenStream` of the `$struct`'s fields to the `$tokens`. +/// +/// All fields must represent a binding of the same name that implements `ToTokens`. +#[macro_export] +macro_rules! literal_struct { + ($tokens:ident, $struct:path, $($field:ident),+) => { + $tokens.append_all(quote! { + $struct { + $($field: #$field),+ + } + }) + }; +} + +/// Create a `String` constructor `TokenStream`. +/// +/// e.g. `"Hello World"` -> `String::from("Hello World")`. +/// This takes a `&String` to reduce casting all the `&String` -> `&str` manually. +pub fn str_lit(s: impl AsRef) -> TokenStream { + let s = s.as_ref(); + quote! { #s.into() } +} + +/// Create an `Option` constructor `TokenStream`. +pub fn opt_lit(item: Option<&impl ToTokens>) -> TokenStream { + match item { + None => quote! { ::core::option::Option::None }, + Some(item) => quote! { ::core::option::Option::Some(#item) }, + } +} + +/// Create an `Option` constructor `TokenStream` over an owned [`ToTokens`] impl type. +pub fn opt_lit_owned(item: Option) -> TokenStream { + match item { + None => quote! { ::core::option::Option::None }, + Some(item) => quote! { ::core::option::Option::Some(#item) }, + } +} + +/// Helper function to combine an `opt_lit` with `str_lit`. +pub fn opt_str_lit(item: Option>) -> TokenStream { + opt_lit(item.map(str_lit).as_ref()) +} + +/// Helper function to combine an `opt_lit` with a list of `str_lit` +pub fn opt_vec_lit( + item: Option>, + map: impl Fn(Raw) -> Tokens, +) -> TokenStream +where + Tokens: ToTokens, +{ + opt_lit(item.map(|list| vec_lit(list, map)).as_ref()) +} + +/// Create a `Vec` constructor, mapping items with a function that spits out `TokenStream`s. +pub fn vec_lit( + list: impl IntoIterator, + map: impl Fn(Raw) -> Tokens, +) -> TokenStream +where + Tokens: ToTokens, +{ + let items = list.into_iter().map(map); + quote! { vec![#(#items),*] } +} + +/// Create a `PathBuf` constructor `TokenStream`. +/// +/// e.g. `"Hello World" -> String::from("Hello World"). +pub fn path_buf_lit(s: impl AsRef) -> TokenStream { + let s = s.as_ref().to_string_lossy().into_owned(); + quote! { ::std::path::PathBuf::from(#s) } +} + +/// Creates a `Url` constructor `TokenStream`. +pub fn url_lit(url: &Url) -> TokenStream { + let url = url.as_str(); + quote! { #url.parse().unwrap() } +} + +/// Create a map constructor, mapping keys and values with other `TokenStream`s. +/// +/// This function is pretty generic because the types of keys AND values get transformed. +pub fn map_lit( + map_type: TokenStream, + map: Map, + map_key: FuncKey, + map_value: FuncValue, +) -> TokenStream +where + ::IntoIter: ExactSizeIterator, + Map: IntoIterator, + TokenStreamKey: ToTokens, + TokenStreamValue: ToTokens, + FuncKey: Fn(Key) -> TokenStreamKey, + FuncValue: Fn(Value) -> TokenStreamValue, +{ + let ident = quote::format_ident!("map"); + let map = map.into_iter(); + + if map.len() > 0 { + let items = map.map(|(key, value)| { + let key = map_key(key); + let value = map_value(value); + quote! { #ident.insert(#key, #value); } + }); + + quote! {{ + let mut #ident = #map_type::new(); + #(#items)* + #ident + }} + } else { + quote! { #map_type::new() } + } +} + +/// Create a `serde_json::Value` variant `TokenStream` for a number +pub fn json_value_number_lit(num: &serde_json::Number) -> TokenStream { + // See for guarantees + let prefix = quote! { ::serde_json::Value }; + if num.is_u64() { + // guaranteed u64 + let num = num.as_u64().unwrap(); + quote! { #prefix::Number(#num.into()) } + } else if num.is_i64() { + // guaranteed i64 + let num = num.as_i64().unwrap(); + quote! { #prefix::Number(#num.into()) } + } else if num.is_f64() { + // guaranteed f64 + let num = num.as_f64().unwrap(); + quote! { #prefix::Number(::serde_json::Number::from_f64(#num).unwrap(/* safe to unwrap, guaranteed f64 */)) } + } else { + // invalid number + quote! { #prefix::Null } + } +} + +/// Create a `serde_json::Value` constructor `TokenStream` +pub fn json_value_lit(jv: &JsonValue) -> TokenStream { + let prefix = quote! { ::serde_json::Value }; + + match jv { + JsonValue::Null => quote! { #prefix::Null }, + JsonValue::Bool(bool) => quote! { #prefix::Bool(#bool) }, + JsonValue::Number(number) => json_value_number_lit(number), + JsonValue::String(str) => { + let s = str_lit(str); + quote! { #prefix::String(#s) } + } + JsonValue::Array(vec) => { + let items = vec.iter().map(json_value_lit); + quote! { #prefix::Array(vec![#(#items),*]) } + } + JsonValue::Object(map) => { + let map = map_lit(quote! { ::serde_json::Map }, map, str_lit, json_value_lit); + quote! { #prefix::Object(#map) } + } + } +} diff --git a/crates/tauri/.scripts/loop_qc.sh b/crates/tauri/.scripts/loop_qc.sh new file mode 100644 index 0000000..13a7c36 --- /dev/null +++ b/crates/tauri/.scripts/loop_qc.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Copyright 2019-2024 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + + +# Loop all quickcheck tests for tauri. +while true +do + cargo test qc_ + if [[ x$? != x0 ]] ; then + exit $? + fi +done diff --git a/crates/tauri/Cargo.toml b/crates/tauri/Cargo.toml new file mode 100644 index 0000000..f75a7b7 --- /dev/null +++ b/crates/tauri/Cargo.toml @@ -0,0 +1,278 @@ +[package] +name = "tauri" +version = "2.11.5" +description = "Make tiny, secure apps for all desktop platforms with Tauri" +exclude = ["/test", "/.scripts", "CHANGELOG.md", "/target"] +links = "Tauri" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +categories.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[package.metadata.docs.rs] +no-default-features = true +features = [ + "wry", + "unstable", + "custom-protocol", + "tray-icon", + "devtools", + "image-png", + "protocol-asset", + "test", + "specta", + "dynamic-acl", + "isolation", +] +default-target = "x86_64-unknown-linux-gnu" +targets = [ + "x86_64-pc-windows-msvc", + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-linux-android", + "x86_64-apple-ios", +] + +[package.metadata.cargo-udeps.ignore] +normal = ["reqwest"] +build = ["tauri-build"] +development = ["quickcheck_macros"] + +[dependencies] +serde_json = { version = "1", features = ["raw_value"] } +serde = { version = "1", features = ["derive", "rc"] } +tokio = { version = "1", features = [ + "rt", + "rt-multi-thread", + "sync", + "fs", + "io-util", +] } +uuid = { version = "1", features = ["v4"], optional = true } +url = "2" +anyhow = "1" +thiserror = "2" +tauri-runtime = { workspace = true } +tauri-macros = { workspace = true } +tauri-utils = { workspace = true, features = ["resources"] } +tauri-runtime-wry = { workspace = true, optional = true } +getrandom = "0.3" +serde_repr = "0.1" +http = "1" +dirs = "7" +percent-encoding = "2" +raw-window-handle = { version = "0.6", features = ["std"] } +glob = "0.3" +mime = "0.3" +data-url = { version = "0.3", optional = true } +serialize-to-javascript = "0.1.2" +image = { version = "0.25", default-features = false, optional = true } +http-range = { version = "0.1", optional = true } +tracing = { version = "0.1", optional = true } +heck = "0.5" +log = "0.4.21" +dunce = "1" +specta = { version = "^2.0.0-rc.16", optional = true, default-features = false, features = [ + "function", + "derive", +] } +# WARNING: cookie::Cookie is re-exported so bumping this is a breaking change, documented to be done as a minor bump +cookie = "0.18" + +# 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.19", default-features = false, features = [ + "serde", + "gtk", +] } +tray-icon = { version = "0.24", default-features = false, features = [ + "serde", + "gtk", +], optional = true } + +# linux +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] +gtk = { version = "0.18", features = ["v3_24"] } +webkit2gtk = { version = "2", features = ["v2_40"], optional = true } + +# darwin +[target.'cfg(target_vendor = "apple")'.dependencies] +objc2 = "0.6" + +# macOS +[target.'cfg(target_os = "macos")'.dependencies] +embed_plist = "1.2" +plist = "1" +objc2-foundation = { version = "0.3.2", default-features = false, features = [ + "std", + "NSData", + "NSEnumerator", + "NSThread", +] } +objc2-app-kit = { version = "0.3.2", default-features = false, features = [ + "std", + "NSApplication", + "NSColor", + "NSResponder", + "NSView", + "NSWindow", + "NSImage", +] } +objc2-web-kit = { version = "0.3.2", default-features = false, features = [ + "objc2-app-kit", + "WKWebView", + "WKWebViewConfiguration", + "WKUserContentController", +] } +window-vibrancy = "0.8.1" + +# windows +[target."cfg(windows)".dependencies] +webview2-com = { version = "0.39", optional = true } +window-vibrancy = "0.8.1" +windows = { version = "0.62", features = [ + "Win32_Foundation", + "Win32_UI", + "Win32_UI_WindowsAndMessaging", + "Win32_System_LibraryLoader", + "Win32_Graphics_Gdi", +] } + +# mobile +[target.'cfg(any(target_os = "android", all(target_vendor = "apple", not(target_os = "macos"))))'.dependencies] +bytes = { version = "1", features = ["serde"] } +reqwest = { version = "0.13", default-features = false, features = [ + "json", + "stream", +] } +rustls = { version = "0.23", default-features = false, features = [ + "ring", +], optional = true } + +# android +[target.'cfg(target_os = "android")'.dependencies] +jni = "0.21" + +# UIKit, i.e. iOS/tvOS/watchOS/visionOS +[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies] +libc = "0.2" +swift-rs = "1.0.8" +objc2-ui-kit = { version = "0.3.2", default-features = false, features = [ + "UIApplication", + "UIResponder", + "UIView", +] } + +[build-dependencies] +glob = "0.3" +heck = "0.5" +tauri-build = { workspace = true } +tauri-utils = { workspace = true, features = ["build-2"] } + +[dev-dependencies] +proptest = "1.6.0" +quickcheck = "1.0.3" +quickcheck_macros = "1.0.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tauri = { path = ".", default-features = false, features = ["wry"] } +tokio = { version = "1", features = ["full"] } +cargo_toml = "1.0" +http-range = "0.1.5" + +[features] +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", + "tauri-runtime-wry?/common-controls-v6", +] +tray-icon = ["dep:tray-icon"] +tracing = ["dep:tracing", "tauri-macros/tracing", "tauri-runtime-wry?/tracing"] +test = [] +compression = ["tauri-macros/compression", "tauri-utils/compression"] +wry = ["webview2-com", "webkit2gtk", "tauri-runtime-wry"] +# TODO: Remove in v3 - wry does not have this feature anymore +objc-exception = [] +linux-libxdo = ["tray-icon/libxdo", "muda/libxdo"] +isolation = ["tauri-utils/isolation", "tauri-macros/isolation", "uuid"] +custom-protocol = ["tauri-macros/custom-protocol"] +# TODO: Remove these flags in v3 and/or enable them by default behind a mobile flag https://github.com/tauri-apps/tauri/issues/12384 +native-tls = ["reqwest/native-tls"] +native-tls-vendored = ["reqwest/native-tls-vendored"] +rustls-tls = ["reqwest/rustls-no-provider", "dep:rustls"] +devtools = ["tauri-runtime/devtools", "tauri-runtime-wry?/devtools"] +process-relaunch-dangerous-allow-symlink-macos = [ + "tauri-utils/process-relaunch-dangerous-allow-symlink-macos", +] +macos-private-api = [ + "tauri-runtime/macos-private-api", + "tauri-runtime-wry?/macos-private-api", +] +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"] +image-ico = ["image/ico"] +image-png = ["image/png"] +macos-proxy = ["tauri-runtime-wry?/macos-proxy"] +dynamic-acl = [] +specta = ["dep:specta"] + +[[example]] +name = "commands" +path = "../../examples/commands/main.rs" + +[[example]] +name = "helloworld" +path = "../../examples/helloworld/main.rs" + +[[example]] +name = "drag" +path = "../../examples/drag/main.rs" + +[[example]] +name = "multiwebview" +path = "../../examples/multiwebview/main.rs" +required-features = ["unstable"] + +[[example]] +name = "multiwindow" +path = "../../examples/multiwindow/main.rs" + +[[example]] +name = "run-return" +path = "../../examples/run-return/main.rs" + +[[example]] +name = "splashscreen" +path = "../../examples/splashscreen/main.rs" + +[[example]] +name = "state" +path = "../../examples/state/main.rs" + +[[example]] +name = "streaming" +path = "../../examples/streaming/main.rs" + +[[example]] +name = "isolation" +path = "../../examples/isolation/main.rs" +required-features = ["isolation"] + +[lints] +workspace = true diff --git a/crates/tauri/LICENSE-APACHE-2.0 b/crates/tauri/LICENSE-APACHE-2.0 new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/crates/tauri/LICENSE-APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/tauri/LICENSE-MIT b/crates/tauri/LICENSE-MIT new file mode 100644 index 0000000..b08530d --- /dev/null +++ b/crates/tauri/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/tauri/README.md b/crates/tauri/README.md new file mode 100644 index 0000000..4496530 --- /dev/null +++ b/crates/tauri/README.md @@ -0,0 +1,42 @@ +# Tauri + + + +[![status](https://img.shields.io/badge/status-stable-blue.svg)](https://github.com/tauri-apps/tauri/tree/dev) +[![License](https://img.shields.io/badge/License-MIT%20or%20Apache%202-green.svg)](https://opencollective.com/tauri) +[![test core](https://img.shields.io/github/actions/workflow/status/tauri-apps/tauri/test-core.yml?label=test%20core&logo=github)](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri?ref=badge_shield) +[![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/SpmNs4S) +[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app) +[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation) +[![support](https://img.shields.io/badge/sponsor-Open%20Collective-blue.svg)](https://opencollective.com/tauri) + +| Component | Version | +| --------- | ---------------------------------------------------------------------------------------------- | +| tauri | [![](https://img.shields.io/crates/v/tauri?style=flat-square)](https://crates.io/crates/tauri) | + +## About Tauri + +Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily. + +Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task. + +## This module + +This is the glue crate that holds everything together. It brings the runtimes, macros, utilities and API into one final product. It reads the `tauri.conf.json` file at compile time in order to bring in features and undertake actual configuration of the app (and even the `Cargo.toml` file in the project's folder). It handles script injection (for polyfills / prototype revision) at runtime, hosts the API for systems interaction, and even manages updating. + +To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document. + +## Semver + +**tauri** is following [Semantic Versioning 2.0](https://semver.org/). + +## Licenses + +Code: (c) 2019 - 2021 - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. + +Logo: CC-BY-NC-ND + +- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum) diff --git a/crates/tauri/build.rs b/crates/tauri/build.rs new file mode 100644 index 0000000..1cbd446 --- /dev/null +++ b/crates/tauri/build.rs @@ -0,0 +1,500 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use heck::AsShoutySnakeCase; +use tauri_utils::write_if_changed; + +use std::{ + collections::BTreeMap, + env, fs, + path::{Path, PathBuf}, +}; + +const PLUGINS: &[(&str, &[(&str, bool)])] = &[ + // (plugin_name, &[(command, enabled-by_default)]) + // TODO: Enable this in v3 + // ("core:channel", &[("fetch", true)]), + ( + "core:path", + &[ + ("resolve_directory", true), + ("resolve", true), + ("normalize", true), + ("join", true), + ("dirname", true), + ("extname", true), + ("basename", true), + ("is_absolute", true), + ], + ), + ( + "core:event", + &[ + ("listen", true), + ("unlisten", true), + ("emit", true), + ("emit_to", true), + ], + ), + ( + "core:window", + &[ + ("create", false), + // getters + ("get_all_windows", true), + ("scale_factor", true), + ("inner_position", true), + ("outer_position", true), + ("inner_size", true), + ("outer_size", true), + ("is_fullscreen", true), + ("is_minimized", true), + ("is_maximized", true), + ("is_focused", true), + ("is_decorated", true), + ("is_resizable", true), + ("is_maximizable", true), + ("is_minimizable", true), + ("is_closable", true), + ("is_visible", true), + ("is_enabled", true), + ("title", true), + ("current_monitor", true), + ("primary_monitor", true), + ("monitor_from_point", true), + ("available_monitors", true), + ("cursor_position", true), + ("theme", true), + ("is_always_on_top", true), + ("activity_name", true), + ("scene_identifier", true), + // setters + ("center", false), + ("request_user_attention", false), + ("set_enabled", false), + ("set_resizable", false), + ("set_maximizable", false), + ("set_minimizable", false), + ("set_closable", false), + ("set_title", false), + ("maximize", false), + ("unmaximize", false), + ("minimize", false), + ("unminimize", false), + ("show", false), + ("hide", false), + ("close", false), + ("destroy", false), + ("set_decorations", false), + ("set_shadow", false), + ("set_effects", false), + ("set_always_on_top", false), + ("set_always_on_bottom", false), + ("set_visible_on_all_workspaces", false), + ("set_content_protected", false), + ("set_size", false), + ("set_min_size", false), + ("set_size_constraints", false), + ("set_max_size", false), + ("set_position", false), + ("set_fullscreen", false), + ("set_fullscreen_on_monitor", false), + ("set_simple_fullscreen", false), + ("set_focus", false), + ("set_focusable", false), + ("set_skip_taskbar", false), + ("set_cursor_grab", false), + ("set_cursor_visible", false), + ("set_cursor_icon", false), + ("set_cursor_position", false), + ("set_ignore_cursor_events", false), + ("start_dragging", false), + ("start_resize_dragging", false), + ("set_progress_bar", false), + ("set_badge_count", false), + ("set_overlay_icon", false), + ("set_badge_label", false), + ("set_icon", false), + ("set_title_bar_style", false), + ("set_theme", false), + ("toggle_maximize", false), + ("set_background_color", false), + // internal + ("internal_toggle_maximize", true), + ], + ), + ( + "core:webview", + &[ + ("create_webview", false), + ("create_webview_window", false), + // getters + ("get_all_webviews", true), + ("webview_position", true), + ("webview_size", true), + // setters + ("webview_close", false), + ("set_webview_size", false), + ("set_webview_position", false), + ("set_webview_focus", false), + ("set_webview_auto_resize", false), + ("set_webview_zoom", false), + ("webview_hide", false), + ("webview_show", false), + ("print", false), + ("reparent", false), + ("clear_all_browsing_data", false), + ("set_webview_background_color", false), + // internal + ("internal_toggle_devtools", true), + ], + ), + ( + "core:app", + &[ + ("version", true), + ("name", true), + ("tauri_version", true), + ("identifier", true), + ("app_show", false), + ("app_hide", false), + ("exit", false), + ("fetch_data_store_identifiers", false), + ("remove_data_store", false), + ("default_window_icon", false), + ("set_app_theme", false), + ("set_dock_visibility", false), + ("bundle_type", true), + ("register_listener", true), + ("remove_listener", true), + ("supports_multiple_windows", true), + ], + ), + ( + "core:image", + &[ + ("new", true), + ("from_bytes", true), + ("from_path", true), + ("rgba", true), + ("size", true), + ], + ), + ("core:resources", &[("close", true)]), + ( + "core:menu", + &[ + ("new", true), + ("append", true), + ("prepend", true), + ("insert", true), + ("remove", true), + ("remove_at", true), + ("items", true), + ("get", true), + ("popup", true), + ("create_default", true), + ("set_as_app_menu", true), + ("set_as_window_menu", true), + ("text", true), + ("set_text", true), + ("is_enabled", true), + ("set_enabled", true), + ("set_accelerator", true), + ("set_as_windows_menu_for_nsapp", true), + ("set_as_help_menu_for_nsapp", true), + ("is_checked", true), + ("set_checked", true), + ("set_icon", true), + ], + ), + ( + "core:tray", + &[ + ("new", true), + ("get_by_id", true), + ("remove_by_id", true), + ("set_icon", true), + ("set_menu", true), + ("set_tooltip", true), + ("set_title", true), + ("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), + ], + ), +]; + +// checks if the given Cargo feature is enabled. +fn has_feature(checked_features: &mut Vec<&'static str>, feature: &'static str) -> bool { + checked_features.push(feature); + // when a feature is enabled, Cargo sets the `CARGO_FEATURE_` env var to 1 + // + std::env::var_os(format!("CARGO_FEATURE_{}", AsShoutySnakeCase(feature))) + .map(|x| x == "1") + .unwrap_or(false) +} + +// creates a cfg alias if `has_feature` is true. +// `alias` must be a snake case string. +fn alias(alias: &str, has_feature: bool) { + println!("cargo:rustc-check-cfg=cfg({alias})"); + if has_feature { + println!("cargo:rustc-cfg={alias}"); + } +} + +fn main() { + let mut checked_features = Vec::new(); + let custom_protocol = has_feature(&mut checked_features, "custom-protocol"); + let dev = !custom_protocol; + alias("custom_protocol", custom_protocol); + alias("dev", dev); + + println!("cargo:dev={dev}"); + + let target_os = std::env::var_os("CARGO_CFG_TARGET_OS").unwrap(); + let mobile = target_os == "ios" || target_os == "android"; + alias("desktop", !mobile); + alias("mobile", mobile); + + let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + + let checked_features_out_path = out_dir.join("checked_features"); + // file is used by a unit test in src/lib.rs to check that the features checked do exist in the Cargo.toml + std::fs::write(checked_features_out_path, checked_features.join(",")) + .expect("failed to write checked_features file"); + + // workaround needed to prevent `STATUS_ENTRYPOINT_NOT_FOUND` error in tests + // see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864 + let target_env = std::env::var_os("CARGO_CFG_TARGET_ENV"); + let is_tauri_workspace = std::env::var_os("__TAURI_WORKSPACE__").is_some_and(|v| v == "true"); + if is_tauri_workspace && target_os == "windows" && target_env.is_some_and(|trgt| trgt == "msvc") { + embed_manifest_for_tests(); + } + + if target_os == "android" { + fn env_var(var: &str) -> String { + std::env::var(var).unwrap_or_else(|_| { + panic!("`{var}` is not set, which is needed to generate the kotlin files for android.") + }) + } + + if let Some(kotlin_out_dir) = std::env::var_os("WRY_ANDROID_KOTLIN_FILES_OUT_DIR") { + let package = env_var("WRY_ANDROID_PACKAGE"); + let library = env_var("WRY_ANDROID_LIBRARY"); + + let kotlin_out_dir = PathBuf::from(&kotlin_out_dir) + .canonicalize() + .unwrap_or_else(move |_| { + panic!( + "Failed to canonicalize `WRY_ANDROID_KOTLIN_FILES_OUT_DIR` path {kotlin_out_dir:?}" + ) + }); + + let kotlin_files_path = + PathBuf::from(env_var("CARGO_MANIFEST_DIR")).join("mobile/android-codegen"); + println!("cargo:rerun-if-changed={}", kotlin_files_path.display()); + let kotlin_files = + fs::read_dir(kotlin_files_path).expect("failed to read Android codegen directory"); + + for file in kotlin_files { + let file = file.unwrap(); + + let content = fs::read_to_string(file.path()) + .expect("failed to read kotlin file as string") + .replace("{{package}}", &package) + .replace("{{library}}", &library); + + let out_path = kotlin_out_dir.join(file.file_name()); + // Overwrite only if changed to not trigger rebuilds + write_if_changed(&out_path, &content).expect("Failed to write kotlin file"); + + println!("cargo:rerun-if-changed={}", out_path.display()); + } + } + + if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { + let package_unescaped = env::var("TAURI_ANDROID_PACKAGE_UNESCAPED") + .unwrap_or_else(|_| env_var("WRY_ANDROID_PACKAGE").replace('`', "")); + let tauri_proguard = + include_str!("./mobile/proguard-tauri.pro").replace("$PACKAGE", &package_unescaped); + std::fs::write( + project_dir.join("app").join("proguard-tauri.pro"), + tauri_proguard, + ) + .expect("failed to write proguard-tauri.pro"); + } + + let lib_path = + PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("mobile/android"); + println!("cargo:android_library_path={}", lib_path.display()); + } + + #[cfg(target_os = "macos")] + { + if target_os == "ios" { + let lib_path = + PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("mobile/ios-api"); + tauri_utils::build::link_apple_library("Tauri", &lib_path); + println!("cargo:ios_library_path={}", lib_path.display()); + } + } + + let tauri_global_scripts = PathBuf::from("./scripts/bundle.global.js") + .canonicalize() + .expect("failed to canonicalize tauri global API script path"); + tauri_utils::plugin::define_global_api_script_path(&tauri_global_scripts); + // This should usually be done in `tauri-build`, + // but we need to do this here for the examples in this workspace to work as they don't have build scripts + if is_tauri_workspace { + tauri_utils::plugin::save_global_api_scripts_paths(&out_dir, Some(tauri_global_scripts)); + } + + let permissions = define_permissions(&out_dir); + tauri_utils::acl::build::generate_allowed_commands(&out_dir, None, permissions).unwrap(); +} + +const LICENSE_HEADER: &str = r"# Copyright 2019-2024 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT +"; + +fn define_permissions( + out_dir: &Path, +) -> BTreeMap> { + let mut all_permissions = BTreeMap::new(); + for (plugin, commands) in PLUGINS { + let plugin_directory_name = plugin.strip_prefix("core:").unwrap_or(plugin); + let permissions_out_dir = out_dir.join("permissions").join(plugin_directory_name); + let autogenerated = + permissions_out_dir.join(tauri_utils::acl::build::AUTOGENERATED_FOLDER_NAME); + let commands_dir = autogenerated.join("commands"); + + tauri_utils::acl::build::autogenerate_command_permissions( + &commands_dir, + &commands.iter().map(|(cmd, _)| *cmd).collect::>(), + LICENSE_HEADER, + false, + ); + let default_permissions: Vec<_> = commands.iter().filter(|(_cmd, default)| *default).collect(); + let all_commands_enabled_by_default = commands.len() == default_permissions.len(); + let default_permissions = default_permissions + .into_iter() + .map(|(cmd, _)| { + let slugified_command = cmd.replace('_', "-"); + format!("\"allow-{slugified_command}\"") + }) + .collect::>() + .join(", "); + + let all_enable_by_default = if all_commands_enabled_by_default { + ", which enables all commands" + } else { + "" + }; + + let default_toml = format!( + r#"{LICENSE_HEADER}# Automatically generated - DO NOT EDIT! + +[default] +description = "Default permissions for the plugin{all_enable_by_default}." +permissions = [{default_permissions}] +"#, + ); + + let out_path = autogenerated.join("default.toml"); + write_if_changed(out_path, default_toml) + .unwrap_or_else(|_| panic!("unable to autogenerate default permissions")); + + let permissions = tauri_utils::acl::build::define_permissions( + &PathBuf::from(glob::Pattern::escape( + &permissions_out_dir.to_string_lossy(), + )) + .join("**") + .join("*.toml") + .to_string_lossy(), + &format!("tauri:{plugin}"), + out_dir, + |_| true, + ) + .unwrap_or_else(|e| panic!("failed to define permissions for {plugin}: {e}")); + + let docs_out_dir = Path::new("permissions") + .join(plugin_directory_name) + .join("autogenerated"); + fs::create_dir_all(&docs_out_dir).expect("failed to create plugin documentation directory"); + tauri_utils::acl::build::generate_docs( + &permissions, + &docs_out_dir, + plugin.strip_prefix("tauri-plugin-").unwrap_or(plugin), + ) + .expect("failed to generate plugin documentation page"); + all_permissions.insert(plugin.to_string(), permissions); + } + + let default_permissions = define_default_permission_set(out_dir); + all_permissions.insert("core".to_string(), default_permissions); + + all_permissions +} + +fn define_default_permission_set( + out_dir: &Path, +) -> Vec { + let permissions_out_dir = out_dir.join("permissions"); + fs::create_dir_all(&permissions_out_dir) + .expect("failed to create core:default permissions directory"); + + let default_toml = permissions_out_dir.join("default.toml"); + let toml_content = format!( + r#"{LICENSE_HEADER} + +[default] +description = "Default core plugins set." +permissions = [{}] +"#, + PLUGINS + .iter() + .map(|(k, _)| format!("\"{k}:default\"")) + .collect::>() + .join(",") + ); + + write_if_changed(default_toml, toml_content) + .unwrap_or_else(|_| panic!("unable to autogenerate core:default set")); + + tauri_utils::acl::build::define_permissions( + &PathBuf::from(glob::Pattern::escape( + &permissions_out_dir.to_string_lossy(), + )) + .join("*.toml") + .to_string_lossy(), + "tauri:core", + out_dir, + |_| true, + ) + .unwrap_or_else(|e| panic!("failed to define permissions for `core:default` : {e}")) +} + +fn embed_manifest_for_tests() { + static WINDOWS_MANIFEST_FILE: &str = "windows-app-manifest.xml"; + + let manifest = std::env::current_dir() + .unwrap() + .join("../tauri-build/src") + .join(WINDOWS_MANIFEST_FILE); + + println!("cargo:rerun-if-changed={}", manifest.display()); + // Embed the Windows application manifest file. + println!("cargo:rustc-link-arg=/MANIFEST:EMBED"); + println!( + "cargo:rustc-link-arg=/MANIFESTINPUT:{}", + manifest.to_str().unwrap() + ); + // Turn linker warnings into errors. + println!("cargo:rustc-link-arg=/WX"); +} diff --git a/crates/tauri/mobile/android-codegen/TauriActivity.kt b/crates/tauri/mobile/android-codegen/TauriActivity.kt new file mode 100644 index 0000000..0cfbe3f --- /dev/null +++ b/crates/tauri/mobile/android-codegen/TauriActivity.kt @@ -0,0 +1,60 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +package {{package}} + +import android.content.Intent +import android.content.res.Configuration +import android.os.Bundle +import app.tauri.plugin.PluginManager + +abstract class TauriActivity : WryActivity() { + override val handleBackNavigation: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + PluginManager.onCreate(this) + } + + fun getPluginManager(): PluginManager { + return PluginManager + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + PluginManager.onNewIntent(intent) + } + + override fun onRestart() { + super.onRestart() + PluginManager.onRestart(this) + } + + override fun onResume() { + super.onResume() + PluginManager.onResume(this) + } + + override fun onPause() { + super.onPause() + PluginManager.onPause(this) + } + + override fun onStop() { + super.onStop() + PluginManager.onStop(this) + } + + override fun onDestroy() { + super.onDestroy() + PluginManager.onDestroy(this) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + PluginManager.onConfigurationChanged(newConfig) + } +} diff --git a/crates/tauri/mobile/android/.gitignore b/crates/tauri/mobile/android/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/crates/tauri/mobile/android/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/crates/tauri/mobile/android/build.gradle.kts b/crates/tauri/mobile/android/build.gradle.kts new file mode 100644 index 0000000..124c36a --- /dev/null +++ b/crates/tauri/mobile/android/build.gradle.kts @@ -0,0 +1,48 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.tauri" + compileSdk = 36 + + defaultConfig { + minSdk = 21 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + buildFeatures { + buildConfig = true + } +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.7.0") + implementation("androidx.appcompat:appcompat:1.6.0") + implementation("com.google.android.material:material:1.7.0") + implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") + // (de)serializes org.json values (JSObject, JSArray) as JSON objects and arrays + // instead of as beans; org.json itself is part of the Android platform + implementation("com.fasterxml.jackson.datatype:jackson-datatype-json-org:2.15.3") { + exclude(group = "org.json", module = "json") + } + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} diff --git a/crates/tauri/mobile/android/consumer-rules.pro b/crates/tauri/mobile/android/consumer-rules.pro new file mode 100644 index 0000000..e21b5be --- /dev/null +++ b/crates/tauri/mobile/android/consumer-rules.pro @@ -0,0 +1,41 @@ +-keep class app.tauri.** { + @app.tauri.JniMethod public ; + native ; +} + +-keep class app.tauri.plugin.JSArray { + public (...); +} + +-keepclassmembers class org.json.JSONArray { + public put(...); +} + +-keep class app.tauri.plugin.JSObject { + public (...); + public put(...); +} + +-keep @app.tauri.annotation.TauriPlugin public class * { + @app.tauri.annotation.Command public ; + @app.tauri.annotation.PermissionCallback ; + @app.tauri.annotation.ActivityCallback ; + @app.tauri.annotation.Permission ; + public (...); +} + +-keep @app.tauri.annotation.InvokeArg public class * { + *; +} + +-keep @com.fasterxml.jackson.databind.annotation.JsonDeserialize public class * { + *; +} + +-keep @com.fasterxml.jackson.databind.annotation.JsonSerialize public class * { + *; +} + +-keep class * extends com.fasterxml.jackson.databind.JsonDeserializer { *; } + +-keep class * extends com.fasterxml.jackson.databind.JsonSerializer { *; } diff --git a/crates/tauri/mobile/android/src/androidTest/java/app/tauri/ExampleInstrumentedTest.kt b/crates/tauri/mobile/android/src/androidTest/java/app/tauri/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..77ce904 --- /dev/null +++ b/crates/tauri/mobile/android/src/androidTest/java/app/tauri/ExampleInstrumentedTest.kt @@ -0,0 +1,28 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("app.tauri.test", appContext.packageName) + } +} diff --git a/crates/tauri/mobile/android/src/main/AndroidManifest.xml b/crates/tauri/mobile/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9a40236 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt new file mode 100644 index 0000000..7bbd09a --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt @@ -0,0 +1,56 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import android.app.Activity +import android.webkit.WebView +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import app.tauri.annotation.Command +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Plugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.JSObject + +@TauriPlugin +class AppPlugin(private val activity: Activity): Plugin(activity) { + private val BACK_BUTTON_EVENT = "back-button" + + private var webView: WebView? = null + + override fun load(webView: WebView) { + this.webView = webView + } + + init { + val callback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (!hasListener(BACK_BUTTON_EVENT)) { + if (this@AppPlugin.webView?.canGoBack() == true) { + this@AppPlugin.webView!!.goBack() + } else { + this.isEnabled = false + this@AppPlugin.activity.onBackPressed() + this.isEnabled = true + } + } else { + val data = JSObject().apply { + put("canGoBack", this@AppPlugin.webView?.canGoBack() ?: false) + } + trigger(BACK_BUTTON_EVENT, data) + } + } + } + (activity as AppCompatActivity).onBackPressedDispatcher.addCallback(activity, callback) + } + + // Called by the Rust side when the app exits so the activity (and any other activity + // of the task, i.e. other windows) is closed gracefully instead of the process being killed + @Command + fun exit(invoke: Invoke) { + invoke.resolve() + activity.finishAffinity() + } +} diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/FsUtils.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/FsUtils.kt new file mode 100644 index 0000000..235775c --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/FsUtils.kt @@ -0,0 +1,208 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import android.content.ContentUris +import android.content.Context +import android.content.res.AssetManager +import android.database.Cursor +import android.net.Uri +import android.os.Environment +import android.provider.DocumentsContract +import android.provider.MediaStore +import android.provider.OpenableColumns +import java.io.File +import java.io.FileOutputStream +import kotlin.math.min + +internal class FsUtils { + companion object { + fun readAsset(assetManager: AssetManager, fileName: String): String { + assetManager.open(fileName).bufferedReader().use { + return it.readText() + } + } + + fun getFileUrlForUri(context: Context, uri: Uri): String? { + // DocumentProvider + if (DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + val docId: String = DocumentsContract.getDocumentId(uri) + val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val type = split[0] + if ("primary".equals(type, ignoreCase = true)) { + return legacyPrimaryPath(split[1]) + } else { + val splitIndex = docId.indexOf(':', 1) + val tag = docId.substring(0, splitIndex) + val path = docId.substring(splitIndex + 1) + val nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag) + if (nonPrimaryVolume != null) { + val result = "$nonPrimaryVolume/$path" + val file = File(result) + return if (file.exists() && file.canRead()) { + result + } else null + } + } + } else if (isDownloadsDocument(uri)) { + val id: String = DocumentsContract.getDocumentId(uri) + val contentUri: Uri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), + java.lang.Long.valueOf(id) + ) + return getDataColumn(context, contentUri, null, null) + } else if (isMediaDocument(uri)) { + val docId: String = DocumentsContract.getDocumentId(uri) + val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val type = split[0] + var contentUri: Uri? = null + when (type) { + "image" -> { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + "video" -> { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + } + "audio" -> { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + } + val selection = "_id=?" + val selectionArgs = arrayOf(split[1]) + if (contentUri != null) { + return getDataColumn(context, contentUri, selection, selectionArgs) + } + } + } else if ("content".equals(uri.scheme, ignoreCase = true)) { + // Return the remote address + return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn( + context, + uri, + null, + null + ) + } else if ("file".equals(uri.scheme, ignoreCase = true)) { + return uri.path + } + return null + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + private fun getDataColumn( + context: Context, + uri: Uri, + selection: String?, + selectionArgs: Array? + ): String? { + var path: String? = null + var cursor: Cursor? = null + val column = "_data" + val projection = arrayOf(column) + try { + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) + if (cursor != null && cursor.moveToFirst()) { + val index = cursor.getColumnIndexOrThrow(column) + path = cursor.getString(index) + } + } catch (ex: IllegalArgumentException) { + return getCopyFilePath(uri, context) + } finally { + cursor?.close() + } + return path ?: getCopyFilePath(uri, context) + } + + private fun getCopyFilePath(uri: Uri, context: Context): String? { + val cursor = context.contentResolver.query(uri, null, null, null, null)!! + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + val name = cursor.getString(nameIndex) + val file = File(context.filesDir, name) + try { + val inputStream = context.contentResolver.openInputStream(uri) + val outputStream = FileOutputStream(file) + var read: Int + val maxBufferSize = 1024 * 1024 + val bufferSize = min(inputStream!!.available(), maxBufferSize) + val buffers = ByteArray(bufferSize) + while (inputStream.read(buffers).also { read = it } != -1) { + outputStream.write(buffers, 0, read) + } + inputStream.close() + outputStream.close() + } catch (e: Exception) { + return null + } finally { + cursor.close() + } + return file.path + } + + private fun legacyPrimaryPath(pathPart: String): String { + return Environment.getExternalStorageDirectory().toString() + "/" + pathPart + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private fun isExternalStorageDocument(uri: Uri): Boolean { + return "com.android.externalstorage.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private fun isDownloadsDocument(uri: Uri): Boolean { + return "com.android.providers.downloads.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private fun isMediaDocument(uri: Uri): Boolean { + return "com.android.providers.media.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + private fun isGooglePhotosUri(uri: Uri): Boolean { + return "com.google.android.apps.photos.content" == uri.authority + } + + private fun getPathToNonPrimaryVolume(context: Context, tag: String): String? { + val volumes = context.externalCacheDirs + if (volumes != null) { + for (volume in volumes) { + if (volume != null) { + val path = volume.absolutePath + val index = path.indexOf(tag) + if (index != -1) { + return path.substring(0, index) + tag + } + } + } + } + return null + } + } +} diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/JniMethod.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/JniMethod.kt new file mode 100644 index 0000000..41301d4 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/JniMethod.kt @@ -0,0 +1,8 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +@Retention(AnnotationRetention.RUNTIME) +internal annotation class JniMethod diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/Logger.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/Logger.kt new file mode 100644 index 0000000..c047378 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/Logger.kt @@ -0,0 +1,85 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/Logger.java + +import android.text.TextUtils; +import android.util.Log; + +class Logger { + companion object { + private const val LOG_TAG_CORE = "Tauri" + + fun tags(vararg subtags: String): String { + return if (subtags.isNotEmpty()) { + LOG_TAG_CORE + "/" + TextUtils.join("/", subtags) + } else LOG_TAG_CORE + } + + fun verbose(message: String) { + verbose(LOG_TAG_CORE, message) + } + + fun verbose(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.v(tag, message) + } + + fun debug(message: String) { + debug(LOG_TAG_CORE, message) + } + + fun debug(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.d(tag, message) + } + + fun info(message: String) { + info(LOG_TAG_CORE, message) + } + + fun info(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.i(tag, message) + } + + fun warn(message: String) { + warn(LOG_TAG_CORE, message) + } + + fun warn(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.w(tag, message) + } + + fun error(message: String) { + error(LOG_TAG_CORE, message, null) + } + + fun error(message: String, e: Throwable?) { + error(LOG_TAG_CORE, message, e) + } + + fun error(tag: String, message: String, e: Throwable?) { + if (!shouldLog()) { + return + } + Log.e(tag, message, e) + } + + private fun shouldLog(): Boolean { + return BuildConfig.DEBUG + } + } +} diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/PathPlugin.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/PathPlugin.kt new file mode 100644 index 0000000..7b2f38d --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/PathPlugin.kt @@ -0,0 +1,132 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import android.app.Activity +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.OpenableColumns +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Plugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.JSObject + +const val TAURI_ASSETS_DIRECTORY_URI = "asset://localhost/" + +@InvokeArg +class GetFileNameFromUriArgs { + lateinit var uri: String +} + +@TauriPlugin +class PathPlugin(private val activity: Activity): Plugin(activity) { + private fun resolvePath(invoke: Invoke, path: String?) { + val obj = JSObject() + obj.put("path", path) + invoke.resolve(obj) + } + + @Command + fun getFileNameFromUri(invoke: Invoke) { + val args = invoke.parseArgs(GetFileNameFromUriArgs::class.java) + val name = getRealNameFromURI(activity, Uri.parse(args.uri)) + val res = JSObject() + res.put("name", name) + invoke.resolve(res) + } + + @Command + fun getAudioDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_MUSIC)?.absolutePath) + } + + @Command + fun getExternalCacheDir(invoke: Invoke) { + resolvePath(invoke, activity.externalCacheDir?.absolutePath) + } + + @Command + fun getConfigDir(invoke: Invoke) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + resolvePath(invoke, activity.dataDir.absolutePath) + } else { + resolvePath(invoke, activity.applicationInfo.dataDir) + } + } + + @Command + fun getDataDir(invoke: Invoke) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + resolvePath(invoke, activity.dataDir.absolutePath) + } else { + resolvePath(invoke, activity.applicationInfo.dataDir) + } + } + + @Command + fun getDocumentDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)?.absolutePath) + } + + @Command + fun getDownloadDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath) + } + + @Command + fun getPictureDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)?.absolutePath) + } + + @Command + fun getPublicDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DCIM)?.absolutePath) + } + + @Command + fun getVideoDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_MOVIES)?.absolutePath) + } + + @Command + fun getResourcesDir(invoke: Invoke) { + resolvePath(invoke, TAURI_ASSETS_DIRECTORY_URI) + } + + @Command + fun getCacheDir(invoke: Invoke) { + resolvePath(invoke, activity.cacheDir.absolutePath) + } + + @Command + fun getHomeDir(invoke: Invoke) { + resolvePath(invoke, Environment.getExternalStorageDirectory().absolutePath) + } +} + +fun getRealNameFromURI(activity: Activity, contentUri: Uri): String? { + var cursor: Cursor? = null + try { + val projection = arrayOf(OpenableColumns.DISPLAY_NAME) + cursor = activity.contentResolver.query(contentUri, projection, null, null, null) + + cursor?.let { + val columnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (it.moveToFirst()) { + return it.getString(columnIndex) + } + } + } catch (e: Exception) { + Logger.error("failed to get real name from URI $e") + } finally { + cursor?.close() + } + + return null // Return null if no file name could be resolved +} diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/PermissionHelper.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/PermissionHelper.kt new file mode 100644 index 0000000..d9a5cd4 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/PermissionHelper.kt @@ -0,0 +1,107 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/PermissionHelper.java + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import androidx.core.app.ActivityCompat; +import java.util.ArrayList; + +object PermissionHelper { + /** + * Checks if a list of given permissions are all granted by the user + * + * @param permissions Permissions to check. + * @return True if all permissions are granted, false if at least one is not. + */ + fun hasPermissions(context: Context?, permissions: Array): Boolean { + for (perm in permissions) { + if (ActivityCompat.checkSelfPermission( + context!!, + perm + ) != PackageManager.PERMISSION_GRANTED + ) { + return false + } + } + return true + } + + /** + * Check whether the given permission has been defined in the AndroidManifest.xml + * + * @param permission A permission to check. + * @return True if the permission has been defined in the Manifest, false if not. + */ + fun hasDefinedPermission(context: Context, permission: String): Boolean { + val requestedPermissions = getManifestPermissions(context) + if (!requestedPermissions.isNullOrEmpty()) { + val requestedPermissionsList = listOf(*requestedPermissions) + val requestedPermissionsArrayList = ArrayList(requestedPermissionsList) + if (requestedPermissionsArrayList.contains(permission)) { + return true + } + } + return false + } + + /** + * Check whether all of the given permissions have been defined in the AndroidManifest.xml + * @param context the app context + * @param permissions a list of permissions + * @return true only if all permissions are defined in the AndroidManifest.xml + */ + fun hasDefinedPermissions(context: Context, permissions: Array): Boolean { + return permissions.all { hasDefinedPermission(context, it) } + } + + /** + * Get the permissions defined in AndroidManifest.xml + * + * @return The permissions defined in AndroidManifest.xml + */ + private fun getManifestPermissions(context: Context): Array? { + var requestedPermissions: Array? = null + try { + val pm = context.packageManager + val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())) + } else { + @Suppress("DEPRECATION") + pm.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS) + } + if (packageInfo != null) { + requestedPermissions = packageInfo.requestedPermissions + } + } catch (_: Exception) { + } + return requestedPermissions + } + + /** + * Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml + * + * @param neededPermissions The permissions needed. + * @return The permissions not present in AndroidManifest.xml + */ + fun getUndefinedPermissions(context: Context, neededPermissions: Array): Array { + val requestedPermissions = getManifestPermissions(context) + if (!requestedPermissions.isNullOrEmpty()) { + val undefinedPermissions = ArrayList() + val requestedPermissionsList = listOf(*requestedPermissions) + val requestedPermissionsArrayList = ArrayList(requestedPermissionsList) + for (permission in neededPermissions) { + if (!requestedPermissionsArrayList.contains(permission)) { + undefinedPermissions.add(permission) + } + } + return undefinedPermissions.toTypedArray() + } + return neededPermissions + } +} diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/PermissionState.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/PermissionState.kt new file mode 100644 index 0000000..33092ed --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/PermissionState.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import java.util.* + +enum class PermissionState(private val state: String) { + GRANTED("granted"), DENIED("denied"), PROMPT("prompt"), PROMPT_WITH_RATIONALE("prompt-with-rationale"); + + override fun toString(): String { + return state + } + + companion object { + fun byState(state: String): PermissionState { + return valueOf(state.uppercase(Locale.ROOT).replace('-', '_')) + } + } +} diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/ActivityCallback.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/ActivityCallback.kt new file mode 100644 index 0000000..d680939 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/ActivityCallback.kt @@ -0,0 +1,9 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class ActivityCallback diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/InvokeArg.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/InvokeArg.kt new file mode 100644 index 0000000..08a2f75 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/InvokeArg.kt @@ -0,0 +1,9 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class InvokeArg diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/Permission.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/Permission.kt new file mode 100644 index 0000000..bd5ab5d --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/Permission.kt @@ -0,0 +1,19 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +@Retention(AnnotationRetention.RUNTIME) +annotation class Permission( + /** + * An array of Android permission strings. + * Eg: {Manifest.permission.ACCESS_COARSE_LOCATION} + * or {"android.permission.ACCESS_COARSE_LOCATION"} + */ + val strings: Array = [], + /** + * An optional name to use instead of the Android permission string. + */ + val alias: String = "" +) diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/PermissionCallback.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/PermissionCallback.kt new file mode 100644 index 0000000..5fdeddf --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/PermissionCallback.kt @@ -0,0 +1,9 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class PermissionCallback diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/PluginMethod.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/PluginMethod.kt new file mode 100644 index 0000000..7a73045 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/PluginMethod.kt @@ -0,0 +1,8 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +@Retention(AnnotationRetention.RUNTIME) +annotation class Command diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/TauriPlugin.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/TauriPlugin.kt new file mode 100644 index 0000000..4e8bc78 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/annotation/TauriPlugin.kt @@ -0,0 +1,19 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +import app.tauri.annotation.Permission + +/** + * Base annotation for all Plugins + */ +@Retention(AnnotationRetention.RUNTIME) +annotation class TauriPlugin( + /** + * Permissions this plugin needs, in order to make permission requests + * easy if the plugin only needs basic permission prompting + */ + val permissions: Array = [] +) diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Channel.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Channel.kt new file mode 100644 index 0000000..581b074 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Channel.kt @@ -0,0 +1,33 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.ObjectMapper + +const val CHANNEL_PREFIX = "__CHANNEL__:" + +internal class ChannelDeserializer(val sendChannelData: (channelId: Long, data: String) -> Unit, private val objectMapper: ObjectMapper): JsonDeserializer() { + override fun deserialize( + jsonParser: JsonParser?, + deserializationContext: DeserializationContext + ): Channel { + val channelDef = deserializationContext.readValue(jsonParser, String::class.java) + val callback = channelDef.substring(CHANNEL_PREFIX.length).toLongOrNull() ?: throw Error("unexpected channel value $channelDef") + return Channel(callback, { res -> sendChannelData(callback, res) }, objectMapper) + } +} + +class Channel(val id: Long, private val handler: (data: String) -> Unit, private val objectMapper: ObjectMapper) { + fun send(data: JSObject) { + handler(PluginResult(data).toString()) + } + + fun sendObject(data: Any) { + handler(objectMapper.writeValueAsString(data)) + } +} diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/InvalidPluginMethodException.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/InvalidPluginMethodException.kt new file mode 100644 index 0000000..4325313 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/InvalidPluginMethodException.kt @@ -0,0 +1,11 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +internal class InvalidCommandException : Exception { + constructor(s: String?) : super(s) {} + constructor(t: Throwable?) : super(t) {} + constructor(s: String?, t: Throwable?) : super(s, t) {} +} diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt new file mode 100644 index 0000000..1f9d080 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt @@ -0,0 +1,93 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import app.tauri.Logger +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper + +class Invoke( + val id: Long, + val command: String, + val callback: Long, + val error: Long, + private val sendResponse: (callback: Long, data: String) -> Unit, + private val argsJson: String, + private val jsonMapper: ObjectMapper +) { + fun getRawArgs(): String { + return argsJson + } + + fun getArgs(): JSObject { + return JSObject(argsJson) + } + + fun parseArgs(cls: Class): T { + return jsonMapper.readValue(argsJson, cls) + } + + fun parseArgs(ref: TypeReference): T { + return jsonMapper.readValue(argsJson, ref) + } + + fun resolve(data: JSObject?) { + sendResponse(callback, PluginResult(data).toString()) + } + + fun resolveObject(data: Any) { + sendResponse(callback, jsonMapper.writeValueAsString(data)) + } + + fun resolve() { + sendResponse(callback, "null") + } + + fun reject(msg: String?, code: String?, ex: Exception?, data: JSObject?) { + val errorResult = PluginResult() + + if (ex != null) { + Logger.error(Logger.tags("Plugin"), msg!!, ex) + } + + errorResult.put("message", msg) + if (code != null) { + errorResult.put("code", code) + } + if (data != null) { + errorResult.put("data", data) + } + + sendResponse(error, errorResult.toString()) + } + + fun reject(msg: String?, ex: Exception?, data: JSObject?) { + reject(msg, null, ex, data) + } + + fun reject(msg: String?, code: String?, data: JSObject?) { + reject(msg, code, null, data) + } + + fun reject(msg: String?, code: String?, ex: Exception?) { + reject(msg, code, ex, null) + } + + fun reject(msg: String?, data: JSObject?) { + reject(msg, null, null, data) + } + + fun reject(msg: String?, ex: Exception?) { + reject(msg, null, ex, null) + } + + fun reject(msg: String?, code: String?) { + reject(msg, code, null, null) + } + + fun reject(msg: String?) { + reject(msg, null, null, null) + } +} diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/JSArray.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/JSArray.kt new file mode 100644 index 0000000..84bdf40 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/JSArray.kt @@ -0,0 +1,45 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import org.json.JSONArray +import org.json.JSONException + +class JSArray : JSONArray { + constructor() : super() {} + constructor(json: String?) : super(json) {} + constructor(copyFrom: Collection<*>?) : super(copyFrom) {} + constructor(array: Any?) : super(array) {} + + @Suppress("UNCHECKED_CAST", "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + @Throws(JSONException::class) + fun toList(): List { + val items: MutableList = ArrayList() + var o: Any? = null + for (i in 0 until this.length()) { + this.get(i).also { o = it } + try { + items.add(this.get(i) as E) + } catch (ex: Exception) { + throw JSONException("Not all items are instances of the given type") + } + } + return items + } + + companion object { + /** + * Create a new JSArray without throwing a error + */ + fun from(array: Any?): JSArray? { + try { + return JSArray(array) + } catch (ex: JSONException) { + // + } + return null + } + } +} diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/JSObject.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/JSObject.kt new file mode 100644 index 0000000..2d40561 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/JSObject.kt @@ -0,0 +1,152 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import org.json.JSONException +import org.json.JSONObject + +class JSObject : JSONObject { + constructor() : super() + constructor(json: String) : super(json) + constructor(obj: JSONObject, names: Array) : super(obj, names) + + override fun getString(key: String): String { + return getString(key, "")!! + } + + fun getString(key: String, defaultValue: String?): String? { + try { + if (!super.isNull(key)) { + return super.getString(key) + } + } catch (_: JSONException) { + } + return defaultValue + } + + fun getInteger(key: String): Int? { + return getIntegerInternal(key, null) + } + + fun getInteger(key: String, defaultValue: Int): Int { + return getIntegerInternal(key, defaultValue)!! + } + + private fun getIntegerInternal(key: String, defaultValue: Int?): Int? { + try { + return super.getInt(key) + } catch (_: JSONException) { + } + return defaultValue + } + + override fun getBoolean(key: String): Boolean { + return getBooleanInternal(key, false)!! + } + + fun getBoolean(key: String, defaultValue: Boolean?): Boolean { + return getBooleanInternal(key, defaultValue)!! + } + + private fun getBooleanInternal(key: String, defaultValue: Boolean?): Boolean? { + try { + return super.getBoolean(key) + } catch (_: JSONException) { + } + return defaultValue + } + + fun getJSObject(name: String): JSObject? { + try { + return getJSObjectInternal(name, null) + } catch (_: JSONException) { + } + return null + } + + fun getJSObject(name: String, defaultValue: JSObject): JSObject { + return getJSObjectInternal(name, defaultValue)!! + } + + private fun getJSObjectInternal(name: String, defaultValue: JSObject?): JSObject? { + try { + val obj = get(name) + if (obj is JSONObject) { + val keysIter = obj.keys() + val keys: MutableList = ArrayList() + while (keysIter.hasNext()) { + keys.add(keysIter.next()) + } + return JSObject(obj, keys.toTypedArray()) + } + } catch (_: JSONException) { + } + return defaultValue + } + + override fun put(key: String, value: Boolean): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + override fun put(key: String, value: Int): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + override fun put(key: String, value: Long): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + override fun put(key: String, value: Double): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + override fun put(key: String, value: Any?): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + fun put(key: String, value: String?): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + companion object { + /** + * Convert a pathetic JSONObject into a JSObject + * @param obj + */ + @Throws(JSONException::class) + fun fromJSONObject(obj: JSONObject): JSObject { + val keysIter = obj.keys() + val keys: MutableList = ArrayList() + while (keysIter.hasNext()) { + keys.add(keysIter.next()) + } + return JSObject(obj, keys.toTypedArray()) + } + } +} 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 new file mode 100644 index 0000000..589db2f --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt @@ -0,0 +1,536 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import android.app.Activity +import android.content.res.Configuration +import android.content.Intent +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 +import app.tauri.PermissionHelper +import app.tauri.PermissionState +import app.tauri.annotation.ActivityCallback +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.PermissionCallback +import app.tauri.annotation.TauriPlugin +import com.fasterxml.jackson.databind.ObjectMapper +import java.util.* +import java.util.concurrent.CopyOnWriteArrayList + +@InvokeArg +internal class RegisterListenerArgs { + lateinit var event: String + lateinit var handler: Channel +} + +@InvokeArg +internal class RemoveListenerArgs { + lateinit var event: String + var channelId: Long = 0 +} + +@InvokeArg internal class RequestPermissionsArgs { + var permissions: List? = null +} + +abstract class Plugin(private val activity: Activity) { + var handle: PluginHandle? = null + private val listeners: MutableMap> = mutableMapOf() + + open fun load(webView: WebView) {} + + fun jsonMapper(): ObjectMapper { + return handle!!.jsonMapper + } + + fun getConfig(cls: Class): T { + return jsonMapper().readValue(handle!!.config, cls) + } + + /** + * Handle a new intent being received by the application + */ + open fun onNewIntent(intent: Intent) {} + + + /** + * This event is called just before another activity comes into the foreground. + */ + open fun onPause(activity: AppCompatActivity) {} + + /** + * This event is called just before another activity comes into the foreground. + */ + @Deprecated("use onPause(activity: AppCompatActivity) instead") + open fun onPause() {} + + /** + * This event is called when the user returns to the activity. It is also called on cold starts. + */ + open fun onResume(activity: AppCompatActivity) {} + + /** + * This event is called when the user returns to the activity. It is also called on cold starts. + */ + @Deprecated("use onResume(activity: AppCompatActivity) instead") + 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() {} + + /** + * This event is called when the app is no longer visible to the user. + * You will next receive either onRestart(), onDestroy(), or nothing, depending on later user activity. + */ + open fun onStop(activity: AppCompatActivity) {} + + /** + * This event is called when the app is no longer visible to the user. + * You will next receive either onRestart(), onDestroy(), or nothing, depending on later user activity. + */ + @Deprecated("use onStop(activity: AppCompatActivity) instead") + open fun onStop() {} + + /** + * 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() + } + + internal fun triggerOnPause(activity: AppCompatActivity) { + onPause(activity) + onPause() + } + + internal fun triggerOnResume(activity: AppCompatActivity) { + onResume(activity) + onResume() + } + + internal fun triggerOnStop(activity: AppCompatActivity) { + onStop(activity) + onStop() + } + + /** + * This event is called when a configuration change occurs but the app does not recreate the activity. + */ + open fun onConfigurationChanged(newConfig: Configuration) {} + + /** + * Start activity for result with the provided Intent and resolve calling the provided callback method name. + * + * If there is no registered activity callback for the method name passed in, the call will + * be rejected. Make sure a valid activity result callback method is registered using the + * [ActivityCallback] annotation. + * + * @param invoke the invoke object + * @param intent the intent used to start an activity + * @param callbackName the name of the callback to run when the launched activity is finished + */ + fun startActivityForResult(invoke: Invoke, intent: Intent, callbackName: String) { + handle!!.startActivityForResult(invoke, intent, callbackName) + } + + /** + * Like startActivityForResult() but taking an IntentSender to describe the activity to be started. + * + * If there is no registered activity callback for the method name passed in, the call will + * be rejected. Make sure a valid activity result callback method is registered using the + * [ActivityCallback] annotation. + * + * @param invoke the invoke object + * @param intentSender the intent used to start an activity + * @param callbackName the name of the callback to run when the launched activity is finished + */ + fun startIntentSenderForResult(invoke: Invoke, intentSender: IntentSenderRequest, callbackName: String) { + handle!!.startIntentSenderForResult(invoke, intentSender, callbackName) + } + + /** + * Get the plugin log tags. + * @param subTags + */ + protected fun getLogTag(vararg subTags: String): String { + return Logger.tags(*subTags) + } + + /** + * Gets a log tag with the plugin's class name as subTag. + */ + protected fun getLogTag(): String { + return Logger.tags(this.javaClass.simpleName) + } + + /** + * Convert an URI to an URL that can be loaded by the webview. + */ + fun assetUrl(u: Uri): String { + var path = FsUtils.getFileUrlForUri(activity, u) + if (path?.startsWith("file://") == true) { + path = path.replace("file://", "") + } + return "asset://localhost$path" + } + + fun trigger(event: String, payload: JSObject) { + val eventListeners = listeners[event] + if (!eventListeners.isNullOrEmpty()) { + val listeners = CopyOnWriteArrayList(eventListeners) + for (channel in listeners) { + channel.send(payload) + } + } + } + + fun triggerObject(event: String, payload: Any) { + val eventListeners = listeners[event] + if (!eventListeners.isNullOrEmpty()) { + val listeners = CopyOnWriteArrayList(eventListeners) + for (channel in listeners) { + channel.sendObject(payload) + } + } + } + + fun hasListener(event: String): Boolean { + return !listeners[event].isNullOrEmpty() + } + + @Command + open fun registerListener(invoke: Invoke) { + val args = invoke.parseArgs(RegisterListenerArgs::class.java) + + val eventListeners = listeners[args.event] + if (eventListeners.isNullOrEmpty()) { + listeners[args.event] = mutableListOf(args.handler) + } else { + eventListeners.add(args.handler) + } + + invoke.resolve() + } + + @Command + open fun removeListener(invoke: Invoke) { + val args = invoke.parseArgs(RemoveListenerArgs::class.java) + + val eventListeners = listeners[args.event] + if (!eventListeners.isNullOrEmpty()) { + val c = eventListeners.find { c -> c.id == args.channelId } + if (c != null) { + eventListeners.remove(c) + } + } + + invoke.resolve() + } + + /** + * Exported plugin method for checking the granted status for each permission + * declared on the plugin. This plugin call responds with a mapping of permissions to + * the associated granted status. + */ + @Command + @PermissionCallback + open fun checkPermissions(invoke: Invoke) { + val permissionsResult: Map = getPermissionStates() + if (permissionsResult.isEmpty()) { + // if no permissions are defined on the plugin, resolve undefined + invoke.resolve() + } else { + val permissionsResultJSON = JSObject() + for ((key, value) in permissionsResult) { + permissionsResultJSON.put(key, value) + } + invoke.resolve(permissionsResultJSON) + } + } + + /** + * Exported plugin method to request all permissions for this plugin. + * To manually request permissions within a plugin use: + * [.requestAllPermissions], or + * [.requestPermissionForAlias], or + * [.requestPermissionForAliases] + * + * @param invoke + */ + @Command + open fun requestPermissions(invoke: Invoke) { + val annotation = handle?.annotation + if (annotation != null) { + // handle permission requests for plugins defined with @TauriPlugin + var permAliases: Array? = null + val autoGrantPerms: MutableSet = HashSet() + + val args = invoke.parseArgs(RequestPermissionsArgs::class.java) + + args.permissions?.let { + val aliasSet: MutableSet = HashSet() + + for (perm in annotation.permissions) { + if (it.contains(perm.alias)) { + aliasSet.add(perm.alias) + } + } + if (aliasSet.isEmpty()) { + invoke.reject("No valid permission alias was requested of this plugin.") + return + } else { + permAliases = aliasSet.toTypedArray() + } + } ?: run { + val aliasSet: MutableSet = HashSet() + + for (perm in annotation.permissions) { + // If a permission is defined with no permission strings, separate it for auto-granting. + // Otherwise, the alias is added to the list to be requested. + if (perm.strings.isEmpty() || perm.strings.size == 1 && perm.strings[0] + .isEmpty() + ) { + if (perm.alias.isNotEmpty()) { + autoGrantPerms.add(perm.alias) + } + } else { + aliasSet.add(perm.alias) + } + } + permAliases = aliasSet.toTypedArray() + } + + permAliases?.let { + // request permissions using provided aliases or all defined on the plugin + requestPermissionForAliases(it, invoke, "checkPermissions") + } ?: run { + if (autoGrantPerms.isNotEmpty()) { + // if the plugin only has auto-grant permissions, return all as GRANTED + val permissionsResults = JSObject() + for (perm in autoGrantPerms) { + permissionsResults.put(perm, PermissionState.GRANTED.toString()) + } + invoke.resolve(permissionsResults) + } else { + // no permissions are defined on the plugin, resolve undefined + invoke.resolve() + } + } + } + } + + /** + * Checks if the given permission alias is correctly declared in AndroidManifest.xml + * @param alias a permission alias defined on the plugin + * @return true only if all permissions associated with the given alias are declared in the manifest + */ + fun isPermissionDeclared(alias: String): Boolean { + for (perm in handle?.annotation?.permissions.orEmpty()) { + if (alias.equals(perm.alias, ignoreCase = true)) { + var result = true + for (permString in perm.strings) { + result = result && PermissionHelper.hasDefinedPermission(activity, permString) + } + return result + } + } + Logger.error( + String.format( + "isPermissionDeclared: No alias defined for %s " + "or missing @TauriPlugin annotation.", + alias + ) + ) + return false + } + + private fun permissionActivityResult( + invoke: Invoke, + permissionStrings: Array, + callbackName: String + ) { + handle!!.requestPermissions(invoke, permissionStrings, callbackName) + } + + /** + * Request all of the specified permissions in the TauriPlugin annotation (if any) + * + * If there is no registered permission callback for the Invoke passed in, the call will + * be rejected. Make sure a valid permission callback method is registered using the + * [PermissionCallback] annotation. + * + * @param invoke + * @param callbackName the name of the callback to run when the permission request is complete + */ + protected fun requestAllPermissions( + invoke: Invoke, + callbackName: String + ) { + val annotation = handle!!.annotation + if (annotation != null) { + val perms: HashSet = HashSet() + for (perm in annotation.permissions) { + perms.addAll(perm.strings) + } + permissionActivityResult(invoke, perms.toArray(arrayOfNulls(0)), callbackName) + } + } + + /** + * Request permissions using an alias defined on the plugin. + * + * If there is no registered permission callback for the Invoke passed in, the call will + * be rejected. Make sure a valid permission callback method is registered using the + * [PermissionCallback] annotation. + * + * @param alias an alias defined on the plugin + * @param invoke the invoke involved in originating the request + * @param callbackName the name of the callback to run when the permission request is complete + */ + protected fun requestPermissionForAlias( + alias: String, + invoke: Invoke, + callbackName: String + ) { + requestPermissionForAliases(arrayOf(alias), invoke, callbackName) + } + + /** + * Request permissions using aliases defined on the plugin. + * + * If there is no registered permission callback for the Invoke passed in, the call will + * be rejected. Make sure a valid permission callback method is registered using the + * [PermissionCallback] annotation. + * + * @param aliases a set of aliases defined on the plugin + * @param invoke the invoke involved in originating the request + * @param callbackName the name of the callback to run when the permission request is complete + */ + fun requestPermissionForAliases( + aliases: Array, + invoke: Invoke, + callbackName: String + ) { + if (aliases.isEmpty()) { + Logger.error("No permission alias was provided") + return + } + val permissions = getPermissionStringsForAliases(aliases) + if (permissions.isNotEmpty()) { + permissionActivityResult(invoke, permissions, callbackName) + } + } + + /** + * Gets the Android permission strings defined on the [TauriPlugin] annotation with + * the provided aliases. + * + * @param aliases aliases for permissions defined on the plugin + * @return Android permission strings associated with the provided aliases, if exists + */ + private fun getPermissionStringsForAliases(aliases: Array): Array { + val perms: HashSet = HashSet() + for (perm in handle?.annotation?.permissions.orEmpty()) { + if (aliases.contains(perm.alias)) { + perms.addAll(perm.strings) + } + } + return perms.toArray(arrayOfNulls(0)) + } + + /** + * Get the permission state for the provided permission alias. + * + * @param alias the permission alias to get + * @return the state of the provided permission alias or null + */ + fun getPermissionState(alias: String): PermissionState? { + return getPermissionStates()[alias] + } + + /** + * Helper to check all permissions defined on a plugin and see the state of each. + * + * @return A mapping of permission aliases to the associated granted status. + */ + open fun getPermissionStates(): Map { + val permissionsResults: MutableMap = HashMap() + for (perm in handle?.annotation?.permissions.orEmpty()) { + // If a permission is defined with no permission constants, return GRANTED for it. + // Otherwise, get its true state. + if (perm.strings.isEmpty() || perm.strings.size == 1 && perm.strings[0].isEmpty()) { + val key = perm.alias + if (key.isNotEmpty()) { + val existingResult = permissionsResults[key] + + // auto set permission state to GRANTED if the alias is empty. + if (existingResult == null) { + permissionsResults[key] = PermissionState.GRANTED + } + } + } else { + for (permString in perm.strings) { + val key = perm.alias.ifEmpty { permString } + var permissionStatus: PermissionState + if (ActivityCompat.checkSelfPermission( + activity, + permString + ) == PackageManager.PERMISSION_GRANTED + ) { + permissionStatus = PermissionState.GRANTED + } else { + permissionStatus = PermissionState.PROMPT + + // Check if there is a cached permission state for the "Never ask again" state + val prefs = + activity.getSharedPreferences("PluginPermStates", Activity.MODE_PRIVATE) + val state = prefs.getString(permString, null) + if (state != null) { + permissionStatus = PermissionState.byState(state) + } + } + val existingResult = permissionsResults[key] + + // multiple permissions with the same alias must all be true, otherwise all false. + if (existingResult == null || existingResult === PermissionState.GRANTED) { + permissionsResults[key] = permissionStatus + } + } + } + } + + return permissionsResults + } + +} diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginHandle.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginHandle.kt new file mode 100644 index 0000000..594a96f --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginHandle.kt @@ -0,0 +1,178 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import android.app.Activity +import android.content.Intent +import android.webkit.WebView +import androidx.activity.result.IntentSenderRequest +import androidx.core.app.ActivityCompat +import app.tauri.PermissionHelper +import app.tauri.PermissionState +import app.tauri.annotation.ActivityCallback +import app.tauri.annotation.Command +import app.tauri.annotation.PermissionCallback +import app.tauri.annotation.TauriPlugin +import com.fasterxml.jackson.databind.ObjectMapper +import java.lang.reflect.Method +import androidx.core.content.edit + +class PluginHandle( + private val manager: PluginManager, + val name: String, + val instance: Plugin, + val config: String, + val jsonMapper: ObjectMapper +) { + private val commands: HashMap = HashMap() + private val permissionCallbackMethods: HashMap = HashMap() + private val startActivityCallbackMethods: HashMap = HashMap() + var annotation: TauriPlugin? + var loaded = false + + init { + indexMethods() + instance.handle = this + annotation = instance.javaClass.getAnnotation(TauriPlugin::class.java) + } + + fun load(webView: WebView) { + instance.load(webView) + loaded = true + } + + fun startActivityForResult(invoke: Invoke, intent: Intent, callbackName: String) { + manager.startActivityForResult(intent) { result -> + val method = startActivityCallbackMethods[callbackName] + if (method != null) { + method.isAccessible = true + method(instance, invoke, result) + } + } + } + + fun startIntentSenderForResult( + invoke: Invoke, + intentSender: IntentSenderRequest, + callbackName: String + ) { + manager.startIntentSenderForResult(intentSender) { result -> + val method = startActivityCallbackMethods[callbackName] + if (method != null) { + method.isAccessible = true + method(instance, invoke, result) + } + } + } + + fun requestPermissions( + invoke: Invoke, + permissions: Array, + callbackName: String + ) { + manager.requestPermissions(permissions) { result -> + if (validatePermissions(invoke, result)) { + val method = permissionCallbackMethods[callbackName] + if (method != null) { + method.isAccessible = true + method(instance, invoke) + } + } + } + } + + /** + * Saves permission states and rejects if permissions were not correctly defined in + * the AndroidManifest.xml file. + * + * @param permissions + * @return true if permissions were saved and defined correctly, false if not + */ + private fun validatePermissions( + invoke: Invoke, + permissions: Map + ): Boolean { + val activity = manager.activity!! + val prefs = + activity.getSharedPreferences("PluginPermStates", Activity.MODE_PRIVATE) + for ((permString, isGranted) in permissions) { + if (isGranted) { + // Permission granted. If previously denied, remove cached state + val state = prefs.getString(permString, null) + if (state != null) { + prefs.edit { + remove(permString) + } + } + } else { + prefs.edit { + if (ActivityCompat.shouldShowRequestPermissionRationale( + activity, + permString + ) + ) { + // Permission denied, can prompt again with rationale + putString(permString, PermissionState.PROMPT_WITH_RATIONALE.toString()) + } else { + // Permission denied permanently, store this state for future reference + putString(permString, PermissionState.DENIED.toString()) + } + } + } + } + val permStrings = permissions.keys.toTypedArray() + if (!PermissionHelper.hasDefinedPermissions(activity, permStrings)) { + val builder = StringBuilder() + builder.append("Missing the following permissions in AndroidManifest.xml:\n") + val missing = PermissionHelper.getUndefinedPermissions(activity, permStrings) + for (perm in missing) { + builder.append( + """ + $perm + + """.trimIndent() + ) + } + invoke.reject(builder.toString()) + return false + } + return true + } + + @Throws( + InvalidCommandException::class, + IllegalAccessException::class + ) + fun invoke(invoke: Invoke) { + val methodMeta = commands[invoke.command] + ?: throw InvalidCommandException("No command " + invoke.command + " found for plugin " + instance.javaClass.name) + methodMeta.method.invoke(instance, invoke) + } + + private fun indexMethods() { + val methods = mutableListOf() + var pluginCursor: Class<*> = instance.javaClass + while (pluginCursor.name != Any::class.java.name) { + methods.addAll(listOf(*pluginCursor.declaredMethods)) + pluginCursor = pluginCursor.superclass + } + + for (method in methods) { + if (method.isAnnotationPresent(Command::class.java)) { + val command = method.getAnnotation(Command::class.java) ?: continue + val methodMeta = CommandData(method, command) + commands[method.name] = methodMeta + } + + if (method.isAnnotationPresent(ActivityCallback::class.java)) { + startActivityCallbackMethods[method.name] = method + } + + if (method.isAnnotationPresent(PermissionCallback::class.java)) { + permissionCallbackMethods[method.name] = method + } + } + } +} 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 new file mode 100644 index 0000000..367fd8e --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt @@ -0,0 +1,245 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import android.content.res.Configuration +import android.content.Context +import android.content.Intent +import android.webkit.WebView +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import app.tauri.annotation.InvokeArg +import app.tauri.FsUtils +import app.tauri.JniMethod +import app.tauri.Logger +import com.fasterxml.jackson.annotation.JsonAutoDetect +import com.fasterxml.jackson.annotation.PropertyAccessor +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsonorg.JsonOrgModule +import com.fasterxml.jackson.databind.module.SimpleModule +import java.lang.reflect.InvocationTargetException + +object PluginManager { + fun interface RequestPermissionsCallback { + fun onResult(permissions: Map) + } + + fun interface ActivityResultCallback { + fun onResult(result: ActivityResult) + } + + /** The result launchers belonging to one activity. */ + private class ResultLaunchers( + val startActivityForResult: ActivityResultLauncher, + val startIntentSenderForResult: ActivityResultLauncher, + val requestPermissions: ActivityResultLauncher> + ) + + // Insertion ordered, so the activity taken over when the current one goes away is the oldest + // surviving one rather than an arbitrary member of a hash set. + private val launchers: LinkedHashMap = LinkedHashMap() + var activity: AppCompatActivity? = null + private val plugins: HashMap = HashMap() + private var requestPermissionsCallback: RequestPermissionsCallback? = null + private var startActivityForResultCallback: ActivityResultCallback? = null + private var startIntentSenderForResultCallback: ActivityResultCallback? = null + 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)) + // Without it, the mapper's field visibility serializes an org.json value through + // its private fields: a `JSArray` as `{"values":[...]}` and a `JSObject` as + // `{"nameValuePairs":{...}}`. + .registerModule(JsonOrgModule()) + } + + fun onCreate(activity: AppCompatActivity) { + // Every activity gets its own launchers, and gets them here: registerForActivityResult must + // be called before its owner reaches STARTED, so an activity that is already running can + // never be given launchers later. + launchers[activity] = registerResultLaunchers(activity) + if (this.activity == null) { + this.activity = activity + } + } + + private fun registerResultLaunchers(activity: AppCompatActivity): ResultLaunchers = + ResultLaunchers( + activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult() + ) { result -> + startActivityForResultCallback?.onResult(result) + }, + + activity.registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult() + ) { result -> + startIntentSenderForResultCallback?.onResult(result) + }, + + activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions() + ) { result -> + requestPermissionsCallback?.onResult(result) + } + ) + + private val currentLaunchers: ResultLaunchers + get() = launchers[activity] + ?: throw IllegalStateException("the plugin manager has no activity to launch from") + + fun onNewIntent(intent: Intent) { + for (plugin in plugins.values) { + plugin.instance.onNewIntent(intent) + } + } + + fun onPause(activity: AppCompatActivity) { + for (plugin in plugins.values) { + plugin.instance.triggerOnPause(activity) + } + } + + fun onResume(activity: AppCompatActivity) { + for (plugin in plugins.values) { + plugin.instance.triggerOnResume(activity) + } + } + + fun onRestart(activity: AppCompatActivity) { + for (plugin in plugins.values) { + plugin.instance.triggerOnRestart(activity) + } + } + + fun onStop(activity: AppCompatActivity) { + for (plugin in plugins.values) { + plugin.instance.triggerOnStop(activity) + } + } + + fun onDestroy(activity: AppCompatActivity) { + for (plugin in plugins.values) { + plugin.instance.triggerOnDestroy(activity) + } + + launchers.remove(activity) + if (this.activity == activity) { + // Whatever is left already holds its own launchers, registered when it was created. Moving + // this activity's launchers over instead would mean registering against an activity that is + // already running, which registerForActivityResult rejects with an IllegalStateException. + this.activity = launchers.keys.firstOrNull() + } + } + + fun onConfigurationChanged(newConfig: Configuration) { + for (plugin in plugins.values) { + plugin.instance.onConfigurationChanged(newConfig) + } + } + + fun startActivityForResult(intent: Intent, callback: ActivityResultCallback) { + startActivityForResultCallback = callback + currentLaunchers.startActivityForResult.launch(intent) + } + + fun startIntentSenderForResult(intent: IntentSenderRequest, callback: ActivityResultCallback) { + startIntentSenderForResultCallback = callback + currentLaunchers.startIntentSenderForResult.launch(intent) + } + + fun requestPermissions( + permissionStrings: Array, + callback: RequestPermissionsCallback + ) { + requestPermissionsCallback = callback + currentLaunchers.requestPermissions.launch(permissionStrings) + } + + @JniMethod + fun onWebViewCreated(webView: WebView) { + for ((_, plugin) in plugins) { + if (!plugin.loaded) { + plugin.load(webView) + } + } + } + + @JniMethod + fun load(webView: WebView?, name: String, plugin: Plugin, config: String) { + val handle = PluginHandle(this, name, plugin, config, jsonMapper) + plugins[name] = handle + if (webView != null) { + plugin.load(webView) + } + } + + @JniMethod + fun runCommand(id: Int, pluginId: String, command: String, data: String) { + val successId = 0L + val errorId = 1L + val invoke = Invoke(id.toLong(), command, successId, errorId, { fn, result -> + var success: String? = null + var error: String? = null + if (fn == successId) { + success = result + } else { + error = result + } + handlePluginResponse(id, success, error) + }, data, jsonMapper) + + dispatchPluginMessage(invoke, pluginId) + } + + private fun dispatchPluginMessage(invoke: Invoke, pluginId: String) { + Logger.verbose( + Logger.tags("Plugin"), + "Tauri plugin: pluginId: $pluginId, command: ${invoke.command}" + ) + + try { + val plugin = plugins[pluginId] + if (plugin == null) { + invoke.reject("Plugin $pluginId not initialized") + } else { + plugins[pluginId]?.invoke(invoke) + } + } catch (e: Exception) { + var exception: Throwable = e + if (exception.message?.isEmpty() != false) { + if (e is InvocationTargetException) { + exception = e.targetException + } + } + invoke.reject(if (exception.message?.isEmpty() != false) { exception.toString() } else { exception.message }) + } + } + + 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?) + private external fun sendChannelData(id: Long, data: String) +} + +@InvokeArg +internal class Config { + lateinit var plugins: Map +} diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginMethodData.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginMethodData.kt new file mode 100644 index 0000000..acb0947 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginMethodData.kt @@ -0,0 +1,16 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import app.tauri.annotation.Command +import java.lang.reflect.Method + +class CommandData( + val method: Method, methodDecorator: Command +) { + + // The name of the method + val name: String = method.name +} diff --git a/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginResult.kt b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginResult.kt new file mode 100644 index 0000000..601cd48 --- /dev/null +++ b/crates/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginResult.kt @@ -0,0 +1,67 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import android.annotation.SuppressLint +import app.tauri.Logger +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* + +class PluginResult @JvmOverloads constructor(json: JSObject? = JSObject()) { + private val json: JSObject + + init { + this.json = json ?: JSObject() + } + + fun put(name: String, value: Boolean): PluginResult { + return jsonPut(name, value) + } + + fun put(name: String, value: Double): PluginResult { + return jsonPut(name, value) + } + + fun put(name: String, value: Int): PluginResult { + return jsonPut(name, value) + } + + fun put(name: String, value: Long): PluginResult { + return jsonPut(name, value) + } + + /** + * Format a date as an ISO string + */ + @SuppressLint("SimpleDateFormat") + fun put(name: String, value: Date): PluginResult { + val tz: TimeZone = TimeZone.getTimeZone("UTC") + val df: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'") + df.timeZone = tz + return jsonPut(name, df.format(value)) + } + + fun put(name: String, value: Any?): PluginResult { + return jsonPut(name, value) + } + + fun put(name: String, value: PluginResult): PluginResult { + return jsonPut(name, value.json) + } + + private fun jsonPut(name: String, value: Any?): PluginResult { + try { + json.put(name, value) + } catch (ex: Exception) { + Logger.error(Logger.tags("Plugin"), "", ex) + } + return this + } + + override fun toString(): String { + return json.toString() + } +} diff --git a/crates/tauri/mobile/android/src/test/java/app/tauri/ExampleUnitTest.kt b/crates/tauri/mobile/android/src/test/java/app/tauri/ExampleUnitTest.kt new file mode 100644 index 0000000..16022f4 --- /dev/null +++ b/crates/tauri/mobile/android/src/test/java/app/tauri/ExampleUnitTest.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/crates/tauri/mobile/ios-api/.gitignore b/crates/tauri/mobile/ios-api/.gitignore new file mode 100644 index 0000000..5922fda --- /dev/null +++ b/crates/tauri/mobile/ios-api/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/crates/tauri/mobile/ios-api/Package.swift b/crates/tauri/mobile/ios-api/Package.swift new file mode 100644 index 0000000..c7a7332 --- /dev/null +++ b/crates/tauri/mobile/ios-api/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version:5.3 +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "Tauri", + platforms: [ + .macOS(.v10_13), + .iOS(.v11), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "Tauri", + type: .static, + targets: ["Tauri"]) + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(name: "SwiftRs", url: "https://github.com/Brendonovich/swift-rs", from: "1.0.0") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "Tauri", + dependencies: [ + .byName(name: "SwiftRs") + ], + path: "Sources" + ), + .testTarget( + name: "TauriTests", + dependencies: ["Tauri"] + ), + ] +) diff --git a/crates/tauri/mobile/ios-api/README.md b/crates/tauri/mobile/ios-api/README.md new file mode 100644 index 0000000..52c3f1c --- /dev/null +++ b/crates/tauri/mobile/ios-api/README.md @@ -0,0 +1,3 @@ +# Tauri + +Tauri iOS API. diff --git a/crates/tauri/mobile/ios-api/Sources/Tauri/Channel.swift b/crates/tauri/mobile/ios-api/Sources/Tauri/Channel.swift new file mode 100644 index 0000000..add065c --- /dev/null +++ b/crates/tauri/mobile/ios-api/Sources/Tauri/Channel.swift @@ -0,0 +1,65 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation + +let CHANNEL_PREFIX = "__CHANNEL__:" +let channelDataKey = CodingUserInfoKey(rawValue: "sendChannelData")! + +public class Channel: Decodable { + public let id: UInt64 + let handler: (UInt64, String) -> Void + + public required init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let channelDef = try container.decode(String.self) + + let components = channelDef.components(separatedBy: CHANNEL_PREFIX) + if components.count < 2 { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Invalid channel definition from \(channelDef)" + ) + + } + guard let channelId = UInt64(components[1]) else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Invalid channel ID from \(channelDef)" + ) + } + + guard let handler = decoder.userInfo[channelDataKey] as? (UInt64, String) -> Void else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "missing userInfo for Channel handler. This is a Tauri issue" + ) + } + + self.id = channelId + self.handler = handler + } + + func serialize(_ data: JsonValue) -> String { + do { + return try data.jsonRepresentation() ?? "\"Failed to serialize payload\"" + } catch { + return "\"\(error)\"" + } + } + + public func send(_ data: JsonObject) { + send(.dictionary(data)) + } + + public func send(_ data: JsonValue) { + handler(id, serialize(data)) + } + + public func send(_ data: T) throws { + let json = try JSONEncoder().encode(data) + handler(id, String(decoding: json, as: UTF8.self)) + } + +} diff --git a/crates/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift b/crates/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift new file mode 100644 index 0000000..6f81086 --- /dev/null +++ b/crates/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift @@ -0,0 +1,118 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation +import UIKit + +@objc public class Invoke: NSObject { + public let command: String + let callback: UInt64 + let error: UInt64 + let data: String + let sendResponse: (UInt64, String?) -> Void + let sendChannelData: (UInt64, String) -> Void + + public init( + command: String, callback: UInt64, error: UInt64, + sendResponse: @escaping (UInt64, String?) -> Void, + sendChannelData: @escaping (UInt64, String) -> Void, data: String + ) { + self.command = command + self.callback = callback + self.error = error + self.data = data + self.sendResponse = sendResponse + self.sendChannelData = sendChannelData + } + + public func getRawArgs() -> String { + return self.data + } + + public func getArgs() throws -> JSObject { + let jsonData = self.data.data(using: .utf8)! + let data = try JSONSerialization.jsonObject(with: jsonData, options: []) + return JSTypes.coerceDictionaryToJSObject( + (data as! NSDictionary), formattingDatesAsStrings: true)! + } + + public func parseArgs(_ type: T.Type) throws -> T { + let jsonData = self.data.data(using: .utf8)! + let decoder = JSONDecoder() + decoder.userInfo[channelDataKey] = sendChannelData + return try decoder.decode(type, from: jsonData) + } + + func serialize(_ data: JsonValue) -> String { + do { + return try data.jsonRepresentation() ?? "\"Failed to serialize payload\"" + } catch { + return "\"\(error)\"" + } + } + + public func resolve() { + sendResponse(callback, nil) + } + + public func resolve(_ data: JsonObject) { + resolve(.dictionary(data)) + } + + public func resolve(_ data: JsonValue) { + sendResponse(callback, serialize(data)) + } + + public func resolve(_ data: T) { + do { + let json = try JSONEncoder().encode(data) + sendResponse(callback, String(decoding: json, as: UTF8.self)) + } catch { + sendResponse(self.error, "\"\(error)\"") + } + } + + public func reject( + _ message: String, code: String? = nil, error: Error? = nil, data: JsonValue? = nil + ) { + let payload: NSMutableDictionary = [ + "message": message + ] + + if let code = code { + payload["code"] = code + } + + if let error = error { + payload["error"] = error + } + + if let data = data { + switch data { + case .dictionary(let dict): + for entry in dict { + payload[entry.key] = entry.value + } + } + } + + sendResponse(self.error, serialize(.dictionary(payload as! JsonObject))) + } + + public func unimplemented() { + unimplemented("not implemented") + } + + public func unimplemented(_ message: String) { + reject(message) + } + + public func unavailable() { + unavailable("not available") + } + + public func unavailable(_ message: String) { + reject(message) + } +} diff --git a/crates/tauri/mobile/ios-api/Sources/Tauri/JSTypes.swift b/crates/tauri/mobile/ios-api/Sources/Tauri/JSTypes.swift new file mode 100644 index 0000000..0ac9f2c --- /dev/null +++ b/crates/tauri/mobile/ios-api/Sources/Tauri/JSTypes.swift @@ -0,0 +1,123 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation + +// declare our empty protocol, and conformance, for typing +public protocol JSValue {} +extension String: JSValue {} +extension Bool: JSValue {} +extension Int: JSValue {} +extension Float: JSValue {} +extension Double: JSValue {} +extension NSNumber: JSValue {} +extension NSNull: JSValue {} +extension Array: JSValue {} +extension Date: JSValue {} +extension Dictionary: JSValue where Key == String, Value == JSValue {} + +// convenience aliases +public typealias JSObject = [String: JSValue] +public typealias JSArray = [JSValue] + +extension Dictionary where Key == String, Value == JSValue { + public func getValue(_ key: String) -> JSValue? { + return self[key] + } + + public func getString(_ key: String) -> String? { + return self[key] as? String + } + + public func getBool(_ key: String) -> Bool? { + return self[key] as? Bool + } + + public func getInt(_ key: String) -> Int? { + return self[key] as? Int + } + + public func getFloat(_ key: String) -> Float? { + if let floatValue = self[key] as? Float { + return floatValue + } else if let doubleValue = self[key] as? Double { + return Float(doubleValue) + } + return nil + } + + public func getDouble(_ key: String) -> Double? { + return self[key] as? Double + } + + public func getArray(_ key: String) -> JSArray? { + return self[key] as? JSArray + } + + public func getObject(_ key: String) -> JSObject? { + return self[key] as? JSObject + } +} + +/* + Simply casting objects from foundation class clusters (such as __NSArrayM) + doesn't work with the JSValue protocol and will always fail. So we need to + recursively and explicitly convert each value in the dictionary. + */ +public enum JSTypes {} +extension JSTypes { + public static func coerceDictionaryToJSObject( + _ dictionary: NSDictionary?, formattingDatesAsStrings: Bool = false + ) -> JSObject? { + return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject + } + + public static func coerceDictionaryToJSObject( + _ dictionary: [AnyHashable: Any]?, formattingDatesAsStrings: Bool = false + ) -> JSObject? { + return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject + } +} + +private let dateStringFormatter = ISO8601DateFormatter() + +// We need a large switch statement because we have a lot of types. +// swiftlint:disable:next cyclomatic_complexity +private func coerceToJSValue(_ value: Any?, formattingDates: Bool) -> JSValue? { + guard let value = value else { + return nil + } + switch value { + case let stringValue as String: + return stringValue + case let numberValue as NSNumber: + return numberValue + case let boolValue as Bool: + return boolValue + case let intValue as Int: + return intValue + case let floatValue as Float: + return floatValue + case let doubleValue as Double: + return doubleValue + case let dateValue as Date: + if formattingDates { + return dateStringFormatter.string(from: dateValue) + } + return dateValue + case let nullValue as NSNull: + return nullValue + case let arrayValue as NSArray: + return arrayValue.compactMap { coerceToJSValue($0, formattingDates: formattingDates) } + case let dictionaryValue as NSDictionary: + let keys = dictionaryValue.allKeys.compactMap { $0 as? String } + var result: JSObject = [:] + for key in keys { + result[key] = coerceToJSValue(dictionaryValue[key], formattingDates: formattingDates) + } + return result + default: + return nil + } +} diff --git a/crates/tauri/mobile/ios-api/Sources/Tauri/JsonValue.swift b/crates/tauri/mobile/ios-api/Sources/Tauri/JsonValue.swift new file mode 100644 index 0000000..ae6d96f --- /dev/null +++ b/crates/tauri/mobile/ios-api/Sources/Tauri/JsonValue.swift @@ -0,0 +1,58 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation + +public typealias JsonObject = [String: Any?] + +public enum JsonValue { + case dictionary(JsonObject) + + enum SerializationError: Error { + case invalidObject + } + + public func jsonRepresentation(includingFields: JsonObject? = nil) throws -> String? { + switch self { + case .dictionary(var dictionary): + if let fields = includingFields { + dictionary.merge(fields) { (current, _) in current } + } + dictionary = prepare(dictionary: dictionary) + guard JSONSerialization.isValidJSONObject(dictionary) else { + throw SerializationError.invalidObject + } + let data = try JSONSerialization.data(withJSONObject: dictionary, options: []) + return String(data: data, encoding: .utf8) + } + } + + private static let formatter = ISO8601DateFormatter() + + private func prepare(dictionary: JsonObject) -> JsonObject { + return dictionary.mapValues { (value) -> Any in + if let date = value as? Date { + return JsonValue.formatter.string(from: date) + } else if let aDictionary = value as? JsonObject { + return prepare(dictionary: aDictionary) + } else if let anArray = value as? [Any] { + return prepare(array: anArray) + } + return value + } + } + + private func prepare(array: [Any]) -> [Any] { + return array.map { (value) -> Any in + if let date = value as? Date { + return JsonValue.formatter.string(from: date) + } else if let aDictionary = value as? JsonObject { + return prepare(dictionary: aDictionary) + } else if let anArray = value as? [Any] { + return prepare(array: anArray) + } + return value + } + } +} diff --git a/crates/tauri/mobile/ios-api/Sources/Tauri/Logger.swift b/crates/tauri/mobile/ios-api/Sources/Tauri/Logger.swift new file mode 100644 index 0000000..d055090 --- /dev/null +++ b/crates/tauri/mobile/ios-api/Sources/Tauri/Logger.swift @@ -0,0 +1,139 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import os.log +import UIKit +import Foundation + +class StdoutRedirector { + private var originalStdout: Int32 = -1 + private var originalStderr: Int32 = -1 + private var stdoutPipe: [Int32] = [-1, -1] + private var stderrPipe: [Int32] = [-1, -1] + private var stdoutReadSource: DispatchSourceRead? + private var stderrReadSource: DispatchSourceRead? + + func start() { + originalStdout = dup(STDOUT_FILENO) + originalStderr = dup(STDERR_FILENO) + + guard Darwin.pipe(&stdoutPipe) == 0, + Darwin.pipe(&stderrPipe) == 0 else { + Logger.error("Failed to create stdout/stderr pipes") + return + } + + dup2(stdoutPipe[1], STDOUT_FILENO) + dup2(stderrPipe[1], STDERR_FILENO) + close(stdoutPipe[1]) + close(stderrPipe[1]) + + stdoutReadSource = createReader( + readPipe: stdoutPipe[0], + writeToOriginal: originalStdout, + label: "stdout" + ) + + stderrReadSource = createReader( + readPipe: stderrPipe[0], + writeToOriginal: originalStderr, + label: "stderr" + ) + } + + private func createReader( + readPipe: Int32, + writeToOriginal: Int32, + label: String + ) -> DispatchSourceRead { + let source = DispatchSource.makeReadSource( + fileDescriptor: readPipe, + queue: .global(qos: .utility) + ) + + source.setEventHandler { + let bufferSize = 4096 + var buffer = [UInt8](repeating: 0, count: bufferSize) + let bytesRead = read(readPipe, &buffer, bufferSize) + + if bytesRead > 0 { + let output = String( + bytes: buffer[0..(_ type: T.Type) throws -> T { + let jsonData = self.config.data(using: .utf8)! + let decoder = JSONDecoder() + return try decoder.decode(type, from: jsonData) + } + + @objc open func load(webview: WKWebView) {} + + @objc open func checkPermissions(_ invoke: Invoke) { + invoke.resolve() + } + + @objc open func requestPermissions(_ invoke: Invoke) { + invoke.resolve() + } + + public func trigger(_ event: String, data: JSObject) { + if let eventListeners = listeners[event] { + for channel in eventListeners { + channel.send(data) + } + } + } + + public func trigger(_ event: String, data: T) throws { + if let eventListeners = listeners[event] { + for channel in eventListeners { + try channel.send(data) + } + } + } + + @objc func registerListener(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(RegisterListenerArgs.self) + + if var eventListeners = listeners[args.event] { + eventListeners.append(args.handler) + listeners[args.event] = eventListeners + } else { + listeners[args.event] = [args.handler] + } + + invoke.resolve() + } + + @objc func removeListener(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(RemoveListenerArgs.self) + + if let eventListeners = listeners[args.event] { + + listeners[args.event] = eventListeners.filter { $0.id != args.channelId } + } + + invoke.resolve() + } +} diff --git a/crates/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift b/crates/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift new file mode 100644 index 0000000..82818c6 --- /dev/null +++ b/crates/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift @@ -0,0 +1,157 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation +import SwiftRs +import UIKit +import WebKit +import os.log + +class PluginHandle { + var instance: Plugin + var loaded = false + + init(plugin: Plugin) { + instance = plugin + } +} + +public class PluginManager { + static let shared: PluginManager = PluginManager() + public var viewController: UIViewController? + var plugins: [String: PluginHandle] = [:] + var ipcDispatchQueue = DispatchQueue(label: "ipc") + public var isSimEnvironment: Bool { + #if targetEnvironment(simulator) + return true + #else + return false + #endif + } + + public func assetUrl(fromLocalURL url: URL?) -> URL? { + guard let inputURL = url else { + return nil + } + + return URL(string: "asset://localhost")!.appendingPathComponent(inputURL.path) + } + + func onWebviewCreated(_ webview: WKWebView) { + for (_, handle) in plugins { + if !handle.loaded { + handle.instance.load(webview: webview) + } + } + } + + func load(name: String, plugin: P, config: String, webview: WKWebView?) { + plugin.setConfig(config) + let handle = PluginHandle(plugin: plugin) + if let webview = webview { + handle.instance.load(webview: webview) + handle.loaded = true + } + plugins[name] = handle + } + + func invoke(name: String, invoke: Invoke) { + if let plugin = plugins[name] { + ipcDispatchQueue.async { + let selectorWithCompletionHandler = Selector(("\(invoke.command):completionHandler:")) + let selectorWithThrows = Selector(("\(invoke.command):error:")) + + if plugin.instance.responds(to: selectorWithCompletionHandler) { + let completion: @convention(block) (NSError?) -> Void = { error in + if let error = error { + invoke.reject("\(error)") + } + } + + let blockObj: AnyObject = unsafeBitCast(completion, to: AnyObject.self) + let imp = plugin.instance.method(for: selectorWithCompletionHandler) + + typealias Fn = @convention(c) (AnyObject, Selector, Invoke, AnyObject) -> Void + let fn = unsafeBitCast(imp, to: Fn.self) + fn(plugin.instance, selectorWithCompletionHandler, invoke, blockObj) + } else if plugin.instance.responds(to: selectorWithThrows) { + var error: NSError? = nil + withUnsafeMutablePointer(to: &error) { + let methodIMP: IMP! = plugin.instance.method(for: selectorWithThrows) + unsafeBitCast( + methodIMP, to: (@convention(c) (Any?, Selector, Invoke, OpaquePointer) -> Void).self)( + plugin.instance, selectorWithThrows, invoke, OpaquePointer($0)) + } + if let error = error { + invoke.reject("\(error)") + // TODO: app crashes without this leak + let _ = Unmanaged.passRetained(error) + } + } else { + let selector = Selector(("\(invoke.command):")) + if plugin.instance.responds(to: selector) { + plugin.instance.perform(selector, with: invoke) + } else { + invoke.reject("No command \(invoke.command) found for plugin \(name)") + } + } + } + } else { + invoke.reject("Plugin \(name) not initialized") + } + } +} + +extension PluginManager: NSCopying { + public func copy(with zone: NSZone? = nil) -> Any { + return self + } +} + +private var stdoutRedirector: StdoutRedirector? + +@_cdecl("log_stdout") +func logStdout() { + stdoutRedirector = StdoutRedirector() + stdoutRedirector!.start() +} + +@_cdecl("register_plugin") +func registerPlugin(name: SRString, plugin: NSObject, config: SRString, webview: WKWebView?) { + PluginManager.shared.load( + name: name.toString(), + plugin: plugin as! Plugin, + config: config.toString(), + webview: webview + ) +} + +@_cdecl("on_webview_created") +func onWebviewCreated(webview: WKWebView, viewController: UIViewController) { + PluginManager.shared.viewController = viewController + PluginManager.shared.onWebviewCreated(webview) +} + +@_cdecl("run_plugin_command") +func runCommand( + id: Int, + name: SRString, + command: SRString, + data: SRString, + callback: @escaping @convention(c) (Int, Bool, UnsafePointer) -> Void, + sendChannelData: @escaping @convention(c) (UInt64, UnsafePointer) -> Void +) { + let callbackId: UInt64 = 0 + let errorId: UInt64 = 1 + let invoke = Invoke( + command: command.toString(), callback: callbackId, error: errorId, + sendResponse: { (fn: UInt64, payload: String?) -> Void in + let success = fn == callbackId + callback(id, success, payload ?? "null") + }, + sendChannelData: { (id: UInt64, payload: String) -> Void in + sendChannelData(id, payload) + }, data: data.toString()) + PluginManager.shared.invoke(name: name.toString(), invoke: invoke) +} diff --git a/crates/tauri/mobile/ios-api/Sources/Tauri/UiUtils.swift b/crates/tauri/mobile/ios-api/Sources/Tauri/UiUtils.swift new file mode 100644 index 0000000..4097596 --- /dev/null +++ b/crates/tauri/mobile/ios-api/Sources/Tauri/UiUtils.swift @@ -0,0 +1,15 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import UIKit + +public class UIUtils { + public static func centerPopover(rootViewController: UIViewController?, popoverController: UIViewController) { + if let viewController = rootViewController { + popoverController.popoverPresentationController?.sourceRect = CGRect(x: viewController.view.center.x, y: viewController.view.center.y, width: 0, height: 0) + popoverController.popoverPresentationController?.sourceView = viewController.view + popoverController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.up + } + } +} diff --git a/crates/tauri/mobile/ios-api/Tests/TauriTests/TauriTests.swift b/crates/tauri/mobile/ios-api/Tests/TauriTests/TauriTests.swift new file mode 100644 index 0000000..49f6ee2 --- /dev/null +++ b/crates/tauri/mobile/ios-api/Tests/TauriTests/TauriTests.swift @@ -0,0 +1,15 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import XCTest +@testable import Tauri + +final class TauriTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(Tauri().text, "Hello, World!") + } +} diff --git a/crates/tauri/mobile/proguard-tauri.pro b/crates/tauri/mobile/proguard-tauri.pro new file mode 100644 index 0000000..6bc0b4b --- /dev/null +++ b/crates/tauri/mobile/proguard-tauri.pro @@ -0,0 +1,5 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. + +-keep class $PACKAGE.TauriActivity { + public app.tauri.plugin.PluginManager getPluginManager(); +} diff --git a/crates/tauri/permissions/app/autogenerated/reference.md b/crates/tauri/permissions/app/autogenerated/reference.md new file mode 100644 index 0000000..d6e5858 --- /dev/null +++ b/crates/tauri/permissions/app/autogenerated/reference.md @@ -0,0 +1,440 @@ +## Default Permission + +Default permissions for the plugin. + +#### This default permission set includes the following: + +- `allow-version` +- `allow-name` +- `allow-tauri-version` +- `allow-identifier` +- `allow-bundle-type` +- `allow-register-listener` +- `allow-remove-listener` +- `allow-supports-multiple-windows` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:app:allow-app-hide` + + + +Enables the app_hide command without any pre-configured scope. + +
+ +`core:app:deny-app-hide` + + + +Denies the app_hide command without any pre-configured scope. + +
+ +`core:app:allow-app-show` + + + +Enables the app_show command without any pre-configured scope. + +
+ +`core:app:deny-app-show` + + + +Denies the app_show command without any pre-configured scope. + +
+ +`core:app:allow-bundle-type` + + + +Enables the bundle_type command without any pre-configured scope. + +
+ +`core:app:deny-bundle-type` + + + +Denies the bundle_type command without any pre-configured scope. + +
+ +`core:app:allow-default-window-icon` + + + +Enables the default_window_icon command without any pre-configured scope. + +
+ +`core:app:deny-default-window-icon` + + + +Denies the default_window_icon command without any pre-configured scope. + +
+ +`core:app:allow-exit` + + + +Enables the exit command without any pre-configured scope. + +
+ +`core:app:deny-exit` + + + +Denies the exit command without any pre-configured scope. + +
+ +`core:app:allow-fetch-data-store-identifiers` + + + +Enables the fetch_data_store_identifiers command without any pre-configured scope. + +
+ +`core:app:deny-fetch-data-store-identifiers` + + + +Denies the fetch_data_store_identifiers command without any pre-configured scope. + +
+ +`core:app:allow-identifier` + + + +Enables the identifier command without any pre-configured scope. + +
+ +`core:app:deny-identifier` + + + +Denies the identifier command without any pre-configured scope. + +
+ +`core:app:allow-name` + + + +Enables the name command without any pre-configured scope. + +
+ +`core:app:deny-name` + + + +Denies the name command without any pre-configured scope. + +
+ +`core:app:allow-register-listener` + + + +Enables the register_listener command without any pre-configured scope. + +
+ +`core:app:deny-register-listener` + + + +Denies the register_listener command without any pre-configured scope. + +
+ +`core:app:allow-remove-data-store` + + + +Enables the remove_data_store command without any pre-configured scope. + +
+ +`core:app:deny-remove-data-store` + + + +Denies the remove_data_store command without any pre-configured scope. + +
+ +`core:app:allow-remove-listener` + + + +Enables the remove_listener command without any pre-configured scope. + +
+ +`core:app:deny-remove-listener` + + + +Denies the remove_listener command without any pre-configured scope. + +
+ +`core:app:allow-set-app-theme` + + + +Enables the set_app_theme command without any pre-configured scope. + +
+ +`core:app:deny-set-app-theme` + + + +Denies the set_app_theme command without any pre-configured scope. + +
+ +`core:app:allow-set-dock-visibility` + + + +Enables the set_dock_visibility command without any pre-configured scope. + +
+ +`core:app:deny-set-dock-visibility` + + + +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` + + + +Enables the tauri_version command without any pre-configured scope. + +
+ +`core:app:deny-tauri-version` + + + +Denies the tauri_version command without any pre-configured scope. + +
+ +`core:app:allow-version` + + + +Enables the version command without any pre-configured scope. + +
+ +`core:app:deny-version` + + + +Denies the version command without any pre-configured scope. + +
diff --git a/crates/tauri/permissions/event/autogenerated/reference.md b/crates/tauri/permissions/event/autogenerated/reference.md new file mode 100644 index 0000000..1907b4a --- /dev/null +++ b/crates/tauri/permissions/event/autogenerated/reference.md @@ -0,0 +1,124 @@ +## Default Permission + +Default permissions for the plugin, which enables all commands. + +#### This default permission set includes the following: + +- `allow-listen` +- `allow-unlisten` +- `allow-emit` +- `allow-emit-to` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:event:allow-emit` + + + +Enables the emit command without any pre-configured scope. + +
+ +`core:event:deny-emit` + + + +Denies the emit command without any pre-configured scope. + +
+ +`core:event:allow-emit-to` + + + +Enables the emit_to command without any pre-configured scope. + +
+ +`core:event:deny-emit-to` + + + +Denies the emit_to command without any pre-configured scope. + +
+ +`core:event:allow-listen` + + + +Enables the listen command without any pre-configured scope. + +
+ +`core:event:deny-listen` + + + +Denies the listen command without any pre-configured scope. + +
+ +`core:event:allow-unlisten` + + + +Enables the unlisten command without any pre-configured scope. + +
+ +`core:event:deny-unlisten` + + + +Denies the unlisten command without any pre-configured scope. + +
diff --git a/crates/tauri/permissions/image/autogenerated/reference.md b/crates/tauri/permissions/image/autogenerated/reference.md new file mode 100644 index 0000000..f66c34c --- /dev/null +++ b/crates/tauri/permissions/image/autogenerated/reference.md @@ -0,0 +1,151 @@ +## Default Permission + +Default permissions for the plugin, which enables all commands. + +#### This default permission set includes the following: + +- `allow-new` +- `allow-from-bytes` +- `allow-from-path` +- `allow-rgba` +- `allow-size` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:image:allow-from-bytes` + + + +Enables the from_bytes command without any pre-configured scope. + +
+ +`core:image:deny-from-bytes` + + + +Denies the from_bytes command without any pre-configured scope. + +
+ +`core:image:allow-from-path` + + + +Enables the from_path command without any pre-configured scope. + +
+ +`core:image:deny-from-path` + + + +Denies the from_path command without any pre-configured scope. + +
+ +`core:image:allow-new` + + + +Enables the new command without any pre-configured scope. + +
+ +`core:image:deny-new` + + + +Denies the new command without any pre-configured scope. + +
+ +`core:image:allow-rgba` + + + +Enables the rgba command without any pre-configured scope. + +
+ +`core:image:deny-rgba` + + + +Denies the rgba command without any pre-configured scope. + +
+ +`core:image:allow-size` + + + +Enables the size command without any pre-configured scope. + +
+ +`core:image:deny-size` + + + +Denies the size command without any pre-configured scope. + +
diff --git a/crates/tauri/permissions/menu/autogenerated/reference.md b/crates/tauri/permissions/menu/autogenerated/reference.md new file mode 100644 index 0000000..4638fa9 --- /dev/null +++ b/crates/tauri/permissions/menu/autogenerated/reference.md @@ -0,0 +1,610 @@ +## Default Permission + +Default permissions for the plugin, which enables all commands. + +#### This default permission set includes the following: + +- `allow-new` +- `allow-append` +- `allow-prepend` +- `allow-insert` +- `allow-remove` +- `allow-remove-at` +- `allow-items` +- `allow-get` +- `allow-popup` +- `allow-create-default` +- `allow-set-as-app-menu` +- `allow-set-as-window-menu` +- `allow-text` +- `allow-set-text` +- `allow-is-enabled` +- `allow-set-enabled` +- `allow-set-accelerator` +- `allow-set-as-windows-menu-for-nsapp` +- `allow-set-as-help-menu-for-nsapp` +- `allow-is-checked` +- `allow-set-checked` +- `allow-set-icon` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:menu:allow-append` + + + +Enables the append command without any pre-configured scope. + +
+ +`core:menu:deny-append` + + + +Denies the append command without any pre-configured scope. + +
+ +`core:menu:allow-create-default` + + + +Enables the create_default command without any pre-configured scope. + +
+ +`core:menu:deny-create-default` + + + +Denies the create_default command without any pre-configured scope. + +
+ +`core:menu:allow-get` + + + +Enables the get command without any pre-configured scope. + +
+ +`core:menu:deny-get` + + + +Denies the get command without any pre-configured scope. + +
+ +`core:menu:allow-insert` + + + +Enables the insert command without any pre-configured scope. + +
+ +`core:menu:deny-insert` + + + +Denies the insert command without any pre-configured scope. + +
+ +`core:menu:allow-is-checked` + + + +Enables the is_checked command without any pre-configured scope. + +
+ +`core:menu:deny-is-checked` + + + +Denies the is_checked command without any pre-configured scope. + +
+ +`core:menu:allow-is-enabled` + + + +Enables the is_enabled command without any pre-configured scope. + +
+ +`core:menu:deny-is-enabled` + + + +Denies the is_enabled command without any pre-configured scope. + +
+ +`core:menu:allow-items` + + + +Enables the items command without any pre-configured scope. + +
+ +`core:menu:deny-items` + + + +Denies the items command without any pre-configured scope. + +
+ +`core:menu:allow-new` + + + +Enables the new command without any pre-configured scope. + +
+ +`core:menu:deny-new` + + + +Denies the new command without any pre-configured scope. + +
+ +`core:menu:allow-popup` + + + +Enables the popup command without any pre-configured scope. + +
+ +`core:menu:deny-popup` + + + +Denies the popup command without any pre-configured scope. + +
+ +`core:menu:allow-prepend` + + + +Enables the prepend command without any pre-configured scope. + +
+ +`core:menu:deny-prepend` + + + +Denies the prepend command without any pre-configured scope. + +
+ +`core:menu:allow-remove` + + + +Enables the remove command without any pre-configured scope. + +
+ +`core:menu:deny-remove` + + + +Denies the remove command without any pre-configured scope. + +
+ +`core:menu:allow-remove-at` + + + +Enables the remove_at command without any pre-configured scope. + +
+ +`core:menu:deny-remove-at` + + + +Denies the remove_at command without any pre-configured scope. + +
+ +`core:menu:allow-set-accelerator` + + + +Enables the set_accelerator command without any pre-configured scope. + +
+ +`core:menu:deny-set-accelerator` + + + +Denies the set_accelerator command without any pre-configured scope. + +
+ +`core:menu:allow-set-as-app-menu` + + + +Enables the set_as_app_menu command without any pre-configured scope. + +
+ +`core:menu:deny-set-as-app-menu` + + + +Denies the set_as_app_menu command without any pre-configured scope. + +
+ +`core:menu:allow-set-as-help-menu-for-nsapp` + + + +Enables the set_as_help_menu_for_nsapp command without any pre-configured scope. + +
+ +`core:menu:deny-set-as-help-menu-for-nsapp` + + + +Denies the set_as_help_menu_for_nsapp command without any pre-configured scope. + +
+ +`core:menu:allow-set-as-window-menu` + + + +Enables the set_as_window_menu command without any pre-configured scope. + +
+ +`core:menu:deny-set-as-window-menu` + + + +Denies the set_as_window_menu command without any pre-configured scope. + +
+ +`core:menu:allow-set-as-windows-menu-for-nsapp` + + + +Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope. + +
+ +`core:menu:deny-set-as-windows-menu-for-nsapp` + + + +Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope. + +
+ +`core:menu:allow-set-checked` + + + +Enables the set_checked command without any pre-configured scope. + +
+ +`core:menu:deny-set-checked` + + + +Denies the set_checked command without any pre-configured scope. + +
+ +`core:menu:allow-set-enabled` + + + +Enables the set_enabled command without any pre-configured scope. + +
+ +`core:menu:deny-set-enabled` + + + +Denies the set_enabled command without any pre-configured scope. + +
+ +`core:menu:allow-set-icon` + + + +Enables the set_icon command without any pre-configured scope. + +
+ +`core:menu:deny-set-icon` + + + +Denies the set_icon command without any pre-configured scope. + +
+ +`core:menu:allow-set-text` + + + +Enables the set_text command without any pre-configured scope. + +
+ +`core:menu:deny-set-text` + + + +Denies the set_text command without any pre-configured scope. + +
+ +`core:menu:allow-text` + + + +Enables the text command without any pre-configured scope. + +
+ +`core:menu:deny-text` + + + +Denies the text command without any pre-configured scope. + +
diff --git a/crates/tauri/permissions/path/autogenerated/reference.md b/crates/tauri/permissions/path/autogenerated/reference.md new file mode 100644 index 0000000..fe6ec6f --- /dev/null +++ b/crates/tauri/permissions/path/autogenerated/reference.md @@ -0,0 +1,232 @@ +## Default Permission + +Default permissions for the plugin, which enables all commands. + +#### This default permission set includes the following: + +- `allow-resolve-directory` +- `allow-resolve` +- `allow-normalize` +- `allow-join` +- `allow-dirname` +- `allow-extname` +- `allow-basename` +- `allow-is-absolute` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:path:allow-basename` + + + +Enables the basename command without any pre-configured scope. + +
+ +`core:path:deny-basename` + + + +Denies the basename command without any pre-configured scope. + +
+ +`core:path:allow-dirname` + + + +Enables the dirname command without any pre-configured scope. + +
+ +`core:path:deny-dirname` + + + +Denies the dirname command without any pre-configured scope. + +
+ +`core:path:allow-extname` + + + +Enables the extname command without any pre-configured scope. + +
+ +`core:path:deny-extname` + + + +Denies the extname command without any pre-configured scope. + +
+ +`core:path:allow-is-absolute` + + + +Enables the is_absolute command without any pre-configured scope. + +
+ +`core:path:deny-is-absolute` + + + +Denies the is_absolute command without any pre-configured scope. + +
+ +`core:path:allow-join` + + + +Enables the join command without any pre-configured scope. + +
+ +`core:path:deny-join` + + + +Denies the join command without any pre-configured scope. + +
+ +`core:path:allow-normalize` + + + +Enables the normalize command without any pre-configured scope. + +
+ +`core:path:deny-normalize` + + + +Denies the normalize command without any pre-configured scope. + +
+ +`core:path:allow-resolve` + + + +Enables the resolve command without any pre-configured scope. + +
+ +`core:path:deny-resolve` + + + +Denies the resolve command without any pre-configured scope. + +
+ +`core:path:allow-resolve-directory` + + + +Enables the resolve_directory command without any pre-configured scope. + +
+ +`core:path:deny-resolve-directory` + + + +Denies the resolve_directory command without any pre-configured scope. + +
diff --git a/crates/tauri/permissions/resources/autogenerated/reference.md b/crates/tauri/permissions/resources/autogenerated/reference.md new file mode 100644 index 0000000..42fdf1b --- /dev/null +++ b/crates/tauri/permissions/resources/autogenerated/reference.md @@ -0,0 +1,43 @@ +## Default Permission + +Default permissions for the plugin, which enables all commands. + +#### This default permission set includes the following: + +- `allow-close` + +## Permission Table + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:resources:allow-close` + + + +Enables the close command without any pre-configured scope. + +
+ +`core:resources:deny-close` + + + +Denies the close command without any pre-configured scope. + +
diff --git a/crates/tauri/permissions/tray/autogenerated/reference.md b/crates/tauri/permissions/tray/autogenerated/reference.md new file mode 100644 index 0000000..3418588 --- /dev/null +++ b/crates/tauri/permissions/tray/autogenerated/reference.md @@ -0,0 +1,340 @@ +## Default Permission + +Default permissions for the plugin, which enables all commands. + +#### This default permission set includes the following: + +- `allow-new` +- `allow-get-by-id` +- `allow-remove-by-id` +- `allow-set-icon` +- `allow-set-menu` +- `allow-set-tooltip` +- `allow-set-title` +- `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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:tray:allow-get-by-id` + + + +Enables the get_by_id command without any pre-configured scope. + +
+ +`core:tray:deny-get-by-id` + + + +Denies the get_by_id command without any pre-configured scope. + +
+ +`core:tray:allow-new` + + + +Enables the new command without any pre-configured scope. + +
+ +`core:tray:deny-new` + + + +Denies the new command without any pre-configured scope. + +
+ +`core:tray:allow-remove-by-id` + + + +Enables the remove_by_id command without any pre-configured scope. + +
+ +`core:tray:deny-remove-by-id` + + + +Denies the remove_by_id command without any pre-configured scope. + +
+ +`core:tray:allow-set-icon` + + + +Enables the set_icon command without any pre-configured scope. + +
+ +`core:tray:deny-set-icon` + + + +Denies the set_icon command without any pre-configured scope. + +
+ +`core:tray:allow-set-icon-as-template` + + + +Enables the set_icon_as_template command without any pre-configured scope. + +
+ +`core:tray:deny-set-icon-as-template` + + + +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` + + + +Enables the set_menu command without any pre-configured scope. + +
+ +`core:tray:deny-set-menu` + + + +Denies the set_menu command without any pre-configured scope. + +
+ +`core:tray:allow-set-show-menu-on-left-click` + + + +Enables the set_show_menu_on_left_click command without any pre-configured scope. + +
+ +`core:tray:deny-set-show-menu-on-left-click` + + + +Denies the set_show_menu_on_left_click command without any pre-configured scope. + +
+ +`core:tray:allow-set-temp-dir-path` + + + +Enables the set_temp_dir_path command without any pre-configured scope. + +
+ +`core:tray:deny-set-temp-dir-path` + + + +Denies the set_temp_dir_path command without any pre-configured scope. + +
+ +`core:tray:allow-set-title` + + + +Enables the set_title command without any pre-configured scope. + +
+ +`core:tray:deny-set-title` + + + +Denies the set_title command without any pre-configured scope. + +
+ +`core:tray:allow-set-tooltip` + + + +Enables the set_tooltip command without any pre-configured scope. + +
+ +`core:tray:deny-set-tooltip` + + + +Denies the set_tooltip command without any pre-configured scope. + +
+ +`core:tray:allow-set-visible` + + + +Enables the set_visible command without any pre-configured scope. + +
+ +`core:tray:deny-set-visible` + + + +Denies the set_visible command without any pre-configured scope. + +
diff --git a/crates/tauri/permissions/webview/autogenerated/reference.md b/crates/tauri/permissions/webview/autogenerated/reference.md new file mode 100644 index 0000000..7bad522 --- /dev/null +++ b/crates/tauri/permissions/webview/autogenerated/reference.md @@ -0,0 +1,488 @@ +## Default Permission + +Default permissions for the plugin. + +#### This default permission set includes the following: + +- `allow-get-all-webviews` +- `allow-webview-position` +- `allow-webview-size` +- `allow-internal-toggle-devtools` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`core:webview:allow-clear-all-browsing-data` + + + +Enables the clear_all_browsing_data command without any pre-configured scope. + +
+ +`core:webview:deny-clear-all-browsing-data` + + + +Denies the clear_all_browsing_data command without any pre-configured scope. + +
+ +`core:webview:allow-create-webview` + + + +Enables the create_webview command without any pre-configured scope. + +
+ +`core:webview:deny-create-webview` + + + +Denies the create_webview command without any pre-configured scope. + +
+ +`core:webview:allow-create-webview-window` + + + +Enables the create_webview_window command without any pre-configured scope. + +
+ +`core:webview:deny-create-webview-window` + + + +Denies the create_webview_window command without any pre-configured scope. + +
+ +`core:webview:allow-get-all-webviews` + + + +Enables the get_all_webviews command without any pre-configured scope. + +
+ +`core:webview:deny-get-all-webviews` + + + +Denies the get_all_webviews command without any pre-configured scope. + +
+ +`core:webview:allow-internal-toggle-devtools` + + + +Enables the internal_toggle_devtools command without any pre-configured scope. + +
+ +`core:webview:deny-internal-toggle-devtools` + + + +Denies the internal_toggle_devtools command without any pre-configured scope. + +
+ +`core:webview:allow-print` + + + +Enables the print command without any pre-configured scope. + +
+ +`core:webview:deny-print` + + + +Denies the print command without any pre-configured scope. + +
+ +`core:webview:allow-reparent` + + + +Enables the reparent command without any pre-configured scope. + +
+ +`core:webview:deny-reparent` + + + +Denies the reparent command without any pre-configured scope. + +
+ +`core:webview:allow-set-webview-auto-resize` + + + +Enables the set_webview_auto_resize command without any pre-configured scope. + +
+ +`core:webview:deny-set-webview-auto-resize` + + + +Denies the set_webview_auto_resize command without any pre-configured scope. + +
+ +`core:webview:allow-set-webview-background-color` + + + +Enables the set_webview_background_color command without any pre-configured scope. + +
+ +`core:webview:deny-set-webview-background-color` + + + +Denies the set_webview_background_color command without any pre-configured scope. + +
+ +`core:webview:allow-set-webview-focus` + + + +Enables the set_webview_focus command without any pre-configured scope. + +
+ +`core:webview:deny-set-webview-focus` + + + +Denies the set_webview_focus command without any pre-configured scope. + +
+ +`core:webview:allow-set-webview-position` + + + +Enables the set_webview_position command without any pre-configured scope. + +
+ +`core:webview:deny-set-webview-position` + + + +Denies the set_webview_position command without any pre-configured scope. + +
+ +`core:webview:allow-set-webview-size` + + + +Enables the set_webview_size command without any pre-configured scope. + +
+ +`core:webview:deny-set-webview-size` + + + +Denies the set_webview_size command without any pre-configured scope. + +
+ +`core:webview:allow-set-webview-zoom` + + + +Enables the set_webview_zoom command without any pre-configured scope. + +
+ +`core:webview:deny-set-webview-zoom` + + + +Denies the set_webview_zoom command without any pre-configured scope. + +
+ +`core:webview:allow-webview-close` + + + +Enables the webview_close command without any pre-configured scope. + +
+ +`core:webview:deny-webview-close` + + + +Denies the webview_close command without any pre-configured scope. + +
+ +`core:webview:allow-webview-hide` + + + +Enables the webview_hide command without any pre-configured scope. + +
+ +`core:webview:deny-webview-hide` + + + +Denies the webview_hide command without any pre-configured scope. + +
+ +`core:webview:allow-webview-position` + + + +Enables the webview_position command without any pre-configured scope. + +
+ +`core:webview:deny-webview-position` + + + +Denies the webview_position command without any pre-configured scope. + +
+ +`core:webview:allow-webview-show` + + + +Enables the webview_show command without any pre-configured scope. + +
+ +`core:webview:deny-webview-show` + + + +Denies the webview_show command without any pre-configured scope. + +
+ +`core:webview:allow-webview-size` + + + +Enables the webview_size command without any pre-configured scope. + +
+ +`core:webview:deny-webview-size` + + + +Denies the webview_size command without any pre-configured scope. + +
diff --git a/crates/tauri/permissions/window/autogenerated/reference.md b/crates/tauri/permissions/window/autogenerated/reference.md new file mode 100644 index 0000000..585943a --- /dev/null +++ b/crates/tauri/permissions/window/autogenerated/reference.md @@ -0,0 +1,2098 @@ +## Default Permission + +Default permissions for the plugin. + +#### This default permission set includes the following: + +- `allow-get-all-windows` +- `allow-scale-factor` +- `allow-inner-position` +- `allow-outer-position` +- `allow-inner-size` +- `allow-outer-size` +- `allow-is-fullscreen` +- `allow-is-minimized` +- `allow-is-maximized` +- `allow-is-focused` +- `allow-is-decorated` +- `allow-is-resizable` +- `allow-is-maximizable` +- `allow-is-minimizable` +- `allow-is-closable` +- `allow-is-visible` +- `allow-is-enabled` +- `allow-title` +- `allow-current-monitor` +- `allow-primary-monitor` +- `allow-monitor-from-point` +- `allow-available-monitors` +- `allow-cursor-position` +- `allow-theme` +- `allow-is-always-on-top` +- `allow-activity-name` +- `allow-scene-identifier` +- `allow-internal-toggle-maximize` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`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. + +
+ +`core:window:allow-available-monitors` + + + +Enables the available_monitors command without any pre-configured scope. + +
+ +`core:window:deny-available-monitors` + + + +Denies the available_monitors command without any pre-configured scope. + +
+ +`core:window:allow-center` + + + +Enables the center command without any pre-configured scope. + +
+ +`core:window:deny-center` + + + +Denies the center command without any pre-configured scope. + +
+ +`core:window:allow-close` + + + +Enables the close command without any pre-configured scope. + +
+ +`core:window:deny-close` + + + +Denies the close command without any pre-configured scope. + +
+ +`core:window:allow-create` + + + +Enables the create command without any pre-configured scope. + +
+ +`core:window:deny-create` + + + +Denies the create command without any pre-configured scope. + +
+ +`core:window:allow-current-monitor` + + + +Enables the current_monitor command without any pre-configured scope. + +
+ +`core:window:deny-current-monitor` + + + +Denies the current_monitor command without any pre-configured scope. + +
+ +`core:window:allow-cursor-position` + + + +Enables the cursor_position command without any pre-configured scope. + +
+ +`core:window:deny-cursor-position` + + + +Denies the cursor_position command without any pre-configured scope. + +
+ +`core:window:allow-destroy` + + + +Enables the destroy command without any pre-configured scope. + +
+ +`core:window:deny-destroy` + + + +Denies the destroy command without any pre-configured scope. + +
+ +`core:window:allow-get-all-windows` + + + +Enables the get_all_windows command without any pre-configured scope. + +
+ +`core:window:deny-get-all-windows` + + + +Denies the get_all_windows command without any pre-configured scope. + +
+ +`core:window:allow-hide` + + + +Enables the hide command without any pre-configured scope. + +
+ +`core:window:deny-hide` + + + +Denies the hide command without any pre-configured scope. + +
+ +`core:window:allow-inner-position` + + + +Enables the inner_position command without any pre-configured scope. + +
+ +`core:window:deny-inner-position` + + + +Denies the inner_position command without any pre-configured scope. + +
+ +`core:window:allow-inner-size` + + + +Enables the inner_size command without any pre-configured scope. + +
+ +`core:window:deny-inner-size` + + + +Denies the inner_size command without any pre-configured scope. + +
+ +`core:window:allow-internal-toggle-maximize` + + + +Enables the internal_toggle_maximize command without any pre-configured scope. + +
+ +`core:window:deny-internal-toggle-maximize` + + + +Denies the internal_toggle_maximize command without any pre-configured scope. + +
+ +`core:window:allow-is-always-on-top` + + + +Enables the is_always_on_top command without any pre-configured scope. + +
+ +`core:window:deny-is-always-on-top` + + + +Denies the is_always_on_top command without any pre-configured scope. + +
+ +`core:window:allow-is-closable` + + + +Enables the is_closable command without any pre-configured scope. + +
+ +`core:window:deny-is-closable` + + + +Denies the is_closable command without any pre-configured scope. + +
+ +`core:window:allow-is-decorated` + + + +Enables the is_decorated command without any pre-configured scope. + +
+ +`core:window:deny-is-decorated` + + + +Denies the is_decorated command without any pre-configured scope. + +
+ +`core:window:allow-is-enabled` + + + +Enables the is_enabled command without any pre-configured scope. + +
+ +`core:window:deny-is-enabled` + + + +Denies the is_enabled command without any pre-configured scope. + +
+ +`core:window:allow-is-focused` + + + +Enables the is_focused command without any pre-configured scope. + +
+ +`core:window:deny-is-focused` + + + +Denies the is_focused command without any pre-configured scope. + +
+ +`core:window:allow-is-fullscreen` + + + +Enables the is_fullscreen command without any pre-configured scope. + +
+ +`core:window:deny-is-fullscreen` + + + +Denies the is_fullscreen command without any pre-configured scope. + +
+ +`core:window:allow-is-maximizable` + + + +Enables the is_maximizable command without any pre-configured scope. + +
+ +`core:window:deny-is-maximizable` + + + +Denies the is_maximizable command without any pre-configured scope. + +
+ +`core:window:allow-is-maximized` + + + +Enables the is_maximized command without any pre-configured scope. + +
+ +`core:window:deny-is-maximized` + + + +Denies the is_maximized command without any pre-configured scope. + +
+ +`core:window:allow-is-minimizable` + + + +Enables the is_minimizable command without any pre-configured scope. + +
+ +`core:window:deny-is-minimizable` + + + +Denies the is_minimizable command without any pre-configured scope. + +
+ +`core:window:allow-is-minimized` + + + +Enables the is_minimized command without any pre-configured scope. + +
+ +`core:window:deny-is-minimized` + + + +Denies the is_minimized command without any pre-configured scope. + +
+ +`core:window:allow-is-resizable` + + + +Enables the is_resizable command without any pre-configured scope. + +
+ +`core:window:deny-is-resizable` + + + +Denies the is_resizable command without any pre-configured scope. + +
+ +`core:window:allow-is-visible` + + + +Enables the is_visible command without any pre-configured scope. + +
+ +`core:window:deny-is-visible` + + + +Denies the is_visible command without any pre-configured scope. + +
+ +`core:window:allow-maximize` + + + +Enables the maximize command without any pre-configured scope. + +
+ +`core:window:deny-maximize` + + + +Denies the maximize command without any pre-configured scope. + +
+ +`core:window:allow-minimize` + + + +Enables the minimize command without any pre-configured scope. + +
+ +`core:window:deny-minimize` + + + +Denies the minimize command without any pre-configured scope. + +
+ +`core:window:allow-monitor-from-point` + + + +Enables the monitor_from_point command without any pre-configured scope. + +
+ +`core:window:deny-monitor-from-point` + + + +Denies the monitor_from_point command without any pre-configured scope. + +
+ +`core:window:allow-outer-position` + + + +Enables the outer_position command without any pre-configured scope. + +
+ +`core:window:deny-outer-position` + + + +Denies the outer_position command without any pre-configured scope. + +
+ +`core:window:allow-outer-size` + + + +Enables the outer_size command without any pre-configured scope. + +
+ +`core:window:deny-outer-size` + + + +Denies the outer_size command without any pre-configured scope. + +
+ +`core:window:allow-primary-monitor` + + + +Enables the primary_monitor command without any pre-configured scope. + +
+ +`core:window:deny-primary-monitor` + + + +Denies the primary_monitor command without any pre-configured scope. + +
+ +`core:window:allow-request-user-attention` + + + +Enables the request_user_attention command without any pre-configured scope. + +
+ +`core:window:deny-request-user-attention` + + + +Denies the request_user_attention command without any pre-configured scope. + +
+ +`core:window:allow-scale-factor` + + + +Enables the scale_factor command without any pre-configured scope. + +
+ +`core:window:deny-scale-factor` + + + +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` + + + +Enables the set_always_on_bottom command without any pre-configured scope. + +
+ +`core:window:deny-set-always-on-bottom` + + + +Denies the set_always_on_bottom command without any pre-configured scope. + +
+ +`core:window:allow-set-always-on-top` + + + +Enables the set_always_on_top command without any pre-configured scope. + +
+ +`core:window:deny-set-always-on-top` + + + +Denies the set_always_on_top command without any pre-configured scope. + +
+ +`core:window:allow-set-background-color` + + + +Enables the set_background_color command without any pre-configured scope. + +
+ +`core:window:deny-set-background-color` + + + +Denies the set_background_color command without any pre-configured scope. + +
+ +`core:window:allow-set-badge-count` + + + +Enables the set_badge_count command without any pre-configured scope. + +
+ +`core:window:deny-set-badge-count` + + + +Denies the set_badge_count command without any pre-configured scope. + +
+ +`core:window:allow-set-badge-label` + + + +Enables the set_badge_label command without any pre-configured scope. + +
+ +`core:window:deny-set-badge-label` + + + +Denies the set_badge_label command without any pre-configured scope. + +
+ +`core:window:allow-set-closable` + + + +Enables the set_closable command without any pre-configured scope. + +
+ +`core:window:deny-set-closable` + + + +Denies the set_closable command without any pre-configured scope. + +
+ +`core:window:allow-set-content-protected` + + + +Enables the set_content_protected command without any pre-configured scope. + +
+ +`core:window:deny-set-content-protected` + + + +Denies the set_content_protected command without any pre-configured scope. + +
+ +`core:window:allow-set-cursor-grab` + + + +Enables the set_cursor_grab command without any pre-configured scope. + +
+ +`core:window:deny-set-cursor-grab` + + + +Denies the set_cursor_grab command without any pre-configured scope. + +
+ +`core:window:allow-set-cursor-icon` + + + +Enables the set_cursor_icon command without any pre-configured scope. + +
+ +`core:window:deny-set-cursor-icon` + + + +Denies the set_cursor_icon command without any pre-configured scope. + +
+ +`core:window:allow-set-cursor-position` + + + +Enables the set_cursor_position command without any pre-configured scope. + +
+ +`core:window:deny-set-cursor-position` + + + +Denies the set_cursor_position command without any pre-configured scope. + +
+ +`core:window:allow-set-cursor-visible` + + + +Enables the set_cursor_visible command without any pre-configured scope. + +
+ +`core:window:deny-set-cursor-visible` + + + +Denies the set_cursor_visible command without any pre-configured scope. + +
+ +`core:window:allow-set-decorations` + + + +Enables the set_decorations command without any pre-configured scope. + +
+ +`core:window:deny-set-decorations` + + + +Denies the set_decorations command without any pre-configured scope. + +
+ +`core:window:allow-set-effects` + + + +Enables the set_effects command without any pre-configured scope. + +
+ +`core:window:deny-set-effects` + + + +Denies the set_effects command without any pre-configured scope. + +
+ +`core:window:allow-set-enabled` + + + +Enables the set_enabled command without any pre-configured scope. + +
+ +`core:window:deny-set-enabled` + + + +Denies the set_enabled command without any pre-configured scope. + +
+ +`core:window:allow-set-focus` + + + +Enables the set_focus command without any pre-configured scope. + +
+ +`core:window:deny-set-focus` + + + +Denies the set_focus command without any pre-configured scope. + +
+ +`core:window:allow-set-focusable` + + + +Enables the set_focusable command without any pre-configured scope. + +
+ +`core:window:deny-set-focusable` + + + +Denies the set_focusable command without any pre-configured scope. + +
+ +`core:window:allow-set-fullscreen` + + + +Enables the set_fullscreen command without any pre-configured scope. + +
+ +`core:window:deny-set-fullscreen` + + + +Denies the set_fullscreen command without any pre-configured scope. + +
+ +`core:window:allow-set-fullscreen-on-monitor` + + + +Enables the set_fullscreen_on_monitor command without any pre-configured scope. + +
+ +`core:window:deny-set-fullscreen-on-monitor` + + + +Denies the set_fullscreen_on_monitor command without any pre-configured scope. + +
+ +`core:window:allow-set-icon` + + + +Enables the set_icon command without any pre-configured scope. + +
+ +`core:window:deny-set-icon` + + + +Denies the set_icon command without any pre-configured scope. + +
+ +`core:window:allow-set-ignore-cursor-events` + + + +Enables the set_ignore_cursor_events command without any pre-configured scope. + +
+ +`core:window:deny-set-ignore-cursor-events` + + + +Denies the set_ignore_cursor_events command without any pre-configured scope. + +
+ +`core:window:allow-set-max-size` + + + +Enables the set_max_size command without any pre-configured scope. + +
+ +`core:window:deny-set-max-size` + + + +Denies the set_max_size command without any pre-configured scope. + +
+ +`core:window:allow-set-maximizable` + + + +Enables the set_maximizable command without any pre-configured scope. + +
+ +`core:window:deny-set-maximizable` + + + +Denies the set_maximizable command without any pre-configured scope. + +
+ +`core:window:allow-set-min-size` + + + +Enables the set_min_size command without any pre-configured scope. + +
+ +`core:window:deny-set-min-size` + + + +Denies the set_min_size command without any pre-configured scope. + +
+ +`core:window:allow-set-minimizable` + + + +Enables the set_minimizable command without any pre-configured scope. + +
+ +`core:window:deny-set-minimizable` + + + +Denies the set_minimizable command without any pre-configured scope. + +
+ +`core:window:allow-set-overlay-icon` + + + +Enables the set_overlay_icon command without any pre-configured scope. + +
+ +`core:window:deny-set-overlay-icon` + + + +Denies the set_overlay_icon command without any pre-configured scope. + +
+ +`core:window:allow-set-position` + + + +Enables the set_position command without any pre-configured scope. + +
+ +`core:window:deny-set-position` + + + +Denies the set_position command without any pre-configured scope. + +
+ +`core:window:allow-set-progress-bar` + + + +Enables the set_progress_bar command without any pre-configured scope. + +
+ +`core:window:deny-set-progress-bar` + + + +Denies the set_progress_bar command without any pre-configured scope. + +
+ +`core:window:allow-set-resizable` + + + +Enables the set_resizable command without any pre-configured scope. + +
+ +`core:window:deny-set-resizable` + + + +Denies the set_resizable command without any pre-configured scope. + +
+ +`core:window:allow-set-shadow` + + + +Enables the set_shadow command without any pre-configured scope. + +
+ +`core:window:deny-set-shadow` + + + +Denies the set_shadow command without any pre-configured scope. + +
+ +`core:window:allow-set-simple-fullscreen` + + + +Enables the set_simple_fullscreen command without any pre-configured scope. + +
+ +`core:window:deny-set-simple-fullscreen` + + + +Denies the set_simple_fullscreen command without any pre-configured scope. + +
+ +`core:window:allow-set-size` + + + +Enables the set_size command without any pre-configured scope. + +
+ +`core:window:deny-set-size` + + + +Denies the set_size command without any pre-configured scope. + +
+ +`core:window:allow-set-size-constraints` + + + +Enables the set_size_constraints command without any pre-configured scope. + +
+ +`core:window:deny-set-size-constraints` + + + +Denies the set_size_constraints command without any pre-configured scope. + +
+ +`core:window:allow-set-skip-taskbar` + + + +Enables the set_skip_taskbar command without any pre-configured scope. + +
+ +`core:window:deny-set-skip-taskbar` + + + +Denies the set_skip_taskbar command without any pre-configured scope. + +
+ +`core:window:allow-set-theme` + + + +Enables the set_theme command without any pre-configured scope. + +
+ +`core:window:deny-set-theme` + + + +Denies the set_theme command without any pre-configured scope. + +
+ +`core:window:allow-set-title` + + + +Enables the set_title command without any pre-configured scope. + +
+ +`core:window:deny-set-title` + + + +Denies the set_title command without any pre-configured scope. + +
+ +`core:window:allow-set-title-bar-style` + + + +Enables the set_title_bar_style command without any pre-configured scope. + +
+ +`core:window:deny-set-title-bar-style` + + + +Denies the set_title_bar_style command without any pre-configured scope. + +
+ +`core:window:allow-set-visible-on-all-workspaces` + + + +Enables the set_visible_on_all_workspaces command without any pre-configured scope. + +
+ +`core:window:deny-set-visible-on-all-workspaces` + + + +Denies the set_visible_on_all_workspaces command without any pre-configured scope. + +
+ +`core:window:allow-show` + + + +Enables the show command without any pre-configured scope. + +
+ +`core:window:deny-show` + + + +Denies the show command without any pre-configured scope. + +
+ +`core:window:allow-start-dragging` + + + +Enables the start_dragging command without any pre-configured scope. + +
+ +`core:window:deny-start-dragging` + + + +Denies the start_dragging command without any pre-configured scope. + +
+ +`core:window:allow-start-resize-dragging` + + + +Enables the start_resize_dragging command without any pre-configured scope. + +
+ +`core:window:deny-start-resize-dragging` + + + +Denies the start_resize_dragging command without any pre-configured scope. + +
+ +`core:window:allow-theme` + + + +Enables the theme command without any pre-configured scope. + +
+ +`core:window:deny-theme` + + + +Denies the theme command without any pre-configured scope. + +
+ +`core:window:allow-title` + + + +Enables the title command without any pre-configured scope. + +
+ +`core:window:deny-title` + + + +Denies the title command without any pre-configured scope. + +
+ +`core:window:allow-toggle-maximize` + + + +Enables the toggle_maximize command without any pre-configured scope. + +
+ +`core:window:deny-toggle-maximize` + + + +Denies the toggle_maximize command without any pre-configured scope. + +
+ +`core:window:allow-unmaximize` + + + +Enables the unmaximize command without any pre-configured scope. + +
+ +`core:window:deny-unmaximize` + + + +Denies the unmaximize command without any pre-configured scope. + +
+ +`core:window:allow-unminimize` + + + +Enables the unminimize command without any pre-configured scope. + +
+ +`core:window:deny-unminimize` + + + +Denies the unminimize command without any pre-configured scope. + +
diff --git a/crates/tauri/scripts/bundle.global.js b/crates/tauri/scripts/bundle.global.js new file mode 100644 index 0000000..28f49c4 --- /dev/null +++ b/crates/tauri/scripts/bundle.global.js @@ -0,0 +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})}async[(l=new WeakMap,Symbol.asyncDispose)](){await this.close()}}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:e,width:n,height:t}).then(e=>new y(e))}static async fromBytes(e){return h("plugin:image|from_bytes",{bytes: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 v=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)},exit:async function(e=0){return h("plugin:app|exit",{code:e})},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 f{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 f(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 f?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:f,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 G extends M{constructor(e,n){super(e,n,"Check")}static async new(e){return F("Check",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 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 V extends M{constructor(e,n){super(e,n,"Icon")}static async new(e){return F("Icon",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 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 G(e,n);case"Icon":return new V(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:G,IconMenuItem:V,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 setFullscreenOnMonitor(e){return h("plugin:window|set_fullscreen_on_monitor",{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",{label:this.label,value: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.LiquidGlassRegular="liquidGlassRegular",e.LiquidGlassClear="liquidGlassClear",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:f,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",{label:this.label,value: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,ve=Object.freeze({__proto__:null,Webview:ge,getAllWebviews:_e,getCurrentWebview:we});function fe(){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 fe()}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",{label:this.label,value:e}).then(()=>h("plugin:webview|set_webview_background_color",{label:this.label,value: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:fe});return e.app=v,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=ve,e.webviewWindow=Te,e.window=he,e}({});window.__TAURI__=__TAURI_IIFE__; diff --git a/crates/tauri/scripts/core.js b/crates/tauri/scripts/core.js new file mode 100644 index 0000000..86bafac --- /dev/null +++ b/crates/tauri/scripts/core.js @@ -0,0 +1,114 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +;(function () { + function uid() { + return window.crypto.getRandomValues(new Uint32Array(1))[0] + } + + const osName = __TEMPLATE_os_name__ + const protocolScheme = __TEMPLATE_protocol_scheme__ + + Object.defineProperty(window.__TAURI_INTERNALS__, 'convertFileSrc', { + value: function (filePath, protocol = 'asset') { + const path = encodeURIComponent(filePath) + return osName === 'windows' || osName === 'android' + ? `${protocolScheme}://${protocol}.localhost/${path}` + : `${protocol}://localhost/${path}` + } + }) + + const callbacks = new Map() + + function registerCallback(callback, once) { + const identifier = uid() + callbacks.set(identifier, (data) => { + if (once) { + unregisterCallback(identifier) + } + return callback && callback(data) + }) + return identifier + } + + function unregisterCallback(id) { + callbacks.delete(id) + } + + function runCallback(id, data) { + const callback = callbacks.get(id) + if (callback) { + callback(data) + } else { + console.warn( + `[TAURI] Couldn't find callback id ${id}. This might happen when the app is reloaded while Rust is running an asynchronous operation.` + ) + } + } + + // Maybe let's rename it to `registerCallback`? + Object.defineProperty(window.__TAURI_INTERNALS__, 'transformCallback', { + value: registerCallback + }) + + Object.defineProperty(window.__TAURI_INTERNALS__, 'unregisterCallback', { + value: unregisterCallback + }) + + Object.defineProperty(window.__TAURI_INTERNALS__, 'runCallback', { + value: runCallback + }) + + // This is just for the debugging purposes + Object.defineProperty(window.__TAURI_INTERNALS__, 'callbacks', { + value: callbacks + }) + + const ipcQueue = [] + let isWaitingForIpc = false + + function waitForIpc() { + if ('ipc' in window.__TAURI_INTERNALS__) { + for (const action of ipcQueue) { + action() + } + } else { + setTimeout(waitForIpc, 50) + } + } + + Object.defineProperty(window.__TAURI_INTERNALS__, 'invoke', { + value: function (cmd, payload = {}, options) { + return new Promise(function (resolve, reject) { + const callback = registerCallback((r) => { + resolve(r) + unregisterCallback(error) + }, true) + const error = registerCallback((e) => { + reject(e) + unregisterCallback(callback) + }, true) + + const action = () => { + window.__TAURI_INTERNALS__.ipc({ + cmd, + callback, + error, + payload, + options + }) + } + if ('ipc' in window.__TAURI_INTERNALS__) { + action() + } else { + ipcQueue.push(action) + if (!isWaitingForIpc) { + waitForIpc() + isWaitingForIpc = true + } + } + }) + } + }) +})() diff --git a/crates/tauri/scripts/freeze_prototype.js b/crates/tauri/scripts/freeze_prototype.js new file mode 100644 index 0000000..113ca8c --- /dev/null +++ b/crates/tauri/scripts/freeze_prototype.js @@ -0,0 +1,5 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +Object.freeze(Object.prototype) diff --git a/crates/tauri/scripts/init.js b/crates/tauri/scripts/init.js new file mode 100644 index 0000000..ef6f72f --- /dev/null +++ b/crates/tauri/scripts/init.js @@ -0,0 +1,15 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +;(function () { + __RAW_freeze_prototype__ + + __RAW_pattern_script__ + + __RAW_ipc_script__ + + __RAW_core_script__ + + __RAW_event_initialization_script__ +})() diff --git a/crates/tauri/scripts/ipc-protocol.js b/crates/tauri/scripts/ipc-protocol.js new file mode 100644 index 0000000..9ca2440 --- /dev/null +++ b/crates/tauri/scripts/ipc-protocol.js @@ -0,0 +1,91 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +;(function () { + /** + * A runtime generated key to ensure an IPC call comes from an initialized frame. + * + * This is declared outside the `window.__TAURI_INVOKE__` definition to prevent + * the key from being leaked by `window.__TAURI_INVOKE__.toString()`. + */ + const __TAURI_INVOKE_KEY__ = __TEMPLATE_invoke_key__ + + const processIpcMessage = __RAW_process_ipc_message_fn__ + const osName = __TEMPLATE_os_name__ + const fetchChannelDataCommand = __TEMPLATE_fetch_channel_data_command__ + let customProtocolIpcFailed = false + + // on Android we never use it because Android does not have support to reading the request body + const canUseCustomProtocol = osName !== 'android' + + function sendIpcMessage(message) { + const { cmd, callback, error, payload, options } = message + + if ( + !customProtocolIpcFailed + && (canUseCustomProtocol || cmd === fetchChannelDataCommand) + ) { + const { contentType, data } = processIpcMessage(payload) + + const headers = new Headers((options && options.headers) || {}) + headers.set('Content-Type', contentType) + headers.set('Tauri-Callback', callback) + headers.set('Tauri-Error', error) + headers.set('Tauri-Invoke-Key', __TAURI_INVOKE_KEY__) + + fetch(window.__TAURI_INTERNALS__.convertFileSrc(cmd, 'ipc'), { + method: 'POST', + body: data, + headers + }) + .then((response) => { + const callbackId = + response.headers.get('Tauri-Response') === 'ok' ? callback : error + // we need to split here because on Android the content-type gets duplicated + switch ((response.headers.get('content-type') || '').split(',')[0]) { + case 'application/json': + return response.json().then((r) => [callbackId, r]) + case 'text/plain': + return response.text().then((r) => [callbackId, r]) + default: + return response.arrayBuffer().then((r) => [callbackId, r]) + } + }) + .then( + ([callbackId, data]) => { + window.__TAURI_INTERNALS__.runCallback(callbackId, data) + }, + (e) => { + console.warn( + 'IPC custom protocol failed, Tauri will now use the postMessage interface instead', + e + ) + // failed to use the custom protocol IPC (either the webview blocked a custom protocol or it was a CSP error) + // so we need to fallback to the postMessage interface + customProtocolIpcFailed = true + sendIpcMessage(message) + } + ) + } else { + // otherwise use the postMessage interface + const { data } = processIpcMessage({ + cmd, + callback, + error, + options: { + ...options, + customProtocolIpcBlocked: customProtocolIpcFailed + }, + payload, + __TAURI_INVOKE_KEY__ + }) + // `window.ipc.postMessage` came from `tauri-runtime-wry` > `wry` [`with_ipc_handler`](https://github.com/tauri-apps/wry/blob/a0403b9e2f1ff9d73be7dce1184f058afcaa1d82/src/lib.rs#L1130) + window.ipc.postMessage(data) + } + } + + Object.defineProperty(window.__TAURI_INTERNALS__, 'postMessage', { + value: sendIpcMessage + }) +})() diff --git a/crates/tauri/scripts/ipc.js b/crates/tauri/scripts/ipc.js new file mode 100644 index 0000000..d6aa080 --- /dev/null +++ b/crates/tauri/scripts/ipc.js @@ -0,0 +1,210 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * @typedef {{callback: string, error: string, data: *}} IsolationPayload - a valid isolation payload + */ + +;(function () { + /** + * @type {string} + */ + const pattern = window.__TAURI_INTERNALS__.__TAURI_PATTERN__.pattern + + /** + * @type {string} + */ + const isolationOrigin = __TEMPLATE_isolation_origin__ + + /** + * @type {{queue: object[], ready: boolean, frame: HTMLElement | null}} + */ + const isolation = Object.create(null) + isolation.queue = [] + isolation.ready = false + isolation.frame = null + + /** + * Detects if a message event is a valid isolation message. + * + * @param {MessageEvent} event - a message event that is expected to be an isolation message + * @return {boolean} - if the event was a valid isolation message + */ + function isIsolationMessage(event) { + if ( + typeof event.data === 'object' + && typeof event.data.payload === 'object' + ) { + const keys = Object.keys(event.data.payload || {}) + return ( + keys.length > 0 + && keys.every( + (key) => key === 'contentType' || key === 'nonce' || key === 'payload' + ) + ) + } + return false + } + + /** + * Detects if data is able to transform into an isolation payload. + * + * @param {object} data - object that is expected to contain at least a callback and error identifier + * @return {boolean} - if the data is able to transform into an isolation payload + */ + function isIsolationPayload(data) { + return ( + typeof data === 'object' + && 'callback' in data + && 'error' in data + && !isIsolationMessage(data) + ) + } + + /** + * Sends a properly formatted message to the isolation frame. + * + * @param {IsolationPayload} data - data that has been validated to be an isolation payload + */ + function sendIsolationMessage(data) { + // set the frame dom element if it's not been set before + if (!isolation.frame) { + const frame = document.querySelector('iframe#__tauri_isolation__') + if (frame.src.startsWith(isolationOrigin)) { + isolation.frame = frame + } else { + console.error( + 'Tauri IPC found an isolation iframe, but it had the wrong origin' + ) + } + } + + // ensure we have the target to send the message to + if (!isolation.frame || !isolation.frame.contentWindow) { + console.error( + 'Tauri "Isolation" Pattern could not find the Isolation iframe window' + ) + return + } + + // `postMessage` uses `structuredClone` to serialize the data before sending it + // unlike `JSON.stringify`, we can't extend a type with a method similar to `toJSON` + // so that `structuredClone` would use, so until https://github.com/whatwg/html/issues/7428 + // we manually call `toIPC` + function serializeIpcPayload(data) { + // if this value changes, make sure to update it in: + // 1. process-ipc-message-fn.js + // 2. core.ts + const SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__' + + if ( + typeof data === 'object' + && data !== null + && 'constructor' in data + && data.constructor === Array + ) { + return data.map((v) => serializeIpcPayload(v)) + } + + if ( + typeof data === 'object' + && data !== null + && SERIALIZE_TO_IPC_FN in data + ) { + return data[SERIALIZE_TO_IPC_FN]() + } + + if ( + typeof data === 'object' + && data !== null + && 'constructor' in data + && data.constructor === Object + ) { + const acc = {} + Object.entries(data).forEach(([k, v]) => { + acc[k] = serializeIpcPayload(v) + }) + return acc + } + + return data + } + + data.payload = serializeIpcPayload(data.payload) + + isolation.frame.contentWindow.postMessage( + data, + '*' /* todo: set this to the secure origin */ + ) + } + + Object.defineProperty(window.__TAURI_INTERNALS__, 'ipc', { + // todo: JSDoc this function + value: Object.freeze((message) => { + switch (pattern) { + case 'brownfield': + window.__TAURI_INTERNALS__.postMessage(message) + break + + case 'isolation': + if (!isIsolationPayload(message)) { + console.error( + 'Tauri "Isolation" Pattern found an invalid isolation message payload', + message + ) + break + } + + if (isolation.ready) { + sendIsolationMessage(message) + } else { + isolation.queue.push(message) + } + + break + + case 'error': + console.error( + 'Tauri IPC found a Tauri Pattern, but it was an error. Check for other log messages to find the cause.' + ) + break + + default: + console.error( + 'Tauri IPC did not find a Tauri Pattern that it understood.' + ) + break + } + }) + }) + + /** + * IMPORTANT: See isolation_secure.js for the isolation frame implementation. + * main frame -> isolation frame = isolation payload + * isolation frame -> main frame = isolation message + */ + if (pattern === 'isolation') { + window.addEventListener( + 'message', + (event) => { + // watch for the isolation frame being ready and flush any queued messages + if (event.data === '__TAURI_ISOLATION_READY__') { + isolation.ready = true + + for (const message of isolation.queue) { + sendIsolationMessage(message) + } + + isolation.queue = [] + return + } + + if (isIsolationMessage(event)) { + window.__TAURI_INTERNALS__.postMessage(event.data) + } + }, + false + ) + } +})() diff --git a/crates/tauri/scripts/isolation.js b/crates/tauri/scripts/isolation.js new file mode 100644 index 0000000..19a34c9 --- /dev/null +++ b/crates/tauri/scripts/isolation.js @@ -0,0 +1,17 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +if (location.href !== __TEMPLATE_isolation_src__) { + window.addEventListener('DOMContentLoaded', () => { + let style = document.createElement('style') + style.textContent = __TEMPLATE_style__ + document.head.append(style) + + let iframe = document.createElement('iframe') + iframe.id = '__tauri_isolation__' + iframe.sandbox.add('allow-scripts') + iframe.src = __TEMPLATE_isolation_src__ + document.body.append(iframe) + }) +} diff --git a/crates/tauri/scripts/pattern.js b/crates/tauri/scripts/pattern.js new file mode 100644 index 0000000..dce39b4 --- /dev/null +++ b/crates/tauri/scripts/pattern.js @@ -0,0 +1,21 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +;(function () { + function __tauriDeepFreeze(object) { + const props = Object.getOwnPropertyNames(object) + + for (const prop of props) { + if (typeof object[prop] === 'object') { + __tauriDeepFreeze(object[prop]) + } + } + + return Object.freeze(object) + } + + Object.defineProperty(window.__TAURI_INTERNALS__, '__TAURI_PATTERN__', { + value: __tauriDeepFreeze(__TEMPLATE_pattern__) + }) +})() diff --git a/crates/tauri/scripts/process-ipc-message-fn.js b/crates/tauri/scripts/process-ipc-message-fn.js new file mode 100644 index 0000000..4ee2cb2 --- /dev/null +++ b/crates/tauri/scripts/process-ipc-message-fn.js @@ -0,0 +1,46 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// this is a function and not an iife so use it carefully + +(function (message) { + if ( + message instanceof ArrayBuffer + || ArrayBuffer.isView(message) + || Array.isArray(message) + ) { + return { + contentType: 'application/octet-stream', + data: message + } + } else { + const data = JSON.stringify(message, (_k, val) => { + // if this value changes, make sure to update it in: + // 1. ipc.js + // 2. core.ts + const SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__' + + if (val instanceof Map) { + return Object.fromEntries(val.entries()) + } else if (val instanceof Uint8Array) { + return Array.from(val) + } else if (val instanceof ArrayBuffer) { + return Array.from(new Uint8Array(val)) + } else if ( + typeof val === 'object' + && val !== null + && SERIALIZE_TO_IPC_FN in val + ) { + return val[SERIALIZE_TO_IPC_FN]() + } else { + return val + } + }) + + return { + contentType: 'application/json', + data + } + } +}) diff --git a/crates/tauri/src/app.rs b/crates/tauri/src/app.rs new file mode 100644 index 0000000..e04aaf6 --- /dev/null +++ b/crates/tauri/src/app.rs @@ -0,0 +1,2842 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + Context, DeviceEventFilter, Emitter, EventLoopMessage, EventName, Listener, Manager, Monitor, + Runtime, Scopes, StateManager, Theme, Webview, WebviewWindowBuilder, Window, + image::Image, + ipc::{ + CallbackFn, CommandArg, CommandItem, Invoke, InvokeError, InvokeHandler, InvokeResponseBody, + channel::ChannelDataIpcQueue, + }, + manager::{AppManager, Asset, webview::UriSchemeProtocol}, + plugin::{Plugin, PluginStore}, + resources::ResourceTable, + runtime::{ + ExitRequestedEventAction, RunEvent as RuntimeRunEvent, + window::{WebviewEvent as RuntimeWebviewEvent, WindowEvent as RuntimeWindowEvent}, + }, + sealed::{ManagerBase, RuntimeOrDispatch}, + utils::{Env, config::Config}, + webview::PageLoadPayload, +}; + +#[cfg(desktop)] +use crate::menu::{Menu, MenuEvent}; +#[cfg(all(desktop, feature = "tray-icon"))] +use crate::tray::{TrayIcon, TrayIconBuilder, TrayIconEvent, TrayIconId}; +use raw_window_handle::HasDisplayHandle; +use serialize_to_javascript::{DefaultTemplate, Template, default_template}; +use tauri_macros::default_runtime; +#[cfg(desktop)] +use tauri_runtime::EventLoopProxy; +use tauri_runtime::{ + RuntimeInitArgs, + dpi::{PhysicalPosition, PhysicalSize}, + window::DragDropEvent, +}; +use tauri_utils::{PackageInfo, assets::AssetsIter}; + +use std::{ + borrow::Cow, + collections::HashMap, + fmt, + sync::{Arc, Mutex, MutexGuard, atomic, mpsc::Sender}, + thread::ThreadId, + time::Duration, +}; + +use crate::{Event, EventTarget, event::EventId, runtime::RuntimeHandle}; + +#[cfg(target_os = "macos")] +use crate::ActivationPolicy; + +pub(crate) mod plugin; + +#[cfg(desktop)] +pub(crate) type GlobalMenuEventListener = Box; +#[cfg(all(desktop, feature = "tray-icon"))] +pub(crate) type GlobalTrayIconEventListener = + Box; +pub(crate) type GlobalWindowEventListener = Box, &WindowEvent) + Send + Sync>; +pub(crate) type GlobalWebviewEventListener = + Box, &WebviewEvent) + Send + Sync>; +/// A closure that is run when the Tauri application is setting up. +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>; + +/// The exit code on [`RunEvent::ExitRequested`] when [`AppHandle#method.restart`] is called. +pub const RESTART_EXIT_CODE: i32 = i32::MAX; + +/// Api exposed on the `ExitRequested` event. +#[derive(Debug, Clone)] +pub struct ExitRequestApi { + tx: Sender, + code: Option, +} + +impl ExitRequestApi { + /// Prevents the app from exiting. + /// + /// **Note:** This is ignored when using [`AppHandle#method.restart`]. + pub fn prevent_exit(&self) { + if self.code != Some(RESTART_EXIT_CODE) { + self.tx.send(ExitRequestedEventAction::Prevent).unwrap(); + } + } +} + +/// Api exposed on the `CloseRequested` event. +#[derive(Debug, Clone)] +pub struct CloseRequestApi(Sender); + +impl CloseRequestApi { + /// Prevents the window from being closed. + pub fn prevent_close(&self) { + self.0.send(true).unwrap(); + } +} + +/// An event from a window. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum WindowEvent { + /// The size of the window has changed. Contains the client area's new dimensions. + Resized(PhysicalSize), + /// The position of the window has changed. Contains the window's new position. + Moved(PhysicalPosition), + /// The window has been requested to close. + #[non_exhaustive] + CloseRequested { + /// An API modify the behavior of the close requested event. + api: CloseRequestApi, + }, + /// The window has been destroyed. + Destroyed, + /// The window gained or lost focus. + /// + /// The parameter is true if the window has gained focus, and false if it has lost focus. + Focused(bool), + /// The window's scale factor has changed. + /// + /// The following user actions can cause DPI changes: + /// + /// - Changing the display's resolution. + /// - Changing the display's scale factor (e.g. in Control Panel on Windows). + /// - Moving the window to a display with a different scale factor. + #[non_exhaustive] + ScaleFactorChanged { + /// The new scale factor. + scale_factor: f64, + /// The window inner size. + new_inner_size: PhysicalSize, + }, + /// An event associated with the drag and drop action. + DragDrop(DragDropEvent), + /// The system window theme has changed. Only delivered if the window [`theme`](`crate::window::WindowBuilder#method.theme`) is `None`. + /// + /// Applications might wish to react to this to change the theme of the content of the window when the system changes the window theme. + /// + /// ## Platform-specific + /// + /// - **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 { + fn from(event: RuntimeWindowEvent) -> Self { + match event { + RuntimeWindowEvent::Resized(size) => Self::Resized(size), + RuntimeWindowEvent::Moved(position) => Self::Moved(position), + RuntimeWindowEvent::CloseRequested { signal_tx } => Self::CloseRequested { + api: CloseRequestApi(signal_tx), + }, + RuntimeWindowEvent::Destroyed => Self::Destroyed, + RuntimeWindowEvent::Focused(flag) => Self::Focused(flag), + RuntimeWindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + } => Self::ScaleFactorChanged { + scale_factor, + new_inner_size, + }, + RuntimeWindowEvent::DragDrop(event) => Self::DragDrop(event), + RuntimeWindowEvent::ThemeChanged(theme) => Self::ThemeChanged(theme), + #[cfg(mobile)] + RuntimeWindowEvent::Suspended => Self::Suspended, + #[cfg(mobile)] + RuntimeWindowEvent::Resumed => Self::Resumed, + } + } +} + +/// An event from a window. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum WebviewEvent { + /// An event associated with the drag and drop action. + DragDrop(DragDropEvent), +} + +impl From for WebviewEvent { + fn from(event: RuntimeWebviewEvent) -> Self { + match event { + RuntimeWebviewEvent::DragDrop(e) => Self::DragDrop(e), + } + } +} + +/// An application event, triggered from the event loop. +/// +/// See [`App::run`](crate::App#method.run) for usage examples. +#[derive(Debug)] +#[non_exhaustive] +pub enum RunEvent { + /// Event loop is exiting. + Exit, + /// The app is about to exit + #[non_exhaustive] + ExitRequested { + /// Exit code. + /// [`Option::None`] when the exit is requested by user interaction, + /// [`Option::Some`] when requested programmatically via [`AppHandle#method.exit`] and [`AppHandle#method.restart`]. + code: Option, + /// Event API + api: ExitRequestApi, + }, + /// An event associated with a window. + #[non_exhaustive] + WindowEvent { + /// The window label. + label: String, + /// The detailed event. + event: WindowEvent, + }, + /// An event associated with a webview. + #[non_exhaustive] + WebviewEvent { + /// The window label. + label: String, + /// The detailed event. + event: WebviewEvent, + }, + /// Application ready. + Ready, + /// Sent if the event loop is being resumed. + Resumed, + /// Emitted when all of the event loop's input events have been processed and redraw processing is about to begin. + /// + /// This event is useful as a place to put your code that should be run after all state-changing events have been handled and you want to do stuff (updating state, performing calculations, etc) that happens as the "main body" of your event loop. + MainEventsCleared, + /// Emitted when the user wants to open the specified resource with the app. + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))] + #[cfg_attr( + docsrs, + doc(cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))) + )] + Opened { + /// The URL of the resources that is being open. + urls: Vec, + }, + /// An event from a menu item, could be on the window menu bar, application menu bar (on macOS) or tray icon menu. + #[cfg(desktop)] + #[cfg_attr(docsrs, doc(cfg(desktop)))] + MenuEvent(crate::menu::MenuEvent), + /// An event from a tray icon. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + TrayIconEvent(crate::tray::TrayIconEvent), + /// Emitted when the NSApplicationDelegate's applicationShouldHandleReopen gets called + #[non_exhaustive] + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + Reopen { + /// 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")] + #[cfg_attr(docsrs, doc(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 { + fn from(event: EventLoopMessage) -> Self { + match event { + #[cfg(desktop)] + EventLoopMessage::MenuEvent(e) => Self::MenuEvent(e), + #[cfg(all(desktop, feature = "tray-icon"))] + EventLoopMessage::TrayIconEvent(e) => Self::TrayIconEvent(e), + } + } +} + +/// The asset resolver is a helper to access the [`crate::Assets`] interface. +#[derive(Debug, Clone)] +pub struct AssetResolver { + manager: Arc>, +} + +impl AssetResolver { + /// Gets the app asset associated with the given path. + /// + /// By default it tries to infer your application's URL scheme in production by checking if all webviews + /// were configured with [`crate::webview::WebviewBuilder::use_https_scheme`] or `tauri.conf.json > app > windows > useHttpsScheme`. + /// If you are resolving an asset for a webview with a more dynamic configuration, see [`AssetResolver::get_for_scheme`]. + /// + /// In production, this resolves to the embedded asset bundled in the app executable + /// which contains your frontend assets in [`frontendDist`](https://v2.tauri.app/reference/config/#frontenddist) during build time. + /// + /// In dev mode, if [`devUrl`](https://v2.tauri.app/reference/config/#devurl) is set, we don't bundle the assets to reduce re-builds, + /// and this will fall back to read from `frontendDist` directly. + /// Note that the dist directory must exist so you might need to build your frontend assets first. + pub fn get(&self, path: String) -> Option { + let use_https_scheme = self + .manager + .webviews() + .values() + .all(|webview| webview.use_https_scheme()); + self.get_for_scheme(path, use_https_scheme) + } + + /// Same as [`AssetResolver::get`] but resolves the custom protocol scheme based on a parameter. + /// + /// - `use_https_scheme`: If `true` when using [`Pattern::Isolation`](crate::Pattern::Isolation), + /// the csp header will contain `https://tauri.localhost` instead of `http://tauri.localhost` + pub fn get_for_scheme(&self, path: String, use_https_scheme: bool) -> Option { + #[cfg(dev)] + { + // We don't bundle the assets when in dev mode and `devUrl` is set, so fall back to read from `frontendDist` directly + // TODO: Maybe handle `FrontendDist::Files` as well + if let (Some(_), Some(crate::utils::config::FrontendDist::Directory(dist_path))) = ( + &self.manager.config().build.dev_url, + &self.manager.config().build.frontend_dist, + ) { + let asset_path = std::path::PathBuf::from(&path) + .components() + .filter(|c| !matches!(c, std::path::Component::RootDir)) + .collect::(); + + let asset_path = self + .manager + .config_parent() + .map(|p| p.join(dist_path).join(&asset_path)) + .unwrap_or_else(|| dist_path.join(&asset_path)); + return std::fs::read(asset_path).ok().map(|bytes| { + let mime_type = crate::utils::mime_type::MimeType::parse(&bytes, &path); + Asset { + bytes, + mime_type, + csp_header: None, + } + }); + } + } + + self.manager.get_asset(path, use_https_scheme).ok() + } + + /// Iterate on all assets. + pub fn iter(&self) -> Box> { + self.manager.assets.iter() + } +} + +/// A handle to the currently running application. +/// +/// This type implements [`Manager`] which allows for manipulation of global application items. +#[default_runtime(crate::Wry, wry)] +#[derive(Debug)] +pub struct AppHandle { + pub(crate) runtime_handle: R::Handle, + pub(crate) manager: Arc>, + event_loop: Arc>, +} + +/// Not the real event loop, only contains the main thread id of the event loop +#[derive(Debug)] +struct EventLoop { + main_thread_id: ThreadId, +} + +/// APIs specific to the wry runtime. +#[cfg(feature = "wry")] +impl AppHandle { + /// Create a new tao window using a callback. The event loop must be running at this point. + pub fn create_tao_window< + F: FnOnce() -> (String, tauri_runtime_wry::TaoWindowBuilder) + Send + 'static, + >( + &self, + f: F, + ) -> crate::Result> { + self.runtime_handle.create_tao_window(f).map_err(Into::into) + } + + /// Sends a window message to the event loop. + pub fn send_tao_window_event( + &self, + window_id: tauri_runtime_wry::TaoWindowId, + message: tauri_runtime_wry::WindowMessage, + ) -> crate::Result<()> { + self + .runtime_handle + .send_event(tauri_runtime_wry::Message::Window( + self.runtime_handle.window_id(window_id), + message, + )) + .map_err(Into::into) + } +} + +#[cfg(target_vendor = "apple")] +impl AppHandle { + /// Fetches all Data Store Identifiers by this app + /// + /// Needs to be called from Main Thread + pub async fn fetch_data_store_identifiers(&self) -> crate::Result> { + let (tx, rx) = tokio::sync::oneshot::channel::, tauri_runtime::Error>>(); + let lock: Arc>> = Arc::new(Mutex::new(Some(tx))); + let runtime_handle = self.runtime_handle.clone(); + + self.run_on_main_thread(move || { + let cloned_lock = lock.clone(); + if let Err(err) = runtime_handle.fetch_data_store_identifiers(move |ids| { + if let Some(tx) = cloned_lock.lock().unwrap().take() { + let _ = tx.send(Ok(ids)); + } + }) { + if let Some(tx) = lock.lock().unwrap().take() { + let _ = tx.send(Err(err)); + } + } + })?; + + rx.await?.map_err(Into::into) + } + /// Deletes a Data Store of this app + /// + /// Needs to be called from Main Thread + pub async fn remove_data_store(&self, uuid: [u8; 16]) -> crate::Result<()> { + let (tx, rx) = tokio::sync::oneshot::channel::>(); + let lock: Arc>> = Arc::new(Mutex::new(Some(tx))); + let runtime_handle = self.runtime_handle.clone(); + + self.run_on_main_thread(move || { + let cloned_lock = lock.clone(); + if let Err(err) = runtime_handle.remove_data_store(uuid, move |result| { + if let Some(tx) = cloned_lock.lock().unwrap().take() { + let _ = tx.send(result); + } + }) { + if let Some(tx) = lock.lock().unwrap().take() { + let _ = tx.send(Err(err)); + } + } + })?; + rx.await?.map_err(Into::into) + } +} + +impl Clone for AppHandle { + fn clone(&self) -> Self { + Self { + runtime_handle: self.runtime_handle.clone(), + manager: self.manager.clone(), + event_loop: self.event_loop.clone(), + } + } +} + +impl<'de, R: Runtime> CommandArg<'de, R> for AppHandle { + /// Grabs the [`Window`] from the [`CommandItem`] and returns the associated [`AppHandle`]. This will never fail. + fn from_command(command: CommandItem<'de, R>) -> std::result::Result { + Ok(command.message.webview().app_handle) + } +} + +impl AppHandle { + /// Runs the given closure on the main thread. + pub fn run_on_main_thread(&self, f: F) -> crate::Result<()> { + self + .runtime_handle + .run_on_main_thread(f) + .map_err(Into::into) + } + + /// Adds a Tauri application plugin. + /// This function can be used to register a plugin that is loaded dynamically e.g. after login. + /// For plugins that are created when the app is started, prefer [`Builder::plugin`]. + /// + /// See [`Builder::plugin`] for more information. + /// + /// # Examples + /// + /// ``` + /// use tauri::{plugin::{Builder as PluginBuilder, TauriPlugin}, Runtime}; + /// + /// fn init_plugin() -> TauriPlugin { + /// PluginBuilder::new("dummy").build() + /// } + /// + /// tauri::Builder::default() + /// .setup(move |app| { + /// let handle = app.handle().clone(); + /// std::thread::spawn(move || { + /// handle.plugin(init_plugin()); + /// }); + /// + /// Ok(()) + /// }); + /// ``` + pub fn plugin + 'static>(&self, plugin: P) -> crate::Result<()> { + self.plugin_boxed(Box::new(plugin)) + } + + /// Adds a Tauri application plugin. + /// + /// This method is similar to [`Self::plugin`], + /// but accepts a boxed trait object instead of a generic type. + #[cfg_attr(feature = "tracing", tracing::instrument(name = "app::plugin::register", skip(plugin), fields(name = plugin.name())))] + pub fn plugin_boxed(&self, mut plugin: Box>) -> crate::Result<()> { + let mut store = self.manager().plugins.lock().unwrap(); + store.initialize(&mut plugin, self, &self.config().plugins)?; + store.register(plugin); + + Ok(()) + } + + /// Removes the plugin with the given name. + /// + /// # Examples + /// + /// ``` + /// use tauri::{plugin::{Builder as PluginBuilder, TauriPlugin, Plugin}, Runtime}; + /// + /// fn init_plugin() -> TauriPlugin { + /// PluginBuilder::new("dummy").build() + /// } + /// + /// let plugin = init_plugin(); + /// // `.name()` requires the `Plugin` trait import + /// let plugin_name = plugin.name(); + /// tauri::Builder::default() + /// .plugin(plugin) + /// .setup(move |app| { + /// let handle = app.handle().clone(); + /// std::thread::spawn(move || { + /// handle.remove_plugin(plugin_name); + /// }); + /// + /// Ok(()) + /// }); + /// ``` + pub fn remove_plugin(&self, plugin: &str) -> bool { + self.manager().plugins.lock().unwrap().unregister(plugin) + } + + /// Exits the app by triggering [`RunEvent::ExitRequested`] and [`RunEvent::Exit`]. + /// + /// ## Platform-specific + /// + /// - **Android**: Unless the exit is prevented, the activity is finished instead of the process being exited directly, + /// so the app closes with the system transition and the activity lifecycle callbacks run. + /// [`RunEvent::Exit`] is triggered once the activity is destroyed, and `exit_code` is not used as the process exit code. + pub fn exit(&self, exit_code: i32) { + if let Err(e) = self.runtime_handle.request_exit(exit_code) { + log::error!("failed to exit: {}", e); + self.cleanup_before_exit(); + std::process::exit(exit_code); + } + } + + /// Finishes the Android activity, and any other activity of its task, so the app closes gracefully. + #[cfg(target_os = "android")] + pub(crate) fn finish_activity(&self) -> crate::Result<()> { + // the app plugin is a core plugin, so its Android handle is always managed + self + .state::>() + .0 + .run_mobile_plugin::<()>("exit", ())?; + Ok(()) + } + + /// Restarts the app by triggering [`RunEvent::ExitRequested`] with code [`RESTART_EXIT_CODE`](crate::RESTART_EXIT_CODE) and [`RunEvent::Exit`]. + /// + /// When this function is called on the main thread, we cannot guarantee the delivery of those events, + /// so we skip them and directly restart the process. + /// + /// If you want to trigger them reliably, use [`Self::request_restart`] instead + pub fn restart(&self) -> ! { + if self.event_loop.lock().unwrap().main_thread_id == std::thread::current().id() { + log::debug!("restart triggered on the main thread"); + self.cleanup_before_exit(); + crate::process::restart(&self.env()); + } else { + log::debug!("restart triggered from a separate thread"); + // we're running on a separate thread, so we must trigger the exit request and wait for it to finish + self + .manager + .restart_on_exit + .store(true, atomic::Ordering::Relaxed); + // We'll be restarting when we receive the next `RuntimeRunEvent::Exit` event in `App::run` if this call succeed + match self.runtime_handle.request_exit(RESTART_EXIT_CODE) { + Ok(()) => loop { + std::thread::sleep(Duration::MAX); + }, + Err(e) => { + log::error!("failed to request exit: {e}"); + self.cleanup_before_exit(); + crate::process::restart(&self.env()); + } + } + } + } + + /// Restarts the app by triggering [`RunEvent::ExitRequested`] with code [`RESTART_EXIT_CODE`] and [`RunEvent::Exit`]. + pub fn request_restart(&self) { + self + .manager + .restart_on_exit + .store(true, atomic::Ordering::Relaxed); + // We'll be restarting when we receive the next `RuntimeRunEvent::Exit` event in `App::run` if this call succeed + if self.runtime_handle.request_exit(RESTART_EXIT_CODE).is_err() { + self.cleanup_before_exit(); + crate::process::restart(&self.env()); + } + } + + /// Sets the activation policy for the application. It is set to `NSApplicationActivationPolicyRegular` by default. + /// + /// # Examples + /// ```,no_run + /// tauri::Builder::default() + /// .setup(move |app| { + /// #[cfg(target_os = "macos")] + /// app.handle().set_activation_policy(tauri::ActivationPolicy::Accessory); + /// Ok(()) + /// }); + /// ``` + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub fn set_activation_policy(&self, activation_policy: ActivationPolicy) -> crate::Result<()> { + self + .runtime_handle + .set_activation_policy(activation_policy) + .map_err(Into::into) + } + + /// Sets the dock visibility for the application. + /// + /// # Examples + /// ```,no_run + /// tauri::Builder::default() + /// .setup(move |app| { + /// #[cfg(target_os = "macos")] + /// app.handle().set_dock_visibility(false); + /// Ok(()) + /// }); + /// ``` + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub fn set_dock_visibility(&self, visible: bool) -> crate::Result<()> { + self + .runtime_handle + .set_dock_visibility(visible) + .map_err(Into::into) + } + + /// Change the device event filter mode. + /// + /// See [App::set_device_event_filter] for details. + /// + /// ## Platform-specific + /// + /// See [App::set_device_event_filter] for details. + 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(); + let _ = self.run_on_main_thread(move || { + 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 { + fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + self.manager.resources_table() + } +} + +impl ManagerBase for AppHandle { + fn manager(&self) -> &AppManager { + &self.manager + } + + fn manager_owned(&self) -> Arc> { + self.manager.clone() + } + + fn runtime(&self) -> RuntimeOrDispatch<'_, R> { + RuntimeOrDispatch::RuntimeHandle(self.runtime_handle.clone()) + } + + 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. +/// +/// This type implements [`Manager`] which allows for manipulation of global application items. +#[default_runtime(crate::Wry, wry)] +pub struct App { + runtime: Option, + setup: Option>, + manager: Arc>, + handle: AppHandle, + ran_setup: bool, +} + +impl fmt::Debug for App { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("App") + .field("runtime", &self.runtime) + .field("manager", &self.manager) + .field("handle", &self.handle) + .finish() + } +} + +impl Manager for App { + fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + self.manager.resources_table() + } +} + +impl ManagerBase for App { + fn manager(&self) -> &AppManager { + &self.manager + } + + fn manager_owned(&self) -> Arc> { + self.manager.clone() + } + + fn runtime(&self) -> RuntimeOrDispatch<'_, R> { + if let Some(runtime) = self.runtime.as_ref() { + RuntimeOrDispatch::Runtime(runtime) + } else { + self.handle.runtime() + } + } + + 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. +#[cfg(feature = "wry")] +impl App { + /// Adds a [`tauri_runtime_wry::Plugin`] using its [`tauri_runtime_wry::PluginBuilder`]. + /// + /// # Stability + /// + /// This API is unstable. + pub fn wry_plugin + Send + 'static>( + &mut self, + plugin: P, + ) where +

>::Plugin: Send, + { + self.handle.runtime_handle.plugin(plugin); + } +} + +macro_rules! shared_app_impl { + ($app: ty) => { + impl $app { + /// Registers a global menu event listener. + #[cfg(desktop)] + pub fn on_menu_event, MenuEvent) + Send + Sync + 'static>( + &self, + handler: F, + ) { + self.manager.menu.on_menu_event(handler) + } + + /// Registers a global tray icon menu event listener. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + pub fn on_tray_icon_event, TrayIconEvent) + Send + Sync + 'static>( + &self, + handler: F, + ) { + self.manager.tray.on_tray_icon_event(handler) + } + + /// Gets a tray icon using the provided id. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + pub fn tray_by_id<'a, I>(&self, id: &'a I) -> Option> + where + I: ?Sized, + TrayIconId: PartialEq<&'a I>, + { + self.manager.tray.tray_by_id(self.app_handle(), id) + } + + /// Removes a tray icon using the provided id from tauri's internal state and returns it. + /// + /// Note that dropping the returned icon, may cause the tray icon to disappear + /// if it wasn't cloned somewhere else or referenced by JS. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + pub fn remove_tray_by_id<'a, I>(&self, id: &'a I) -> Option> + where + I: ?Sized, + TrayIconId: PartialEq<&'a I>, + { + self.manager.tray.remove_tray_by_id(self.app_handle(), id) + } + + /// Gets the app's configuration, defined on the `tauri.conf.json` file. + pub fn config(&self) -> &Config { + self.manager.config() + } + + /// Gets the app's package information. + pub fn package_info(&self) -> &PackageInfo { + self.manager.package_info() + } + + /// The application's asset resolver. + pub fn asset_resolver(&self) -> AssetResolver { + AssetResolver { + manager: self.manager.clone(), + } + } + + /// Returns the primary monitor of the system. + /// + /// Returns None if it can't identify any monitor as a primary one. + pub fn primary_monitor(&self) -> crate::Result> { + Ok(match self.runtime() { + RuntimeOrDispatch::Runtime(h) => h.primary_monitor().map(Into::into), + RuntimeOrDispatch::RuntimeHandle(h) => h.primary_monitor()?.map(Into::into), + _ => unreachable!(), + }) + } + + /// Returns the monitor that contains the given point. + pub fn monitor_from_point(&self, x: f64, y: f64) -> crate::Result> { + Ok(match self.runtime() { + RuntimeOrDispatch::Runtime(h) => h.monitor_from_point(x, y).map(Into::into), + RuntimeOrDispatch::RuntimeHandle(h) => h.monitor_from_point(x, y)?.map(Into::into), + _ => unreachable!(), + }) + } + + /// Returns the list of all the monitors available on the system. + pub fn available_monitors(&self) -> crate::Result> { + Ok(match self.runtime() { + RuntimeOrDispatch::Runtime(h) => { + h.available_monitors().into_iter().map(Into::into).collect() + } + RuntimeOrDispatch::RuntimeHandle(h) => h + .available_monitors()? + .into_iter() + .map(Into::into) + .collect(), + _ => unreachable!(), + }) + } + + /// Get the cursor position relative to the top-left hand corner of the desktop. + /// + /// Note that the top-left hand corner of the desktop is not necessarily the same as the screen. + /// If the user uses a desktop with multiple monitors, + /// the top-left hand corner of the desktop is the top-left hand corner of the main monitor on Windows and macOS + /// or the top-left of the leftmost monitor on X11. + /// + /// The coordinates can be negative if the top-left hand corner of the window is outside of the visible screen region. + pub fn cursor_position(&self) -> crate::Result> { + Ok(match self.runtime() { + RuntimeOrDispatch::Runtime(h) => h.cursor_position()?, + RuntimeOrDispatch::RuntimeHandle(h) => h.cursor_position()?, + _ => unreachable!(), + }) + } + + /// Sets the app theme. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + pub fn set_theme(&self, theme: Option) { + #[cfg(windows)] + for window in self.manager.windows().values() { + if let (Some(menu), Ok(hwnd)) = (window.menu(), window.hwnd()) { + let raw_hwnd = hwnd.0 as isize; + let _ = 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), + ) + }; + }); + }; + } + match self.runtime() { + RuntimeOrDispatch::Runtime(h) => h.set_theme(theme), + RuntimeOrDispatch::RuntimeHandle(h) => h.set_theme(theme), + _ => unreachable!(), + } + } + + /// Returns the default window icon. + pub fn default_window_icon(&self) -> Option<&Image<'_>> { + self.manager.window.default_icon.as_ref() + } + + /// Returns the app-wide menu. + #[cfg(desktop)] + pub fn menu(&self) -> Option> { + self.manager.menu.menu_lock().clone() + } + + /// Sets the app-wide menu and returns the previous one. + /// + /// If a window was not created with an explicit menu or had one set explicitly, + /// this menu will be assigned to it. + #[cfg(desktop)] + pub fn set_menu(&self, menu: Menu) -> crate::Result>> { + let prev_menu = self.remove_menu()?; + + self.manager.menu.insert_menu_into_stash(&menu); + + self.manager.menu.menu_lock().replace(menu.clone()); + + // set it on all windows that don't have one or previously had the app-wide menu + #[cfg(not(target_os = "macos"))] + { + for window in self.manager.windows().values() { + let has_app_wide_menu = window.has_app_wide_menu() || window.menu().is_none(); + if has_app_wide_menu { + window.set_menu(menu.clone())?; + window.menu_lock().replace(crate::window::WindowMenu { + is_app_wide: true, + menu: menu.clone(), + }); + } + } + } + + // set it app-wide for macos + #[cfg(target_os = "macos")] + { + let menu_ = menu.clone(); + self.run_on_main_thread(move || { + let _ = init_app_menu(&menu_); + })?; + } + + Ok(prev_menu) + } + + /// Remove the app-wide menu and returns it. + /// + /// If a window was not created with an explicit menu or had one set explicitly, + /// this will remove the menu from it. + #[cfg(desktop)] + pub fn remove_menu(&self) -> crate::Result>> { + let menu = self.manager.menu.menu_lock().as_ref().cloned(); + #[allow(unused_variables)] + if let Some(menu) = menu { + // remove from windows that have the app-wide menu + #[cfg(not(target_os = "macos"))] + { + for window in self.manager.windows().values() { + let has_app_wide_menu = window.has_app_wide_menu(); + if has_app_wide_menu { + window.remove_menu()?; + *window.menu_lock() = None; + } + } + } + + // remove app-wide for macos + #[cfg(target_os = "macos")] + { + self.run_on_main_thread(move || { + menu.inner().remove_for_nsapp(); + })?; + } + } + + let prev_menu = self.manager.menu.menu_lock().take(); + + self + .manager + .remove_menu_from_stash_by_id(prev_menu.as_ref().map(|m| m.id())); + + Ok(prev_menu) + } + + /// Hides the app-wide menu from windows that have it. + /// + /// If a window was not created with an explicit menu or had one set explicitly, + /// this will hide the menu from it. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + #[cfg(desktop)] + pub fn hide_menu(&self) -> crate::Result<()> { + #[cfg(not(target_os = "macos"))] + { + let is_app_menu_set = self.manager.menu.menu_lock().is_some(); + if is_app_menu_set { + for window in self.manager.windows().values() { + if window.has_app_wide_menu() { + window.hide_menu()?; + } + } + } + } + + Ok(()) + } + + /// Shows the app-wide menu for windows that have it. + /// + /// If a window was not created with an explicit menu or had one set explicitly, + /// this will show the menu for it. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + #[cfg(desktop)] + pub fn show_menu(&self) -> crate::Result<()> { + #[cfg(not(target_os = "macos"))] + { + let is_app_menu_set = self.manager.menu.menu_lock().is_some(); + if is_app_menu_set { + for window in self.manager.windows().values() { + if window.has_app_wide_menu() { + window.show_menu()?; + } + } + } + } + + Ok(()) + } + + /// Shows the application, but does not automatically focus it. + #[cfg(target_os = "macos")] + pub fn show(&self) -> crate::Result<()> { + match self.runtime() { + RuntimeOrDispatch::Runtime(r) => r.show(), + RuntimeOrDispatch::RuntimeHandle(h) => h.show()?, + _ => unreachable!(), + } + Ok(()) + } + + /// Hides the application. + #[cfg(target_os = "macos")] + pub fn hide(&self) -> crate::Result<()> { + match self.runtime() { + RuntimeOrDispatch::Runtime(r) => r.hide(), + RuntimeOrDispatch::RuntimeHandle(h) => h.hide()?, + _ => unreachable!(), + } + Ok(()) + } + + /// Runs necessary cleanup tasks before exiting the process. + /// **You should always exit the tauri app immediately after this function returns and not use any tauri-related APIs.** + pub fn cleanup_before_exit(&self) { + // run plugin cleanup hooks first so plugins can still use the app resources (e.g. stop sidecars) + // cleanup is best-effort, so a plugin store poisoned by an earlier panic must not abort it + self + .manager + .plugins + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .cleanup_before_exit(self.app_handle()); + + #[cfg(all(desktop, feature = "tray-icon"))] + self.manager.tray.icons.lock().unwrap().clear(); + self.manager.resources_table().clear(); + for (_, window) in self.manager.windows() { + window.resources_table().clear(); + #[cfg(windows)] + let _ = window.hide(); + } + for (_, webview) in self.manager.webviews() { + webview.resources_table().clear(); + } + } + + /// Gets the invoke key that must be referenced when using [`crate::webview::InvokeRequest`]. + /// + /// # Security + /// + /// DO NOT expose this key to third party scripts as might grant access to the backend from external URLs and iframes. + 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 { + /// Listen to an event on this app. + /// + /// # Examples + /// + /// ``` + /// use tauri::Listener; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// app.listen("component-loaded", move |event| { + /// println!("window just loaded a component"); + /// }); + /// + /// Ok(()) + /// }); + /// ``` + fn listen(&self, event: impl Into, handler: F) -> EventId + where + F: Fn(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager.listen(event, EventTarget::App, handler) + } + + /// Listen to an event on this app only once. + /// + /// See [`Self::listen`] for more information. + fn once(&self, event: impl Into, handler: F) -> EventId + where + F: FnOnce(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager.once(event, EventTarget::App, handler) + } + + /// Unlisten to an event on this app. + /// + /// # Examples + /// + /// ``` + /// use tauri::Listener; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let handler = app.listen("component-loaded", move |event| { + /// println!("app just loaded a component"); + /// }); + /// + /// // stop listening to the event when you do not need it anymore + /// app.unlisten(handler); + /// + /// Ok(()) + /// }); + /// ``` + fn unlisten(&self, id: EventId) { + self.manager.unlisten(id) + } + } + + impl Emitter for $app {} + }; +} + +shared_app_impl!(App); +shared_app_impl!(AppHandle); + +impl App { + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "app::core_plugins::register") + )] + fn register_core_plugins(&self) -> crate::Result<()> { + self.handle.plugin(crate::path::plugin::init())?; + self.handle.plugin(crate::event::plugin::init(self))?; + self.handle.plugin(crate::window::plugin::init())?; + self.handle.plugin(crate::webview::plugin::init())?; + self.handle.plugin(crate::app::plugin::init())?; + self.handle.plugin(crate::resources::plugin::init())?; + self.handle.plugin(crate::image::plugin::init())?; + #[cfg(desktop)] + self.handle.plugin(crate::menu::plugin::init())?; + #[cfg(all(desktop, feature = "tray-icon"))] + self.handle.plugin(crate::tray::plugin::init())?; + Ok(()) + } + + /// Runs the given closure on the main thread. + pub fn run_on_main_thread(&self, f: F) -> crate::Result<()> { + self.app_handle().run_on_main_thread(f) + } + + /// Gets a handle to the application instance. + pub fn handle(&self) -> &AppHandle { + &self.handle + } + + /// Whether the application supports multiple windows. + #[cfg(target_os = "ios")] + pub fn supports_multiple_windows(&self) -> bool { + 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 + /// ```,no_run + /// tauri::Builder::default() + /// .setup(move |app| { + /// #[cfg(target_os = "macos")] + /// app.set_activation_policy(tauri::ActivationPolicy::Accessory); + /// Ok(()) + /// }); + /// ``` + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub fn set_activation_policy(&mut self, activation_policy: ActivationPolicy) { + if let Some(runtime) = self.runtime.as_mut() { + runtime.set_activation_policy(activation_policy); + } else { + let _ = self.app_handle().set_activation_policy(activation_policy); + } + } + + /// Sets the dock visibility for the application. + /// + /// # Examples + /// ```,no_run + /// tauri::Builder::default() + /// .setup(move |app| { + /// #[cfg(target_os = "macos")] + /// app.set_dock_visibility(false); + /// Ok(()) + /// }); + /// ``` + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub fn set_dock_visibility(&mut self, visible: bool) { + if let Some(runtime) = self.runtime.as_mut() { + runtime.set_dock_visibility(visible); + } else { + let _ = self.app_handle().set_dock_visibility(visible); + } + } + + /// Change the device event filter mode. + /// + /// Since the DeviceEvent capture can lead to high CPU usage for unfocused windows, [`tao`] + /// will ignore them by default for unfocused windows on Windows. This method allows changing + /// the filter to explicitly capture them again. + /// + /// ## Platform-specific + /// + /// - ** Linux / macOS / iOS / Android**: Unsupported. + /// + /// # Examples + /// ```,no_run + /// let mut app = tauri::Builder::default() + /// // on an actual app, remove the string argument + /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while building tauri application"); + /// app.set_device_event_filter(tauri::DeviceEventFilter::Always); + /// app.run(|_app_handle, _event| {}); + /// ``` + /// + /// [`tao`]: https://crates.io/crates/tao + pub fn set_device_event_filter(&mut self, filter: DeviceEventFilter) { + self + .runtime + .as_mut() + .unwrap() + .set_device_event_filter(filter); + } + + /// Runs the application. + /// + /// This function never returns. When the application finishes, the process is exited directly using [`std::process::exit`]. + /// See [`run_return`](Self::run_return) if you need to run code after the application event loop exits. + /// + /// # Panics + /// + /// This function will panic if the setup-function supplied in [`Builder::setup`] fails. + /// + /// # Examples + /// ```,no_run + /// let app = tauri::Builder::default() + /// // on an actual app, remove the string argument + /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while building tauri application"); + /// app.run(|_app_handle, event| match event { + /// tauri::RunEvent::ExitRequested { api, .. } => { + /// api.prevent_exit(); + /// } + /// _ => {} + /// }); + /// ``` + pub fn run, RunEvent) + 'static>(mut self, callback: F) { + self.handle.event_loop.lock().unwrap().main_thread_id = std::thread::current().id(); + + self + .runtime + .take() + .unwrap() + .run(self.make_run_event_loop_callback(callback)); + } + + /// Runs the application, returning its intended exit code. + /// + /// Note when using [`AppHandle::restart`] and [`AppHandle::request_restart`], + /// this function will handle the restart request, exit and restart the app without returning + /// + /// ## Platform-specific + /// + /// - **iOS**: Unsupported. The application will fallback to [`run`](Self::run). + /// + /// # Panics + /// + /// This function will panic if the setup-function supplied in [`Builder::setup`] fails. + /// + /// # Examples + /// ```,no_run + /// let app = tauri::Builder::default() + /// // on an actual app, remove the string argument + /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while building tauri application"); + /// let exit_code = app + /// .run_return(|_app_handle, event| match event { + /// tauri::RunEvent::ExitRequested { api, .. } => { + /// api.prevent_exit(); + /// } + /// _ => {} + /// }); + /// + /// std::process::exit(exit_code); + /// ``` + pub fn run_return, RunEvent) + 'static>(mut self, callback: F) -> i32 { + self.handle.event_loop.lock().unwrap().main_thread_id = std::thread::current().id(); + + self + .runtime + .take() + .unwrap() + .run_return(self.make_run_event_loop_callback(callback)) + } + + fn make_run_event_loop_callback, RunEvent) + 'static>( + mut self, + mut callback: F, + ) -> impl FnMut(RuntimeRunEvent) { + // whether the activity is being finished to fulfill an exit request the app accepted + #[cfg(target_os = "android")] + let mut finishing_activity = false; + + move |event| match event { + RuntimeRunEvent::Ready => { + if let Err(e) = setup(&mut self) { + panic!("Failed to setup app: {e}"); + } + let event = on_event_loop_event(self.handle(), RuntimeRunEvent::Ready, self.manager()); + callback(self.handle(), event); + } + RuntimeRunEvent::Exit => { + let event = on_event_loop_event(self.handle(), RuntimeRunEvent::Exit, self.manager()); + callback(self.handle(), event); + self.cleanup_before_exit(); + if self.manager.restart_on_exit.load(atomic::Ordering::Relaxed) { + crate::process::restart(&self.env()); + } + } + // On Android, exiting the process while the activity is still on screen skips the close transition + // and flashes a blank screen, so a programmatic exit finishes the activity instead and the event loop + // exits through the regular window destroyed path. + #[cfg(target_os = "android")] + RuntimeRunEvent::ExitRequested { + code: Some(code), + tx, + } if code != RESTART_EXIT_CODE && !self.manager.windows().is_empty() => { + let (app_tx, app_rx) = std::sync::mpsc::channel(); + let event = on_event_loop_event( + self.handle(), + RuntimeRunEvent::ExitRequested { + code: Some(code), + tx: app_tx, + }, + self.manager(), + ); + callback(self.handle(), event); + + if matches!(app_rx.try_recv(), Ok(ExitRequestedEventAction::Prevent)) { + let _ = tx.send(ExitRequestedEventAction::Prevent); + } else { + match self.handle().finish_activity() { + Ok(()) => { + finishing_activity = true; + // keep the event loop running until the activity is destroyed + let _ = tx.send(ExitRequestedEventAction::Prevent); + } + // fall back to exiting the process directly + Err(e) => log::error!("failed to finish the activity: {e}"), + } + } + } + #[cfg(target_os = "android")] + RuntimeRunEvent::ExitRequested { code: None, .. } if finishing_activity => { + // the app already accepted this exit when it was requested + } + _ => { + let event = on_event_loop_event(self.handle(), event, self.manager()); + callback(self.handle(), event); + } + } + } + + /// Runs an iteration of the runtime event loop and immediately return. + /// + /// Note that when using this API, app cleanup is not automatically done. + /// The cleanup calls [`App::cleanup_before_exit`] so you may want to call that function before exiting the application. + /// + /// # Examples + /// ```no_run + /// use tauri::Manager; + /// + /// let mut app = tauri::Builder::default() + /// // on an actual app, remove the string argument + /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while building tauri application"); + /// + /// loop { + /// app.run_iteration(|_app, _event| {}); + /// if app.webview_windows().is_empty() { + /// app.cleanup_before_exit(); + /// break; + /// } + /// } + /// ``` + #[cfg(desktop)] + #[deprecated( + note = "When called in a loop (as suggested by the name), this function will busy-loop. To re-gain control of control flow after the app has exited, use `App::run_return` instead." + )] + pub fn run_iteration, RunEvent) + 'static>(&mut self, mut callback: F) { + if !self.ran_setup { + if let Err(e) = setup(self) { + panic!("Failed to setup app: {e}"); + } + } + + let app_handle = self.handle().clone(); + + app_handle.event_loop.lock().unwrap().main_thread_id = std::thread::current().id(); + + self.runtime.as_mut().unwrap().run_iteration(move |event| { + let event = on_event_loop_event(&app_handle, event, app_handle.manager()); + callback(&app_handle, event); + }) + } +} + +/// Builds a Tauri application. +/// +/// # Examples +/// ```,no_run +/// tauri::Builder::default() +/// // on an actual app, remove the string argument +/// .run(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) +/// .expect("error while running tauri application"); +/// ``` +#[allow(clippy::type_complexity)] +pub struct Builder { + /// A flag indicating that the runtime must be started on an environment that supports the event loop not on the main thread. + #[cfg(any(windows, target_os = "linux"))] + runtime_any_thread: bool, + + /// The JS message handler. + invoke_handler: Box>, + + /// The script that initializes the `window.__TAURI_INTERNALS__.postMessage` function. + pub(crate) invoke_initialization_script: String, + + channel_interceptor: Option>, + + /// The setup hook. + setup: SetupHook, + + /// Page load hook. + on_page_load: Option>>, + + /// Permission request hook. + on_permission_request: 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, + + /// The webview protocols available to all windows. + uri_scheme_protocols: HashMap>>, + + /// App state. + state: StateManager, + + /// A closure that returns the menu set to all windows. + #[cfg(desktop)] + menu: Option) -> crate::Result> + Send>>, + + /// Menu event listeners for any menu event. + #[cfg(desktop)] + menu_event_listeners: Vec>>, + + /// Tray event listeners for any tray icon event. + #[cfg(all(desktop, feature = "tray-icon"))] + tray_icon_event_listeners: Vec>>, + + /// Enable macOS default menu creation. + #[allow(unused)] + enable_macos_default_menu: bool, + + /// Whether to activate the application when launched while another application is active. + #[cfg(target_os = "macos")] + activate_ignoring_other_apps: bool, + + /// Window event handlers that listens to all windows. + window_event_listeners: Vec>, + + /// Webview event handlers that listens to all webviews. + webview_event_listeners: Vec>, + + /// The device event filter. + device_event_filter: DeviceEventFilter, + + pub(crate) invoke_key: String, +} + +#[derive(Template)] +#[default_template("../scripts/ipc-protocol.js")] +pub(crate) struct InvokeInitializationScript<'a> { + /// The function that processes the IPC message. + #[raw] + pub(crate) process_ipc_message_fn: &'a str, + pub(crate) os_name: &'a str, + pub(crate) fetch_channel_data_command: &'a str, + pub(crate) invoke_key: &'a str, +} + +/// Make `Wry` the default `Runtime` for `Builder` +#[cfg(feature = "wry")] +#[cfg_attr(docsrs, doc(cfg(feature = "wry")))] +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(not(feature = "wry"))] +#[cfg_attr(docsrs, doc(cfg(not(feature = "wry"))))] +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} + +impl Builder { + /// Creates a new App builder. + pub fn new() -> Self { + let invoke_key = crate::generate_invoke_key().unwrap(); + + Self { + #[cfg(any(windows, target_os = "linux"))] + runtime_any_thread: false, + setup: Box::new(|_| Ok(())), + invoke_handler: Box::new(|_| false), + invoke_initialization_script: InvokeInitializationScript { + process_ipc_message_fn: crate::manager::webview::PROCESS_IPC_MESSAGE_FN, + os_name: std::env::consts::OS, + fetch_channel_data_command: crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND, + invoke_key: &invoke_key.clone(), + } + .render_default(&Default::default()) + .unwrap() + .into_string(), + channel_interceptor: None, + on_page_load: None, + on_permission_request: 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(), + #[cfg(desktop)] + menu: None, + #[cfg(desktop)] + menu_event_listeners: Vec::new(), + #[cfg(all(desktop, feature = "tray-icon"))] + tray_icon_event_listeners: Vec::new(), + enable_macos_default_menu: true, + #[cfg(target_os = "macos")] + activate_ignoring_other_apps: true, + window_event_listeners: Vec::new(), + webview_event_listeners: Vec::new(), + device_event_filter: Default::default(), + invoke_key, + } + } +} + +impl Builder { + /// Builds a new Tauri application running on any thread, bypassing the main thread requirement. + /// + /// ## Platform-specific + /// + /// - **macOS:** on macOS the application *must* be executed on the main thread, so this function is not exposed. + #[cfg(any(windows, target_os = "linux"))] + #[cfg_attr(docsrs, doc(cfg(any(windows, target_os = "linux"))))] + #[must_use] + pub fn any_thread(mut self) -> Self { + self.runtime_any_thread = true; + self + } + + /// Defines the JS message handler callback. + /// + /// # Examples + /// ``` + /// #[tauri::command] + /// fn command_1() -> String { + /// return "hello world".to_string(); + /// } + /// tauri::Builder::default() + /// .invoke_handler(tauri::generate_handler![ + /// command_1, + /// // etc... + /// ]); + /// ``` + #[must_use] + pub fn invoke_handler(mut self, invoke_handler: F) -> Self + where + F: Fn(Invoke) -> bool + Send + Sync + 'static, + { + self.invoke_handler = Box::new(invoke_handler); + self + } + + /// Defines a custom JS message system. + /// + /// The `initialization_script` is a script that initializes `window.__TAURI_INTERNALS__.postMessage`. + /// That function must take the `(message: object, options: object)` arguments and send it to the backend. + /// + /// Additionally, the script must include a `__INVOKE_KEY__` token that is replaced with a value that must be sent with the IPC payload + /// to check the integrity of the message by the [`crate::WebviewWindow::on_message`] API, e.g. + /// + /// ```js + /// const invokeKey = __INVOKE_KEY__; + /// fetch('my-impl://command', { + /// headers: { + /// 'Tauri-Invoke-Key': invokeKey, + /// } + /// }) + /// ``` + /// + /// Note that the implementation details is up to your implementation. + #[must_use] + pub fn invoke_system(mut self, initialization_script: impl AsRef) -> Self { + self.invoke_initialization_script = initialization_script + .as_ref() + .replace("__INVOKE_KEY__", &format!("\"{}\"", self.invoke_key)); + self + } + + /// Registers a channel interceptor that can overwrite the default channel implementation. + /// + /// If the event has been consumed, it must return `true`. + /// + /// The channel automatically orders the messages, so the third closure argument represents the message number. + /// The payload expected by the channel receiver is in the form of `{ id: usize, message: T }`. + pub fn channel_interceptor< + F: Fn(&Webview, CallbackFn, usize, &InvokeResponseBody) -> bool + Send + Sync + 'static, + >( + mut self, + interceptor: F, + ) -> Self { + self.channel_interceptor.replace(Box::new(interceptor)); + self + } + + /// Append a custom initialization script. + /// + /// Allow to append custom initialization script instead of replacing entire invoke system. + /// + /// # Examples + /// + /// ``` + /// let custom_script = r#" + /// // A custom call system bridge build on top of tauri invoke system. + /// async function invoke(cmd, args = {}) { + /// if (!args) args = {}; + /// + /// let prefix = ""; + /// + /// if (args?.__module) { + /// prefix = `plugin:hybridcall.${args.__module}|`; + /// } + /// + /// const command = `${prefix}tauri_${cmd}`; + /// + /// const invoke = window.__TAURI_INTERNALS__.invoke; + /// + /// return invoke(command, args).then(result => { + /// if (window.build.debug) { + /// console.log(`call: ${command}`); + /// console.log(`args: ${JSON.stringify(args)}`); + /// console.log(`return: ${JSON.stringify(result)}`); + /// } + /// + /// return result; + /// }); + /// } + /// "#; + /// + /// tauri::Builder::default() + /// .append_invoke_initialization_script(custom_script); + /// ``` + pub fn append_invoke_initialization_script( + mut self, + initialization_script: impl AsRef, + ) -> Self { + self + .invoke_initialization_script + .push_str(initialization_script.as_ref()); + self + } + + /// Defines the setup hook. + /// + /// # Examples + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::Manager; +tauri::Builder::default() + .setup(|app| { + let main_window = app.get_webview_window("main").unwrap(); + main_window.set_title("Tauri!")?; + Ok(()) + }); +``` + "#### + )] + #[must_use] + pub fn setup(mut self, setup: F) -> Self + where + F: FnOnce(&mut App) -> std::result::Result<(), Box> + Send + 'static, + { + self.setup = Box::new(setup); + self + } + + /// Defines the page load hook. + #[must_use] + pub fn on_page_load(mut self, on_page_load: F) -> Self + where + F: Fn(&Webview, &PageLoadPayload<'_>) + Send + Sync + 'static, + { + self.on_page_load.replace(Arc::new(on_page_load)); + self + } + + /// Defines a closure to be executed when a permission is requested. + /// + /// The handler receives the [`crate::webview::PermissionKind`] and should return + /// the desired [`crate::webview::PermissionResponse`]. + /// + /// This is not called if a [`crate::webview::WebviewBuilder::on_permission_request`] + /// is set on the webview and returned a response other than [`crate::webview::PermissionResponse::Default`]. + /// + /// > [!NOTE] + /// > This handler only triggers for new permission requests. If the user has already + /// > allowed or denied a permission persistently within the webview, the browser + /// > will use the saved preference instead of calling this handler. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Fully supported via WebView2's PermissionRequested event. + /// - **macOS / iOS**: Fully supported via WKUIDelegate's requestMediaCapturePermission. + /// - **Linux**: Fully supported via WebKitGTK's permission-request signal. + /// - **Android**: Supported via JNI bridge for geolocation, microphone, camera, + /// protected media, and MIDI requests. Android runtime permissions may still + /// trigger native OS prompts before access is granted. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::webview::{PermissionKind, PermissionResponse}; + /// tauri::Builder::default() + /// .on_permission_request(|_, kind| match kind { + /// PermissionKind::Geolocation => PermissionResponse::Allow, + /// PermissionKind::Notifications => PermissionResponse::Allow, + /// _ => PermissionResponse::Default, + /// }); + /// ``` + #[must_use] + pub fn on_permission_request(mut self, on_permission_request: F) -> Self + where + F: Fn(Webview, crate::webview::PermissionKind) -> crate::webview::PermissionResponse + + Send + + Sync + + 'static, + { + self + .on_permission_request + .replace(Arc::new(on_permission_request)); + 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. + /// + /// # Examples + /// + /// ``` + /// mod plugin { + /// use tauri::{plugin::{Builder as PluginBuilder, TauriPlugin}, RunEvent, Runtime}; + /// + /// // this command can be called in the frontend using `invoke('plugin:window|do_something')`. + /// #[tauri::command] + /// async fn do_something(app: tauri::AppHandle, window: tauri::Window) -> Result<(), String> { + /// println!("command called"); + /// Ok(()) + /// } + /// pub fn init() -> TauriPlugin { + /// PluginBuilder::new("window") + /// .setup(|app, api| { + /// // initialize the plugin here + /// Ok(()) + /// }) + /// .on_event(|app, event| { + /// match event { + /// RunEvent::Ready => { + /// println!("app is ready"); + /// } + /// RunEvent::WindowEvent { label, event, .. } => { + /// println!("window {} received an event: {:?}", label, event); + /// } + /// _ => (), + /// } + /// }) + /// .invoke_handler(tauri::generate_handler![do_something]) + /// .build() + /// } + /// } + /// + /// tauri::Builder::default() + /// .plugin(plugin::init()); + /// ``` + #[must_use] + pub fn plugin + 'static>(self, plugin: P) -> Self { + self.plugin_boxed(Box::new(plugin)) + } + + /// Adds a Tauri application plugin. + /// + /// This method is similar to [`Self::plugin`], + /// but accepts a boxed trait object instead of a generic type. + #[must_use] + pub fn plugin_boxed(mut self, plugin: Box>) -> Self { + self.plugins.register(plugin); + self + } + + /// Add `state` to the state managed by the application. + /// + /// This method can be called any number of times as long as each call + /// refers to a different `T`. + /// + /// Managed state can be retrieved by any command handler via the + /// [`crate::State`] guard. In particular, if a value of type `T` + /// is managed by Tauri, adding `State` to the list of arguments in a + /// command handler instructs Tauri to retrieve the managed value. + /// Additionally, [`state`](crate::Manager#method.state) can be used to retrieve the value manually. + /// + /// # Panics + /// + /// Panics if state of type `T` is already being managed. + /// + /// # Mutability + /// + /// Since the managed state is global and must be [`Send`] + [`Sync`], mutations can only happen through interior mutability: + /// + /// ```,no_run + /// use std::{collections::HashMap, sync::Mutex}; + /// use tauri::State; + /// // here we use Mutex to achieve interior mutability + /// struct Storage { + /// store: Mutex>, + /// } + /// struct Connection; + /// struct DbConnection { + /// db: Mutex>, + /// } + /// + /// #[tauri::command] + /// fn connect(connection: State) { + /// // initialize the connection, mutating the state with interior mutability + /// *connection.db.lock().unwrap() = Some(Connection {}); + /// } + /// + /// #[tauri::command] + /// fn storage_insert(key: u64, value: String, storage: State) { + /// // mutate the storage behind the Mutex + /// storage.store.lock().unwrap().insert(key, value); + /// } + /// + /// tauri::Builder::default() + /// .manage(Storage { store: Default::default() }) + /// .manage(DbConnection { db: Default::default() }) + /// .invoke_handler(tauri::generate_handler![connect, storage_insert]) + /// // on an actual app, remove the string argument + /// .run(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while running tauri application"); + /// ``` + /// + /// # Examples + /// + /// ```,no_run + /// use tauri::State; + /// + /// struct MyInt(isize); + /// struct MyString(String); + /// + /// #[tauri::command] + /// fn int_command(state: State) -> String { + /// format!("The stateful int is: {}", state.0) + /// } + /// + /// #[tauri::command] + /// fn string_command<'r>(state: State<'r, MyString>) { + /// println!("state: {}", state.inner().0); + /// } + /// + /// tauri::Builder::default() + /// .manage(MyInt(10)) + /// .manage(MyString("Hello, managed state!".to_string())) + /// .invoke_handler(tauri::generate_handler![int_command, string_command]) + /// // on an actual app, remove the string argument + /// .run(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while running tauri application"); + /// ``` + #[must_use] + pub fn manage(self, state: T) -> Self + where + T: Send + Sync + 'static, + { + let type_name = std::any::type_name::(); + assert!( + self.state.set(state), + "state for type '{type_name}' is already being managed", + ); + self + } + + /// Sets the menu to use on all windows. + /// + /// # Examples + /// ``` + /// use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu}; + /// + /// tauri::Builder::default() + /// .menu(|handle| Menu::with_items(handle, &[ + /// &Submenu::with_items( + /// handle, + /// "File", + /// true, + /// &[ + /// &PredefinedMenuItem::close_window(handle, None)?, + /// #[cfg(target_os = "macos")] + /// &MenuItem::new(handle, "Hello", true, None::<&str>)?, + /// ], + /// )? + /// ])); + /// ``` + #[must_use] + #[cfg(desktop)] + pub fn menu) -> crate::Result> + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.menu.replace(Box::new(f)); + self + } + + /// Registers an event handler for any menu event. + /// + /// # Examples + /// ``` + /// use tauri::menu::*; + /// + /// tauri::Builder::default() + /// .on_menu_event(|app, event| { + /// if event.id() == "quit" { + /// app.exit(0); + /// } + /// }); + /// ``` + #[must_use] + #[cfg(desktop)] + pub fn on_menu_event, MenuEvent) + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.menu_event_listeners.push(Box::new(f)); + self + } + + /// Registers an event handler for any tray icon event. + /// + /// # Examples + /// ``` + /// use tauri::Manager; + /// + /// tauri::Builder::default() + /// .on_tray_icon_event(|app, event| { + /// let tray = app.tray_by_id(event.id()).expect("can't find tray icon"); + /// let _ = tray.set_visible(false); + /// }); + /// ``` + #[must_use] + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + pub fn on_tray_icon_event, TrayIconEvent) + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.tray_icon_event_listeners.push(Box::new(f)); + self + } + + /// Enable or disable the default menu on macOS. Enabled by default. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .enable_macos_default_menu(false); + /// ``` + #[must_use] + pub fn enable_macos_default_menu(mut self, enable: bool) -> Self { + self.enable_macos_default_menu = enable; + self + } + + /// Configures whether the application should activate when launched while another application + /// is active. + /// + /// By default, the application ignores other applications and activates when launched. + /// + /// If `false`, the application activates only if no other application is currently active. + /// If `true`, the application activates regardless. + /// + /// This only affects the initial activation at launch. Explicitly focusing a window later + /// (e.g. via [`Window::set_focus`](crate::window::Window::set_focus)) still activates the + /// application regardless of this setting. + /// + /// # Examples + /// ```,no_run + /// tauri::Builder::default() + /// .activate_ignoring_other_apps(false); + /// ``` + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + #[must_use] + pub fn activate_ignoring_other_apps(mut self, ignore: bool) -> Self { + self.activate_ignoring_other_apps = ignore; + self + } + + /// Registers a window event handler for all windows. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .on_window_event(|window, event| match event { + /// tauri::WindowEvent::Focused(focused) => { + /// // hide window whenever it loses focus + /// if !focused { + /// window.hide().unwrap(); + /// } + /// } + /// _ => {} + /// }); + /// ``` + #[must_use] + pub fn on_window_event, &WindowEvent) + Send + Sync + 'static>( + mut self, + handler: F, + ) -> Self { + self.window_event_listeners.push(Box::new(handler)); + self + } + + /// Registers a webview event handler for all webviews. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .on_webview_event(|window, event| match event { + /// tauri::WebviewEvent::DragDrop(event) => { + /// println!("{:?}", event); + /// } + /// _ => {} + /// }); + /// ``` + #[must_use] + pub fn on_webview_event, &WebviewEvent) + Send + Sync + 'static>( + mut self, + handler: F, + ) -> Self { + self.webview_event_listeners.push(Box::new(handler)); + self + } + + /// Registers a URI scheme protocol available to all webviews. + /// + /// Leverages [setURLSchemeHandler](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler) on macOS, + /// [AddWebResourceRequestedFilter](https://docs.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.addwebresourcerequestedfilter?view=webview2-dotnet-1.0.774.44) on Windows + /// and [webkit-web-context-register-uri-scheme](https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebContext.html#webkit-web-context-register-uri-scheme) on Linux. + /// + /// # Arguments + /// + /// * `uri_scheme` The URI scheme to register, such as `example`. + /// * `protocol` the protocol associated with the given URI scheme. It's a function that takes a request and returns a response. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .register_uri_scheme_protocol("app-files", |_ctx, request| { + /// // skip leading `/` + /// if let Ok(data) = std::fs::read(&request.uri().path()[1..]) { + /// http::Response::builder() + /// .body(data) + /// .unwrap() + /// } else { + /// http::Response::builder() + /// .status(http::StatusCode::BAD_REQUEST) + /// .header(http::header::CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + /// .body("failed to read file".as_bytes().to_vec()) + /// .unwrap() + /// } + /// }); + /// ``` + /// + /// # Warning + /// + /// Pages loaded from a custom protocol will have a different Origin on different platforms. + /// Servers which enforce CORS will need to add the exact same Origin header (or `*`) in `Access-Control-Allow-Origin` + /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the + /// different Origin headers across platforms: + /// + /// - macOS, iOS and Linux: `://localhost/` (so it will be `my-scheme://localhost/path/to/page). + /// - Windows and Android: `http://.localhost/` by default (so it will be `http://my-scheme.localhost/path/to/page`). + /// To use `https` instead of `http`, use [`super::webview::WebviewBuilder::use_https_scheme`]. + #[must_use] + pub fn register_uri_scheme_protocol< + N: Into, + T: Into>, + H: Fn(UriSchemeContext<'_, R>, http::Request>) -> http::Response + + Send + + Sync + + 'static, + >( + mut self, + uri_scheme: N, + protocol_handler: H, + ) -> Self { + self.uri_scheme_protocols.insert( + uri_scheme.into(), + Arc::new(UriSchemeProtocol { + handler: Box::new(move |ctx, request, responder| { + responder.respond(protocol_handler(ctx, request)) + }), + }), + ); + self + } + + /// Similar to [`Self::register_uri_scheme_protocol`] but with an asynchronous responder that allows you + /// to process the request in a separate thread and respond asynchronously. + /// + /// # Arguments + /// + /// * `uri_scheme` The URI scheme to register, such as `example`. + /// * `protocol` the protocol associated with the given URI scheme. It's a function that takes an URL such as `example://localhost/asset.css`. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .register_asynchronous_uri_scheme_protocol("app-files", |_ctx, request, responder| { + /// // skip leading `/` + /// let path = request.uri().path()[1..].to_string(); + /// std::thread::spawn(move || { + /// if let Ok(data) = std::fs::read(path) { + /// responder.respond( + /// http::Response::builder() + /// .body(data) + /// .unwrap() + /// ); + /// } else { + /// responder.respond( + /// http::Response::builder() + /// .status(http::StatusCode::BAD_REQUEST) + /// .header(http::header::CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + /// .body("failed to read file".as_bytes().to_vec()) + /// .unwrap() + /// ); + /// } + /// }); + /// }); + /// ``` + /// + /// # Warning + /// + /// Pages loaded from a custom protocol will have a different Origin on different platforms. + /// Servers which enforce CORS will need to add the exact same Origin header (or `*`) in `Access-Control-Allow-Origin` + /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the + /// different Origin headers across platforms: + /// + /// - macOS, iOS and Linux: `://localhost/` (so it will be `my-scheme://localhost/path/to/page). + /// - Windows and Android: `http://.localhost/` by default (so it will be `http://my-scheme.localhost/path/to/page`). + /// To use `https` instead of `http`, use [`super::webview::WebviewBuilder::use_https_scheme`]. + #[must_use] + pub fn register_asynchronous_uri_scheme_protocol< + N: Into, + H: Fn(UriSchemeContext<'_, R>, http::Request>, UriSchemeResponder) + Send + Sync + 'static, + >( + mut self, + uri_scheme: N, + protocol_handler: H, + ) -> Self { + self.uri_scheme_protocols.insert( + uri_scheme.into(), + Arc::new(UriSchemeProtocol { + handler: Box::new(protocol_handler), + }), + ); + self + } + + /// Change the device event filter mode. + /// + /// Since the DeviceEvent capture can lead to high CPU usage for unfocused windows, [`tao`] + /// will ignore them by default for unfocused windows on Windows. This method allows changing + /// the filter to explicitly capture them again. + /// + /// ## Platform-specific + /// + /// - ** Linux / macOS / iOS / Android**: Unsupported. + /// + /// # Examples + /// ```,no_run + /// tauri::Builder::default() + /// .device_event_filter(tauri::DeviceEventFilter::Always); + /// ``` + /// + /// [`tao`]: https://crates.io/crates/tao + pub fn device_event_filter(mut self, filter: DeviceEventFilter) -> Self { + self.device_event_filter = filter; + self + } + + /// Builds the application. + /// + /// This creates the underlying webview runtime (the event loop), registers the core plugins and + /// initializes everything defined on the builder, but it does not start the application: + /// call [`App::run`] or [`App::run_return`] on the returned value to do so, or use [`Self::run`] + /// to do both at once. + /// + /// # Errors + /// + /// Returns an error if the application cannot be initialized, in particular when: + /// + /// - the webview runtime (event loop) cannot be created, for instance when it is created outside + /// the main thread, when the display server cannot be reached or when the WebView2 runtime is + /// missing on Windows; + /// - the `app > security > assetProtocol > scope` patterns in the configuration are invalid; + /// - the app menu defined with [`Self::menu`] cannot be created; + /// - the tray icon defined in the configuration cannot be created; + /// - a plugin fails to initialize, i.e. its [`Plugin::initialize`](crate::plugin::Plugin::initialize) + /// hook returns an error, which is also the case when a plugin is registered twice. + /// + /// # Examples + /// + /// ```rust,no_run + /// tauri::Builder::default() + /// // on an actual app, remove the string argument + /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while building tauri application") + /// .run(|_app_handle, _event| {}); + /// ``` + #[allow(clippy::type_complexity, unused_mut)] + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "app::build", skip_all) + )] + pub fn build(mut self, context: Context) -> crate::Result> { + #[cfg(target_os = "macos")] + if self.menu.is_none() && self.enable_macos_default_menu { + self.menu = Some(Box::new(|app_handle| { + crate::menu::Menu::default(app_handle) + })); + } + + let manager = Arc::new(AppManager::with_handlers( + context, + self.plugins, + self.invoke_handler, + self.on_page_load, + self.on_permission_request, + #[cfg(any(target_os = "macos", target_os = "ios"))] + self.on_web_content_process_terminate, + self.uri_scheme_protocols, + self.state, + #[cfg(desktop)] + self.menu_event_listeners, + #[cfg(all(desktop, feature = "tray-icon"))] + self.tray_icon_event_listeners, + self.window_event_listeners, + self.webview_event_listeners, + #[cfg(desktop)] + HashMap::new(), + self.invoke_initialization_script, + self.channel_interceptor, + self.invoke_key, + )); + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + let app_id = if manager.config.app.enable_gtk_app_id { + Some(manager.config.identifier.clone()) + } else { + None + }; + + let runtime_args = RuntimeInitArgs { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + app_id, + + #[cfg(windows)] + msg_hook: { + let menus = manager.menu.menus.clone(); + Some(Box::new(move |msg| { + use windows::Win32::UI::WindowsAndMessaging::{HACCEL, MSG, TranslateAcceleratorW}; + unsafe { + let msg = msg as *const MSG; + for menu in menus.lock().unwrap().values() { + let translated = + TranslateAcceleratorW((*msg).hwnd, HACCEL(menu.inner().haccel() as _), msg); + if translated == 1 { + return true; + } + } + + false + } + })) + }, + }; + + // The env var must be set before the Runtime is created so that GetAvailableBrowserVersionString picks it up. + #[cfg(windows)] + { + if let crate::utils::config::WebviewInstallMode::FixedRuntime { path } = + &manager.config.bundle.windows.webview_install_mode + { + if let Some(exe_dir) = crate::utils::platform::current_exe() + .ok() + .and_then(|p| p.parent().map(|p| p.to_path_buf())) + { + // SAFETY: Always safe on Windows + unsafe { std::env::set_var("WEBVIEW2_BROWSER_EXECUTABLE_FOLDER", exe_dir.join(path)) }; + } else { + #[cfg(debug_assertions)] + eprintln!( + "failed to resolve resource directory; fallback to the installed Webview2 runtime." + ); + } + } + } + + #[cfg(any(windows, target_os = "linux"))] + let mut runtime = if self.runtime_any_thread { + R::new_any_thread(runtime_args)? + } else { + R::new(runtime_args)? + }; + #[cfg(not(any(windows, target_os = "linux")))] + let mut runtime = R::new(runtime_args)?; + + #[cfg(target_os = "macos")] + runtime.set_activate_ignoring_other_apps(self.activate_ignoring_other_apps); + + #[cfg(desktop)] + { + // setup menu event handler + let proxy = runtime.create_proxy(); + muda::MenuEvent::set_event_handler(Some(move |e: muda::MenuEvent| { + let _ = proxy.send_event(EventLoopMessage::MenuEvent(e.into())); + })); + + // setup tray event handler + #[cfg(feature = "tray-icon")] + { + let proxy = runtime.create_proxy(); + tray_icon::TrayIconEvent::set_event_handler(Some(move |e: tray_icon::TrayIconEvent| { + let _ = proxy.send_event(EventLoopMessage::TrayIconEvent(e.into())); + })); + } + } + + runtime.set_device_event_filter(self.device_event_filter); + + let runtime_handle = runtime.handle(); + + let app = App { + runtime: Some(runtime), + setup: Some(self.setup), + manager: manager.clone(), + handle: AppHandle { + runtime_handle, + manager, + event_loop: Arc::new(Mutex::new(EventLoop { + main_thread_id: std::thread::current().id(), + })), + }, + ran_setup: false, + }; + + app.register_core_plugins()?; + + #[cfg(desktop)] + if let Some(menu) = self.menu { + let menu = menu(&app.handle)?; + app + .manager + .menu + .menus_stash_lock() + .insert(menu.id().clone(), menu.clone()); + + #[cfg(target_os = "macos")] + init_app_menu(&menu)?; + + app.manager.menu.menu_lock().replace(menu); + } + + let env = Env::default(); + app.manage(env); + + app.manage(Scopes { + #[cfg(feature = "protocol-asset")] + asset_protocol: crate::scope::fs::Scope::new( + &app, + &app.config().app.security.asset_protocol.scope, + )?, + }); + + app.manage(ChannelDataIpcQueue::default()); + app.handle.plugin(crate::ipc::channel::plugin())?; + + let handle = app.handle(); + + // initialize default tray icon if defined + #[cfg(all(desktop, feature = "tray-icon"))] + { + let config = app.config(); + if let Some(tray_config) = &config.app.tray_icon { + let mut tray = + TrayIconBuilder::with_id(tray_config.id.clone().unwrap_or_else(|| "main".into())) + .icon_as_template(tray_config.icon_as_template) + .show_menu_on_left_click(tray_config.show_menu_on_left_click); + if let Some(icon) = &app.manager.tray.icon { + tray = tray.icon(icon.clone()); + } + if let Some(title) = &tray_config.title { + tray = tray.title(title); + } + if let Some(tooltip) = &tray_config.tooltip { + tray = tray.tooltip(tooltip); + } + tray.build(handle)?; + } + } + + app.manager.initialize_plugins(handle)?; + + Ok(app) + } + + /// Builds the configured application and runs it. + /// + /// This is a shorthand for [`Self::build`] followed by [`App::run`]. + /// For more flexibility, consider using those functions manually. + pub fn run(self, context: Context) -> crate::Result<()> { + self.build(context)?.run(|_, _| {}); + Ok(()) + } +} + +pub(crate) type UriSchemeResponderFn = Box>) + Send>; + +/// Async uri scheme protocol responder. +pub struct UriSchemeResponder(pub(crate) UriSchemeResponderFn); + +impl UriSchemeResponder { + /// Resolves the request with the given response. + pub fn respond>>(self, response: http::Response) { + let (parts, body) = response.into_parts(); + (self.0)(http::Response::from_parts(parts, body.into())) + } +} + +/// Uri scheme protocol context +pub struct UriSchemeContext<'a, R: Runtime> { + pub(crate) app_handle: &'a AppHandle, + pub(crate) webview_label: &'a str, +} + +impl<'a, R: Runtime> UriSchemeContext<'a, R> { + /// Get a reference to an [`AppHandle`]. + pub fn app_handle(&self) -> &'a AppHandle { + self.app_handle + } + + /// Get the webview label that made the uri scheme request. + pub fn webview_label(&self) -> &'a str { + self.webview_label + } +} + +#[cfg(target_os = "macos")] +fn init_app_menu(menu: &Menu) -> crate::Result<()> { + menu.inner().init_for_nsapp(); + + if let Some(window_menu) = menu.get(crate::menu::WINDOW_SUBMENU_ID) { + if let Some(m) = window_menu.as_submenu() { + m.set_as_windows_menu_for_nsapp()?; + } + } + if let Some(help_menu) = menu.get(crate::menu::HELP_SUBMENU_ID) { + if let Some(m) = help_menu.as_submenu() { + m.set_as_help_menu_for_nsapp()?; + } + } + + Ok(()) +} + +impl HasDisplayHandle for AppHandle { + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + self.runtime_handle.display_handle() + } +} + +impl HasDisplayHandle for App { + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + self.handle.display_handle() + } +} + +#[cfg_attr(feature = "tracing", tracing::instrument(name = "app::setup"))] +fn setup(app: &mut App) -> crate::Result<()> { + app.ran_setup = true; + + for window_config in app.config().app.windows.iter().filter(|w| w.create) { + WebviewWindowBuilder::from_config(app.handle(), window_config)?.build()?; + } + + app.manager.assets.setup(app); + + if let Some(setup) = app.setup.take() { + (setup)(app).map_err(|e| crate::Error::Setup(e.into()))?; + } + + Ok(()) +} + +fn on_event_loop_event( + app_handle: &AppHandle, + event: RuntimeRunEvent, + manager: &AppManager, +) -> RunEvent { + if let RuntimeRunEvent::WindowEvent { + label, + event: RuntimeWindowEvent::Destroyed, + } = &event + { + manager.on_window_close(label); + } + + let event = match event { + RuntimeRunEvent::Exit => RunEvent::Exit, + RuntimeRunEvent::ExitRequested { code, tx } => RunEvent::ExitRequested { + code, + api: ExitRequestApi { tx, code }, + }, + RuntimeRunEvent::WindowEvent { label, event } => RunEvent::WindowEvent { + label, + event: event.into(), + }, + RuntimeRunEvent::WebviewEvent { label, event } => RunEvent::WebviewEvent { + label, + event: event.into(), + }, + RuntimeRunEvent::Ready => { + // set the app icon in development + #[cfg(all(dev, target_os = "macos"))] + { + use objc2::{AllocAnyThread, MainThreadMarker}; + use objc2_app_kit::{NSApplication, NSImage}; + use objc2_foundation::NSData; + + if let Some(icon) = app_handle.manager.app_icon.clone() { + // TODO: Enable this check. + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + let app = NSApplication::sharedApplication(mtm); + let data = NSData::with_bytes(&icon); + let app_icon = NSImage::initWithData(NSImage::alloc(), &data).expect("creating icon"); + unsafe { app.setApplicationIconImage(Some(&app_icon)) }; + } + } + RunEvent::Ready + } + RuntimeRunEvent::Resumed => RunEvent::Resumed, + RuntimeRunEvent::MainEventsCleared => RunEvent::MainEventsCleared, + RuntimeRunEvent::UserEvent(t) => { + match t { + #[cfg(desktop)] + EventLoopMessage::MenuEvent(ref e) => { + for listener in &*app_handle + .manager + .menu + .global_event_listeners + .lock() + .unwrap() + { + listener(app_handle, e.clone()); + } + for (label, listener) in &*app_handle.manager.menu.event_listeners.lock().unwrap() { + if let Some(w) = app_handle.manager().get_window(label) { + listener(&w, e.clone()); + } + } + } + #[cfg(all(desktop, feature = "tray-icon"))] + EventLoopMessage::TrayIconEvent(ref e) => { + for listener in &*app_handle + .manager + .tray + .global_event_listeners + .lock() + .unwrap() + { + listener(app_handle, e.clone()); + } + + for (id, listener) in &*app_handle.manager.tray.event_listeners.lock().unwrap() { + if e.id() == id { + if let Some(tray) = app_handle.tray_by_id(id) { + listener(&tray, e.clone()); + } + } + } + } + } + + #[allow(unreachable_code)] + t.into() + } + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))] + RuntimeRunEvent::Opened { urls } => RunEvent::Opened { urls }, + #[cfg(target_os = "macos")] + RuntimeRunEvent::Reopen { + has_visible_windows, + } => RunEvent::Reopen { + has_visible_windows, + }, + #[cfg(target_os = "ios")] + RuntimeRunEvent::SceneRequested { scene, options } => { + RunEvent::SceneRequested { scene, options } + } + _ => unimplemented!(), + }; + + manager + .plugins + .lock() + .expect("poisoned plugin store") + .on_event(app_handle, &event); + + event +} + +#[cfg(test)] +mod tests { + #[test] + fn is_send_sync() { + crate::test_utils::assert_send::(); + crate::test_utils::assert_sync::(); + + #[cfg(feature = "wry")] + { + crate::test_utils::assert_send::>(); + crate::test_utils::assert_sync::>(); + } + } +} diff --git a/crates/tauri/src/app/plugin.rs b/crates/tauri/src/app/plugin.rs new file mode 100644 index 0000000..1decde6 --- /dev/null +++ b/crates/tauri/src/app/plugin.rs @@ -0,0 +1,158 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri_utils::{Theme, config::BundleType}; + +use crate::{ + AppHandle, Manager, ResourceId, Runtime, Webview, command, + plugin::{Builder, TauriPlugin}, +}; + +#[command(root = "crate")] +pub fn version(app: AppHandle) -> String { + app.package_info().version.to_string() +} + +#[command(root = "crate")] +pub fn name(app: AppHandle) -> String { + app.package_info().name.clone() +} + +#[command(root = "crate")] +pub fn tauri_version() -> &'static str { + crate::VERSION +} + +#[command(root = "crate")] +pub fn identifier(app: AppHandle) -> String { + app.config().identifier.clone() +} + +#[command(root = "crate")] +#[allow(unused_variables)] +pub fn app_show(app: AppHandle) -> crate::Result<()> { + #[cfg(target_os = "macos")] + app.show()?; + Ok(()) +} + +#[command(root = "crate")] +#[allow(unused_variables)] +pub fn app_hide(app: AppHandle) -> crate::Result<()> { + #[cfg(target_os = "macos")] + app.hide()?; + Ok(()) +} + +#[command(root = "crate")] +pub fn exit(app: AppHandle, code: i32) { + app.exit(code) +} + +#[command(root = "crate")] +#[allow(unused_variables)] +pub async fn fetch_data_store_identifiers( + app: AppHandle, +) -> crate::Result> { + #[cfg(target_vendor = "apple")] + return app.fetch_data_store_identifiers().await; + #[cfg(not(target_vendor = "apple"))] + return Ok(Vec::new()); +} + +#[command(root = "crate")] +#[allow(unused_variables)] +pub async fn remove_data_store(app: AppHandle, uuid: [u8; 16]) -> crate::Result<()> { + #[cfg(target_vendor = "apple")] + app.remove_data_store(uuid).await?; + #[cfg(not(target_vendor = "apple"))] + let _ = uuid; + Ok(()) +} + +#[command(root = "crate")] +pub fn default_window_icon( + webview: Webview, + app: AppHandle, +) -> Option { + app.default_window_icon().cloned().map(|icon| { + let mut resources_table = webview.resources_table(); + resources_table.add(icon.to_owned()) + }) +} + +#[command(root = "crate")] +pub async fn set_app_theme(app: AppHandle, theme: Option) { + app.set_theme(theme); +} + +#[command(root = "crate")] +pub async fn set_dock_visibility( + app: AppHandle, + visible: bool, +) -> crate::Result<()> { + #[cfg(target_os = "macos")] + { + let mut focused_window = None; + for window in app.manager.windows().into_values() { + if window.is_focused().unwrap_or_default() { + focused_window.replace(window); + break; + } + } + + app.set_dock_visibility(visible)?; + + // retain focus + if let Some(focused_window) = focused_window { + let _ = focused_window.set_focus(); + } + } + #[cfg(not(target_os = "macos"))] + let (_app, _visible) = (app, visible); + Ok(()) +} + +#[command(root = "crate")] +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![ + #![plugin(app)] + version, + name, + tauri_version, + identifier, + app_show, + app_hide, + exit, + fetch_data_store_identifiers, + remove_data_store, + default_window_icon, + set_app_theme, + set_dock_visibility, + bundle_type, + supports_multiple_windows, + ]) + .setup(|_app, _api| { + #[cfg(target_os = "android")] + { + let handle = _api.register_android_plugin("app.tauri", "AppPlugin")?; + _app.manage(AppPlugin(handle)); + } + Ok(()) + }) + .build() +} + +#[cfg(target_os = "android")] +pub(crate) struct AppPlugin(pub crate::plugin::PluginHandle); diff --git a/crates/tauri/src/async_runtime.rs b/crates/tauri/src/async_runtime.rs new file mode 100644 index 0000000..970d5e3 --- /dev/null +++ b/crates/tauri/src/async_runtime.rs @@ -0,0 +1,363 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The singleton async runtime used by Tauri and exposed to users. +//! +//! Tauri uses [`tokio`] Runtime to initialize code, such as +//! [`Plugin::initialize`](../plugin/trait.Plugin.html#method.initialize) and [`crate::Builder::setup`] hooks. +//! This module also re-export some common items most developers need from [`tokio`]. If there's +//! one you need isn't here, you could use types in [`tokio`] directly. +//! For custom command handlers, it's recommended to use a plain `async fn` command. + +pub use tokio::{ + runtime::{Handle as TokioHandle, Runtime as TokioRuntime}, + sync::{ + Mutex, RwLock, + mpsc::{Receiver, Sender, channel}, + }, + task::JoinHandle as TokioJoinHandle, +}; + +use std::{ + future::Future, + pin::Pin, + sync::OnceLock, + task::{Context, Poll}, +}; + +static RUNTIME: OnceLock = OnceLock::new(); + +struct GlobalRuntime { + runtime: Option, + handle: RuntimeHandle, +} + +impl GlobalRuntime { + fn handle(&self) -> RuntimeHandle { + if let Some(r) = &self.runtime { + r.handle() + } else { + self.handle.clone() + } + } + + #[track_caller] + fn spawn(&self, task: F) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + if let Some(r) = &self.runtime { + r.spawn(task) + } else { + self.handle.spawn(task) + } + } + + #[track_caller] + /// Runs the provided function on an executor dedicated to blocking operations. + pub fn spawn_blocking(&self, func: F) -> JoinHandle + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + if let Some(r) = &self.runtime { + r.spawn_blocking(func) + } else { + self.handle.spawn_blocking(func) + } + } + + #[track_caller] + fn block_on(&self, task: F) -> F::Output { + if let Some(r) = &self.runtime { + r.block_on(task) + } else { + self.handle.block_on(task) + } + } +} + +/// A runtime used to execute asynchronous tasks. +pub enum Runtime { + /// The tokio runtime. + Tokio(TokioRuntime), +} + +impl Runtime { + /// Gets a reference to the [`TokioRuntime`]. + pub fn inner(&self) -> &TokioRuntime { + let Self::Tokio(r) = self; + r + } + + /// Returns a handle of the async runtime. + pub fn handle(&self) -> RuntimeHandle { + match self { + Self::Tokio(r) => RuntimeHandle::Tokio(r.handle().clone()), + } + } + + #[track_caller] + /// Spawns a future onto the runtime. + pub fn spawn(&self, task: F) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + match self { + Self::Tokio(r) => { + let _guard = r.enter(); + JoinHandle::Tokio(tokio::spawn(task)) + } + } + } + + #[track_caller] + /// Runs the provided function on an executor dedicated to blocking operations. + pub fn spawn_blocking(&self, func: F) -> JoinHandle + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + match self { + Self::Tokio(r) => JoinHandle::Tokio(r.spawn_blocking(func)), + } + } + + #[track_caller] + /// Runs a future to completion on runtime. + pub fn block_on(&self, task: F) -> F::Output { + match self { + Self::Tokio(r) => r.block_on(task), + } + } +} + +/// An owned permission to join on a task (await its termination). +#[derive(Debug)] +pub enum JoinHandle { + /// The tokio JoinHandle. + Tokio(TokioJoinHandle), +} + +impl JoinHandle { + /// Gets a reference to the [`TokioJoinHandle`]. + pub fn inner(&self) -> &TokioJoinHandle { + let Self::Tokio(t) = self; + t + } + + /// Abort the task associated with the handle. + /// + /// Awaiting a cancelled task might complete as usual if the task was + /// already completed at the time it was cancelled, but most likely it + /// will fail with a cancelled `JoinError`. + pub fn abort(&self) { + match self { + Self::Tokio(t) => t.abort(), + } + } +} + +impl Future for JoinHandle { + type Output = crate::Result; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.get_mut() { + Self::Tokio(t) => Pin::new(t).poll(cx).map_err(Into::into), + } + } +} + +/// A handle to the async runtime +#[derive(Clone)] +pub enum RuntimeHandle { + /// The tokio handle. + Tokio(TokioHandle), +} + +impl RuntimeHandle { + /// Gets a reference to the [`TokioHandle`]. + pub fn inner(&self) -> &TokioHandle { + let Self::Tokio(h) = self; + h + } + + #[track_caller] + /// Runs the provided function on an executor dedicated to blocking operations. + /// + /// Use this instead of [`Self::spawn`] for code that blocks the thread it runs on + /// (synchronous file or network I/O, heavy computation, FFI calls) so the async executor + /// is not blocked. The returned [`JoinHandle`] resolves to the closure's return value. + /// + /// # Examples + /// + /// ```rust + /// use tauri::Manager; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle().clone(); + /// tauri::async_runtime::handle().spawn_blocking(move || { + /// let contents = std::fs::read_to_string("file.txt").unwrap_or_default(); + /// handle.package_info().name.len() + contents.len() + /// }); + /// Ok(()) + /// }); + /// ``` + pub fn spawn_blocking(&self, func: F) -> JoinHandle + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + match self { + Self::Tokio(h) => JoinHandle::Tokio(h.spawn_blocking(func)), + } + } + + #[track_caller] + /// Spawns a future onto the runtime. + pub fn spawn(&self, task: F) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + match self { + Self::Tokio(h) => { + let _guard = h.enter(); + JoinHandle::Tokio(tokio::spawn(task)) + } + } + } + + #[track_caller] + /// Runs a future to completion on runtime. + pub fn block_on(&self, task: F) -> F::Output { + match self { + Self::Tokio(h) => h.block_on(task), + } + } +} + +fn default_runtime() -> GlobalRuntime { + let runtime = Runtime::Tokio(TokioRuntime::new().unwrap()); + let handle = runtime.handle(); + GlobalRuntime { + runtime: Some(runtime), + handle, + } +} + +/// Sets the runtime to use to execute asynchronous tasks. +/// For convenience, this method takes a [`TokioHandle`]. +/// Note that you cannot drop the underlying [`TokioRuntime`]. +/// +/// # Examples +/// +/// ```rust +/// #[tokio::main] +/// async fn main() { +/// // perform some async task before initializing the app +/// do_something().await; +/// // share the current runtime with Tauri +/// tauri::async_runtime::set(tokio::runtime::Handle::current()); +/// +/// // bootstrap the tauri app... +/// // tauri::Builder::default().run().unwrap(); +/// } +/// +/// async fn do_something() {} +/// ``` +/// +/// # Panics +/// +/// Panics if the runtime is already set. +pub fn set(handle: TokioHandle) { + RUNTIME + .set(GlobalRuntime { + runtime: None, + handle: RuntimeHandle::Tokio(handle), + }) + .unwrap_or_else(|_| panic!("runtime already initialized")) +} + +/// Returns a handle of the async runtime. +pub fn handle() -> RuntimeHandle { + let runtime = RUNTIME.get_or_init(default_runtime); + 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 + F: Future + Send + 'static, + F::Output: Send + 'static, +{ + let runtime = RUNTIME.get_or_init(default_runtime); + runtime.spawn(task) +} + +#[track_caller] +/// Runs the provided function on an executor dedicated to blocking operations. +pub fn spawn_blocking(func: F) -> JoinHandle +where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, +{ + let runtime = RUNTIME.get_or_init(default_runtime); + runtime.spawn_blocking(func) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn runtime_spawn() { + let join = spawn(async { 5 }); + assert_eq!(join.await.unwrap(), 5); + } + + #[test] + fn runtime_block_on() { + assert_eq!(block_on(async { 0 }), 0); + } + + #[tokio::test] + async fn handle_spawn() { + let handle = handle(); + let join = handle.spawn(async { 5 }); + assert_eq!(join.await.unwrap(), 5); + } + + #[test] + fn handle_block_on() { + let handle = handle(); + assert_eq!(handle.block_on(async { 0 }), 0); + } + + #[tokio::test] + async fn handle_abort() { + let handle = handle(); + let join = handle.spawn(async { + // Here we sleep 1 second to ensure this task to be uncompleted when abort() invoked. + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + 5 + }); + join.abort(); + if let crate::Error::JoinError(raw_error) = join.await.unwrap_err() { + assert!(raw_error.is_cancelled()); + } else { + panic!("Abort did not result in the expected `JoinError`"); + } + } +} diff --git a/crates/tauri/src/error.rs b/crates/tauri/src/error.rs new file mode 100644 index 0000000..cc1f950 --- /dev/null +++ b/crates/tauri/src/error.rs @@ -0,0 +1,198 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::fmt; + +/// A generic boxed error. +#[derive(Debug)] +pub struct SetupError(Box); + +impl From> for SetupError { + fn from(error: Box) -> Self { + Self(error) + } +} + +// safety: the setup error is only used on the main thread +// and we exit the process immediately. +unsafe impl Send for SetupError {} +unsafe impl Sync for SetupError {} + +impl fmt::Display for SetupError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl std::error::Error for SetupError {} + +/// Runtime errors that can happen inside a Tauri application. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum Error { + /// Runtime error. + #[error("runtime error: {0}")] + Runtime(#[from] tauri_runtime::Error), + /// Window label must be unique. + #[error("a window with label `{0}` already exists")] + WindowLabelAlreadyExists(String), + /// Webview label must be unique. + #[error("a webview with label `{0}` already exists")] + WebviewLabelAlreadyExists(String), + /// Cannot use the webview reparent function on webview windows. + #[error("cannot reparent when using a WebviewWindow")] + CannotReparentWebviewWindow, + /// Embedded asset not found. + #[error("asset not found: {0}")] + AssetNotFound(String), + /// Failed to serialize/deserialize. + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + /// IO error. + #[error("{0}")] + Io(#[from] std::io::Error), + /// Failed to load window icon. + #[error("invalid icon: {0}")] + InvalidIcon(std::io::Error), + /// Invalid args when running a command. + #[error("invalid args `{1}` for command `{0}`: {2}")] + InvalidArgs(&'static str, &'static str, serde_json::Error), + /// Encountered an error in the setup hook, + #[error("error encountered during setup hook: {0}")] + Setup(SetupError), + /// Error initializing plugin. + #[error("failed to initialize plugin `{0}`: {1}")] + PluginInitialization(String, String), + /// A part of the URL is malformed or invalid. This may occur when parsing and combining + /// user-provided URLs and paths. + #[error("invalid url: {0}")] + InvalidUrl(url::ParseError), + /// Task join error. + #[error(transparent)] + JoinError(#[from] tokio::task::JoinError), + /// An error happened inside the isolation pattern. + #[cfg(feature = "isolation")] + #[error("isolation pattern error: {0}")] + IsolationPattern(#[from] tauri_utils::pattern::isolation::Error), + /// An invalid window URL was provided. Includes details about the error. + #[error("invalid window url: {0}")] + InvalidWebviewUrl(&'static str), + /// Invalid glob pattern. + #[error("invalid glob pattern: {0}")] + GlobPattern(#[from] glob::PatternError), + /// Image error. + #[cfg(any(feature = "image-png", feature = "image-ico"))] + #[error("failed to process image: {0}")] + Image(#[from] image::error::ImageError), + /// The Window's raw handle is invalid for the platform. + #[error("Unexpected `raw_window_handle` for the current platform")] + InvalidWindowHandle, + /// JNI error. + #[cfg(target_os = "android")] + #[error("jni error: {0}")] + Jni(#[from] jni::errors::Error), + /// Failed to receive message . + #[error("failed to receive message")] + FailedToReceiveMessage, + /// Menu error. + #[error("menu error: {0}")] + #[cfg(desktop)] + Menu(#[from] muda::Error), + /// Bad menu icon error. + #[error(transparent)] + #[cfg(desktop)] + BadMenuIcon(#[from] muda::BadIcon), + /// Tray icon error. + #[error("tray icon error: {0}")] + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + Tray(#[from] tray_icon::Error), + /// Bad tray icon error. + #[error(transparent)] + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + BadTrayIcon(#[from] tray_icon::BadIcon), + /// Path does not have a parent. + #[error("path does not have a parent")] + NoParent, + /// Path does not have an extension. + #[error("path does not have an extension")] + NoExtension, + /// Path does not have a basename. + #[error("path does not have a basename")] + NoBasename, + /// Path is not valid UTF-8 and cannot be represented in a URL. + #[error("path is not valid UTF-8: {}", .0.display())] + NonUtf8Path(std::path::PathBuf), + /// Cannot resolve current directory. + #[error("failed to read current dir: {0}")] + CurrentDir(std::io::Error), + /// Unknown path. + #[cfg(not(target_os = "android"))] + #[error("unknown path")] + UnknownPath, + /// Failed to invoke mobile plugin. + #[cfg(target_os = "android")] + #[error(transparent)] + PluginInvoke(#[from] crate::plugin::mobile::PluginInvokeError), + /// window not found. + #[error("window not found")] + WindowNotFound, + /// The resource id is invalid. + #[error("The resource id {0} is invalid.")] + BadResourceId(crate::resources::ResourceId), + /// The anyhow crate error. + #[error(transparent)] + Anyhow(#[from] anyhow::Error), + /// webview not found. + #[error("webview not found")] + WebviewNotFound, + /// API requires the unstable feature flag. + #[error("this feature requires the `unstable` flag on Cargo.toml")] + UnstableFeatureNotSupported, + /// Failed to deserialize scope object. + #[error("error deserializing scope: {0}")] + CannotDeserializeScope(Box), + /// Failed to get a raw handle. + #[error(transparent)] + RawHandleError(#[from] raw_window_handle::HandleError), + /// Something went wrong with the CSPRNG. + #[error("unable to generate random bytes from the operating system: {0}")] + Csprng(getrandom::Error), + /// Bad `__TAURI_INVOKE_KEY__` value received in ipc message. + #[error("bad __TAURI_INVOKE_KEY__ value received in ipc message")] + InvokeKey, + /// Illegal event name. + #[error("only alphanumeric, '-', '/', ':', '_' permitted for event names: {0:?}")] + IllegalEventName(String), + /// tokio oneshot channel failed to receive message + #[error(transparent)] + TokioOneshotRecv(#[from] tokio::sync::oneshot::error::RecvError), + /// Unexpected menu kind passed to menu/tray plugin command + #[error("Unexpected menu kind")] + UnexpectedMenuKind, + /// [`crate::image::Image::from_icon_resource`] failed + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + #[error("Can not load Image from icon resources: {0}")] + ImageFromResource(windows::core::Error), +} + +impl From for Error { + fn from(value: getrandom::Error) -> Self { + Self::Csprng(value) + } +} + +/// `Result` +pub type Result = std::result::Result; + +#[cfg(test)] +mod tests { + #[test] + fn error_is_send_sync() { + crate::test_utils::assert_send::(); + crate::test_utils::assert_sync::(); + } +} diff --git a/crates/tauri/src/event/event-system-spec.md b/crates/tauri/src/event/event-system-spec.md new file mode 100644 index 0000000..2fc910b --- /dev/null +++ b/crates/tauri/src/event/event-system-spec.md @@ -0,0 +1,34 @@ +# Tauri Event System Specification + +## Emitters + +Emitters can emit to any and all listeners. + +- `App` and `AppHandle` +- `Window` +- `Webview` +- `WebviewWindow` +- Any type that implements `Manager` trait. + +## Emit functions + +- `emit`: emits an event to all listeners. +- `emit_to`: emits an event to a specified target. +- `emit_filter`: emits an event to targets based on a filtering callback. + +## Listeners + +Emitters can emit to any and all listeners. + +- `App` and `AppHandle` +- `Window` +- `Webview` +- `WebviewWindow` +- Any type that implements `Manager` trait but is limited to only using `listen_any/once_any`. + +## Listen functions + +- `listen`: Listens to all events targeting this listener type only. +- `once`: Listens to a single event targeting this listener type only. +- `listen_any` (available only through `Manager` trait): Listens to all events to any target (aka event sniffer). +- `once_any` (available only through `Manager` trait): Listens to a single event to any target (aka event sniffer). diff --git a/crates/tauri/src/event/event_name.rs b/crates/tauri/src/event/event_name.rs new file mode 100644 index 0000000..3b553b6 --- /dev/null +++ b/crates/tauri/src/event/event_name.rs @@ -0,0 +1,70 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Deserialize, Deserializer}; + +/// Checks if an event name is valid. +fn is_event_name_valid(event: &str) -> bool { + event + .chars() + .all(|c| c.is_alphanumeric() || c == '-' || c == '/' || c == ':' || c == '_') +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) struct EventName(S); + +impl Copy for EventName<&str> {} + +impl> EventName { + pub(crate) fn new(s: S) -> crate::Result> { + if !is_event_name_valid(s.as_ref()) { + return Err(crate::Error::IllegalEventName(s.as_ref().to_string())); + } + Ok(EventName(s)) + } + + pub(crate) fn as_str_event(&self) -> EventName<&str> { + EventName(self.0.as_ref()) + } + + pub(crate) fn as_str(&self) -> &str { + self.0.as_ref() + } +} + +impl std::fmt::Display for EventName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl EventName<&'static str> { + // this convenience method is for using in const contexts to discharge the preconditions + // &'static prevents using this function accidentally with dynamically built string slices + pub(crate) const fn from_str(s: &'static str) -> EventName<&'static str> { + EventName(s) + } +} + +impl EventName<&str> { + pub fn into_owned(self) -> EventName { + EventName(self.0.to_string()) + } +} + +impl<'de> Deserialize<'de> for EventName { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let event_id = String::deserialize(deserializer)?; + if is_event_name_valid(&event_id) { + Ok(EventName(event_id)) + } else { + Err(serde::de::Error::custom( + "Event name must include only alphanumeric characters, `-`, `/`, `:` and `_`.", + )) + } + } +} diff --git a/crates/tauri/src/event/init.js b/crates/tauri/src/event/init.js new file mode 100644 index 0000000..8e80004 --- /dev/null +++ b/crates/tauri/src/event/init.js @@ -0,0 +1,10 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// eslint-disable-next-line +Object.defineProperty(window, '__TAURI_EVENT_PLUGIN_INTERNALS__', { + value: { + unregisterListener: __RAW_unregister_listener_function__ + } +}) diff --git a/crates/tauri/src/event/listener.rs b/crates/tauri/src/event/listener.rs new file mode 100644 index 0000000..d45c84c --- /dev/null +++ b/crates/tauri/src/event/listener.rs @@ -0,0 +1,488 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{Runtime, Webview}; + +use super::{EmitArgs, Event, EventId, EventTarget}; + +use std::{ + boxed::Box, + cell::Cell, + collections::{HashMap, HashSet}, + sync::{ + Arc, Mutex, + atomic::{AtomicU32, Ordering}, + }, +}; + +/// What to do with the pending handler when resolving it? +enum Pending { + Unlisten(EventId), + Listen { + id: EventId, + event: crate::EventName, + handler: Handler, + }, + Emit(EmitArgs), + RemoveForTarget(EventTarget), +} + +/// Stored in [`Listeners`] to be called upon, when the event that stored it, is triggered. +struct Handler { + target: EventTarget, + callback: Box, +} + +impl Handler { + fn new(target: EventTarget, callback: F) -> Self { + Self { + target, + callback: Box::new(callback), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct JsHandler { + target: EventTarget, + id: EventId, +} + +impl JsHandler { + fn new(target: EventTarget, id: EventId) -> Self { + Self { target, id } + } +} + +type WebviewLabel = String; + +/// Holds event handlers and pending event handlers, along with the salts associating them. +struct InnerListeners { + pending: Mutex>, + handlers: Mutex>>, + js_event_listeners: Mutex>>>, + function_name: &'static str, + listeners_object_name: &'static str, + next_event_id: Arc, +} + +/// A self-contained event manager. +#[derive(Clone)] +pub struct Listeners { + inner: Arc, +} + +impl Default for Listeners { + fn default() -> Self { + Self { + inner: Arc::new(InnerListeners { + pending: Mutex::default(), + handlers: Mutex::default(), + js_event_listeners: Mutex::default(), + function_name: "__internal_unstable_listeners_function_id__", + listeners_object_name: "__internal_unstable_listeners_object_id__", + next_event_id: Default::default(), + }), + } + } +} + +impl Listeners { + pub(crate) fn next_event_id(&self) -> EventId { + self.inner.next_event_id.fetch_add(1, Ordering::Relaxed) + } + + /// Function name to represent the JavaScript event function. + pub(crate) fn function_name(&self) -> &str { + self.inner.function_name + } + + /// Listener object name to represent the JavaScript event listener object. + pub(crate) fn listeners_object_name(&self) -> &str { + self.inner.listeners_object_name + } + + /// Insert a pending event action to the queue. + fn insert_pending(&self, action: Pending) { + self + .inner + .pending + .lock() + .expect("poisoned pending event queue") + .push(action) + } + + /// Finish all pending event actions. + fn flush_pending(&self) -> crate::Result<()> { + let pending = { + let mut lock = self + .inner + .pending + .lock() + .expect("poisoned pending event queue"); + std::mem::take(&mut *lock) + }; + + for action in pending { + match action { + Pending::Unlisten(id) => self.unlisten(id), + Pending::Listen { id, event, handler } => self.listen_with_id(id, event, handler), + Pending::Emit(args) => self.emit(args)?, + Pending::RemoveForTarget(target) => self.remove_listeners_for_target(target), + } + } + + Ok(()) + } + + fn listen_with_id(&self, id: EventId, event: crate::EventName, handler: Handler) { + match self.inner.handlers.try_lock() { + Err(_) => self.insert_pending(Pending::Listen { id, event, handler }), + Ok(mut lock) => { + lock.entry(event).or_default().insert(id, handler); + } + } + } + + /// Adds an event listener. + pub(crate) fn listen( + &self, + event: crate::EventName, + target: EventTarget, + handler: F, + ) -> EventId { + let id = self.next_event_id(); + let handler = Handler::new(target, handler); + self.listen_with_id(id, event, handler); + id + } + + /// Listen to an event and immediately unlisten. + pub(crate) fn once( + &self, + event: crate::EventName, + target: EventTarget, + handler: F, + ) -> EventId { + let self_ = self.clone(); + let handler = Cell::new(Some(handler)); + + self.listen(event, target, move |event| { + let id = event.id; + self_.unlisten(id); + let handler = handler + .take() + .expect("attempted to call handler more than once"); + handler(event); + }) + } + + /// Removes an event listener. + pub(crate) fn unlisten(&self, id: EventId) { + match self.inner.handlers.try_lock() { + Err(_) => self.insert_pending(Pending::Unlisten(id)), + Ok(mut lock) => lock.values_mut().for_each(|handler| { + handler.remove(&id); + }), + } + } + + /// Removes every Rust-side event listener registered for the exact given target. + /// + /// Called when a window or webview is destroyed so its listeners don't leak. + /// Global targets such as [`EventTarget::App`], [`EventTarget::Any`] and [`EventTarget::AnyLabel`] are never matched here. + fn remove_listeners_for_target(&self, target: EventTarget) { + match self.inner.handlers.try_lock() { + Err(_) => self.insert_pending(Pending::RemoveForTarget(target)), + Ok(mut lock) => { + for handlers in lock.values_mut() { + handlers.retain(|_, handler| handler.target != target); + } + lock.retain(|_, handlers| !handlers.is_empty()); + } + } + } + + /// Emits the given event with its payload based on a filter. + pub(crate) fn emit_filter(&self, emit_args: EmitArgs, filter: Option) -> crate::Result<()> + where + F: Fn(&EventTarget) -> bool, + { + let mut maybe_pending = false; + + match self.inner.handlers.try_lock() { + Err(_) => self.insert_pending(Pending::Emit(emit_args)), + Ok(lock) => { + if let Some(handlers) = lock.get(&emit_args.event) { + let handlers = handlers.iter(); + let handlers = handlers.filter(|(_, h)| match_any_or_filter(&h.target, &filter)); + for (&id, Handler { callback, .. }) in handlers { + maybe_pending = true; + (callback)(Event::new(id, emit_args.payload.clone())) + } + } + } + } + + if maybe_pending { + self.flush_pending()?; + } + + Ok(()) + } + + /// Emits the given event with its payload. + pub(crate) fn emit(&self, emit_args: EmitArgs) -> crate::Result<()> { + self.emit_filter(emit_args, None::<&dyn Fn(&EventTarget) -> bool>) + } + + pub(crate) fn listen_js( + &self, + event: crate::EventName<&str>, + source_webview_label: &str, + target: EventTarget, + id: EventId, + ) { + let event = event.into_owned(); + let mut listeners = self.inner.js_event_listeners.lock().unwrap(); + listeners + .entry(source_webview_label.to_string()) + .or_default() + .entry(event) + .or_default() + .insert(JsHandler::new(target, id)); + } + + pub(crate) fn unlisten_js(&self, event: crate::EventName<&str>, id: EventId) { + let event = event.into_owned(); + let mut js_listeners = self.inner.js_event_listeners.lock().unwrap(); + let js_listeners = js_listeners.values_mut(); + for js_listeners in js_listeners { + if let Some(handlers) = js_listeners.get_mut(&event) { + handlers.retain(|h| h.id != id); + + if handlers.is_empty() { + js_listeners.remove(&event); + } + } + } + } + + /// Removes all JS event listeners registered from the given webview. + fn remove_webview_js_listeners(&self, webview_label: &str) { + self + .inner + .js_event_listeners + .lock() + .unwrap() + .remove(webview_label); + } + + /// Removes all event listeners associated with a destroyed webview. + pub(crate) fn remove_webview_listeners(&self, label: &str) { + self.remove_webview_js_listeners(label); + self.remove_listeners_for_target(EventTarget::webview(label)); + } + + /// Removes all event listeners associated with a destroyed window. + pub(crate) fn remove_window_listeners(&self, label: &str) { + self.remove_listeners_for_target(EventTarget::window(label)); + self.remove_listeners_for_target(EventTarget::webview_window(label)); + } + + pub(crate) fn has_js_listener bool>( + &self, + event: crate::EventName<&str>, + filter: F, + ) -> bool { + let event = event.into_owned(); + let js_listeners = self.inner.js_event_listeners.lock().unwrap(); + js_listeners.values().any(|events| { + events + .get(&event) + .map(|handlers| handlers.iter().any(|handler| filter(&handler.target))) + .unwrap_or(false) + }) + } + + pub(crate) fn emit_js_filter<'a, R, I, F>( + &self, + mut webviews: I, + emit_args: &EmitArgs, + filter: Option, + ) -> crate::Result<()> + where + R: Runtime, + I: Iterator>, + F: Fn(&EventTarget) -> bool, + { + let event = &emit_args.event; + let js_listeners = self.inner.js_event_listeners.lock().unwrap(); + webviews.try_for_each(|webview| { + if let Some(handlers) = js_listeners.get(webview.label()).and_then(|s| s.get(event)) { + let ids = handlers + .iter() + .filter(|handler| match_any_or_filter(&handler.target, &filter)) + .map(|handler| handler.id) + .collect::>(); + webview.emit_js(emit_args, &ids)?; + } + + Ok(()) + }) + } + + pub(crate) fn emit_js<'a, R, I>(&self, webviews: I, emit_args: &EmitArgs) -> crate::Result<()> + where + R: Runtime, + I: Iterator>, + { + self.emit_js_filter(webviews, emit_args, None::<&dyn Fn(&EventTarget) -> bool>) + } +} + +#[inline(always)] +fn match_any_or_filter bool>( + target: &EventTarget, + filter: &Option, +) -> bool { + *target == EventTarget::Any || filter.as_ref().map(|f| f(target)).unwrap_or(true) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::event::EventTarget; + use proptest::prelude::*; + + // dummy event handler function + fn event_fn(s: Event) { + println!("{s:?}"); + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(10000))] + + // check to see if listen() is properly passing keys into the LISTENERS map + #[test] + fn listeners_check_key(e in "[a-z]+") { + let listeners: Listeners = Default::default(); + // clone e as the key + let key = crate::EventName::new(e).unwrap(); + // pass e and an dummy func into listen + listeners.listen(key.clone(), EventTarget::Any, event_fn); + + // lock mutex + let l = listeners.inner.handlers.lock().unwrap(); + + // check if the generated key is in the map + assert!(l.contains_key(&key)); + } + + // check to see if listen inputs a handler function properly into the LISTENERS map. + #[test] + fn listeners_check_fn(e in "[a-z]+") { + let listeners: Listeners = Default::default(); + let key = crate::EventName::new(e).unwrap(); + // pass e and an dummy func into listen + listeners.listen(key.clone(), EventTarget::Any, event_fn); + + // lock mutex + let mut l = listeners.inner.handlers.lock().unwrap(); + + // check if l contains key + if l.contains_key(&key) { + // grab key if it exists + let handler = l.get_mut(&key); + // check to see if we get back a handler or not + match handler { + // pass on Some(handler) + Some(_) => {}, + // Fail on None + None => panic!("handler is None") + } + } + } + + // check to see if on_event properly grabs the stored function from listen. + #[test] + fn check_on_event(e in "[a-z]+", d in "[a-z]+") { + let listeners: Listeners = Default::default(); + let key = crate::EventName::new(e).unwrap(); + // call listen with key and the event_fn dummy func + listeners.listen(key.clone(), EventTarget::Any, event_fn); + // call on event with key and d. + listeners.emit(EmitArgs::new(key.as_str_event(), &d)?)?; + + // lock the mutex + let l = listeners.inner.handlers.lock().unwrap(); + + // assert that the key is contained in the listeners map + assert!(l.contains_key(&key)); + } + } + + #[test] + fn event_no_deadlocks() { + let listeners = Listeners::default(); + let listeners_clone = listeners.clone(); + let event = crate::EventName::new("test-event".to_owned()).unwrap(); + let event_clone = event.clone(); + listeners.once(event.clone(), EventTarget::Any, move |_event| { + listeners_clone + .emit(EmitArgs::new(event_clone.as_str_event(), &()).unwrap()) + .unwrap(); + }); + listeners + .emit(EmitArgs::new(event.as_str_event(), &()).unwrap()) + .unwrap(); + } + + #[test] + fn listeners_removed_on_webview_close() { + let listeners = Listeners::default(); + let event = crate::EventName::new("test-event".to_owned()).unwrap(); + let webview_label = "main"; + + let window = listeners.listen(event.clone(), EventTarget::window(webview_label), event_fn); + // Same label as the window, but a different target kind (multi-webview mode). + let webview = listeners.listen(event.clone(), EventTarget::webview(webview_label), event_fn); + let webview_window = listeners.listen( + event.clone(), + EventTarget::webview_window(webview_label), + event_fn, + ); + let other = listeners.listen(event.clone(), EventTarget::webview("other"), event_fn); + let app = listeners.listen(event.clone(), EventTarget::App, event_fn); + let any = listeners.listen(event.clone(), EventTarget::Any, event_fn); + let labeled = listeners.listen(event.clone(), EventTarget::labeled(webview_label), event_fn); + + let id = listeners.next_event_id(); + listeners.listen_js( + event.as_str_event(), + webview_label, + EventTarget::webview(webview_label), + id, + ); + assert!(listeners.has_js_listener(event.as_str_event(), |_| true)); + + listeners.remove_webview_listeners(webview_label); + + assert!(!listeners.has_js_listener(event.as_str_event(), |_| true)); + + let lock = listeners.inner.handlers.lock().unwrap(); + let remaining: HashSet = lock.get(&event).unwrap().keys().copied().collect(); + + // only the exact `Webview { label: "main" }` listener is gone + assert!(!remaining.contains(&webview)); + // same label but different kind is kept + assert!(remaining.contains(&window)); + assert!(remaining.contains(&webview_window)); + // unrelated and global listeners are kept + assert!(remaining.contains(&other)); + assert!(remaining.contains(&app)); + assert!(remaining.contains(&any)); + assert!(remaining.contains(&labeled)); + } +} diff --git a/crates/tauri/src/event/mod.rs b/crates/tauri/src/event/mod.rs new file mode 100644 index 0000000..05dfd04 --- /dev/null +++ b/crates/tauri/src/event/mod.rs @@ -0,0 +1,258 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +mod listener; +pub(crate) mod plugin; +use std::{convert::Infallible, str::FromStr}; + +pub(crate) use listener::Listeners; +use serde::{Deserialize, Serialize}; + +mod event_name; + +pub(crate) use event_name::EventName; + +use crate::ipc::CallbackFn; + +/// Unique id of an event. +pub type EventId = u32; + +/// Event Target +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +#[serde(tag = "kind")] +#[non_exhaustive] +pub enum EventTarget { + /// Any and all event targets. + Any, + + /// Any [`Window`](crate::Window), [`Webview`](crate::Webview) or [`WebviewWindow`](crate::WebviewWindow) that have this label. + AnyLabel { + /// Target label. + label: String, + }, + + /// [`App`](crate::App) and [`AppHandle`](crate::AppHandle) targets. + App, + + /// [`Window`](crate::Window) target. + Window { + /// window label. + label: String, + }, + + /// [`Webview`](crate::Webview) target. + Webview { + /// webview label. + label: String, + }, + + /// [`WebviewWindow`](crate::WebviewWindow) target. + WebviewWindow { + /// webview window label. + label: String, + }, +} + +impl EventTarget { + /// [`Self::Any`] target. + pub fn any() -> Self { + Self::Any + } + + /// [`Self::App`] target. + pub fn app() -> Self { + Self::App + } + + /// [`Self::AnyLabel`] target. + pub fn labeled(label: impl Into) -> Self { + Self::AnyLabel { + label: label.into(), + } + } + + /// [`Self::Window`] target. + pub fn window(label: impl Into) -> Self { + Self::Window { + label: label.into(), + } + } + + /// [`Self::Webview`] target. + pub fn webview(label: impl Into) -> Self { + Self::Webview { + label: label.into(), + } + } + + /// [`Self::WebviewWindow`] target. + pub fn webview_window(label: impl Into) -> Self { + Self::WebviewWindow { + label: label.into(), + } + } +} + +impl> From for EventTarget { + fn from(value: T) -> Self { + Self::AnyLabel { + label: value.as_ref().to_string(), + } + } +} + +impl FromStr for EventTarget { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self::AnyLabel { + label: s.to_string(), + }) + } +} + +/// Serialized emit arguments. +#[derive(Clone)] +pub struct EmitArgs { + /// event name. + event: EventName, + /// Serialized payload. + payload: String, +} + +impl EmitArgs { + pub fn new(event: EventName<&str>, payload: &S) -> crate::Result { + #[cfg(feature = "tracing")] + let _span = tracing::debug_span!("window::emit::serialize").entered(); + Ok(EmitArgs { + event: event.into_owned(), + payload: serde_json::to_string(payload)?, + }) + } + + pub fn new_str(event: EventName<&str>, payload: String) -> crate::Result { + #[cfg(feature = "tracing")] + let _span = tracing::debug_span!("window::emit::json").entered(); + Ok(EmitArgs { + event: event.into_owned(), + payload, + }) + } +} + +/// An event that was emitted. +#[derive(Debug, Clone)] +pub struct Event { + id: EventId, + data: String, +} + +impl Event { + fn new(id: EventId, data: String) -> Self { + Self { id, data } + } + + /// The [`EventId`] of the handler that was triggered. + pub fn id(&self) -> EventId { + self.id + } + + /// The event payload. + pub fn payload(&self) -> &str { + &self.data + } +} + +pub(crate) fn listen_js_script( + listeners_object_name: &str, + serialized_target: &str, + event: EventName<&str>, + event_id: EventId, + handler: CallbackFn, +) -> String { + let handler_id = handler.0; + format!( + "(function () {{ + if (window['{listeners_object_name}'] === void 0) {{ + Object.defineProperty(window, '{listeners_object_name}', {{ value: Object.create(null) }}); + }} + if (window['{listeners_object_name}']['{event}'] === void 0) {{ + Object.defineProperty(window['{listeners_object_name}'], '{event}', {{ value: Object.create(null) }}); + }} + const eventListeners = window['{listeners_object_name}']['{event}'] + const listener = {{ + target: {serialized_target}, + handlerId: {handler_id} + }}; + Object.defineProperty(eventListeners, '{event_id}', {{ value: listener, configurable: true }}); + }})() + ", + ) +} + +pub(crate) fn emit_js_script( + event_emit_function_name: &str, + emit_args: &EmitArgs, + serialized_ids: &str, +) -> crate::Result { + Ok(format!( + "(function () {{ const fn = window['{}']; fn && fn({{event: '{}', payload: {}}}, {ids}) }})()", + event_emit_function_name, + emit_args.event, + emit_args.payload, + ids = serialized_ids, + )) +} + +pub(crate) fn unlisten_js_script( + listeners_object_name: &str, + event_arg: &str, + event_id_arg: &str, +) -> String { + format!( + "(function () {{ + const listeners = (window['{listeners_object_name}'] || {{}})[{event_arg}] + if (listeners) {{ + const listener = listeners[{event_id_arg}] + if (listener) {{ + window.__TAURI_INTERNALS__.unregisterCallback(listener.handlerId) + }} + }} + }})() + ", + ) +} + +pub(crate) fn event_initialization_script(function_name: &str, listeners: &str) -> String { + format!( + "Object.defineProperty(window, '{function_name}', {{ + value: function (eventData, ids) {{ + const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || [] + for (const id of ids) {{ + const listener = listeners[id] + if (listener) {{ + eventData.id = id + window.__TAURI_INTERNALS__.runCallback(listener.handlerId, eventData) + }} + }} + }} + }}); + " + ) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_illegal_event_name() { + let s = EventName::new("some\r illegal event name") + .unwrap_err() + .to_string(); + assert_eq!( + "only alphanumeric, '-', '/', ':', '_' permitted for event names: \"some\\r illegal event name\"", + s + ); + } +} diff --git a/crates/tauri/src/event/plugin.rs b/crates/tauri/src/event/plugin.rs new file mode 100644 index 0000000..fdbbf58 --- /dev/null +++ b/crates/tauri/src/event/plugin.rs @@ -0,0 +1,82 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT +use serde_json::Value as JsonValue; +use serialize_to_javascript::{DefaultTemplate, Template, default_template}; + +use crate::plugin::{Builder, TauriPlugin}; +use crate::{AppHandle, Emitter, Manager, Webview}; +use crate::{EventId, Result, Runtime, command, ipc::CallbackFn}; + +use super::EventName; +use super::EventTarget; + +#[command(root = "crate")] +async fn listen( + webview: Webview, + event: EventName, + target: EventTarget, + handler: CallbackFn, +) -> Result { + webview.listen_js(event.as_str_event(), target, handler) +} + +#[command(root = "crate")] +async fn unlisten( + webview: Webview, + event: EventName, + event_id: EventId, +) -> Result<()> { + webview.unlisten_js(event.as_str_event(), event_id) +} + +#[command(root = "crate")] +async fn emit( + app: AppHandle, + event: EventName, + payload: Option, +) -> Result<()> { + app.emit(event.as_str(), payload) +} + +#[command(root = "crate")] +async fn emit_to( + app: AppHandle, + target: EventTarget, + event: EventName, + payload: Option, +) -> Result<()> { + app.emit_to(target, event.as_str(), payload) +} + +/// Initializes the event plugin. +pub(crate) fn init>(manager: &M) -> TauriPlugin { + let listeners = manager.manager().listeners(); + + #[derive(Template)] + #[default_template("./init.js")] + struct InitJavascript { + #[raw] + unregister_listener_function: String, + } + + let init_script = InitJavascript { + unregister_listener_function: format!( + "(event, eventId) => {}", + crate::event::unlisten_js_script(listeners.listeners_object_name(), "event", "eventId") + ), + }; + + Builder::new("event") + .invoke_handler(crate::generate_handler![ + #![plugin(event)] + listen, unlisten, emit, emit_to + ]) + .js_init_script( + init_script + .render_default(&Default::default()) + .unwrap() + .to_string(), + ) + .build() +} diff --git a/crates/tauri/src/image/mod.rs b/crates/tauri/src/image/mod.rs new file mode 100644 index 0000000..03cb07a --- /dev/null +++ b/crates/tauri/src/image/mod.rs @@ -0,0 +1,527 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Image types used by this crate and also referenced by the JavaScript API layer. + +pub(crate) mod plugin; + +use std::borrow::Cow; +use std::sync::Arc; + +#[cfg(windows)] +use windows::{ + Win32::{ + Foundation::{E_FAIL, ERROR_INVALID_PARAMETER, ERROR_NOT_SUPPORTED, WIN32_ERROR}, + Graphics::Gdi::{ + BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleDC, DIB_RGB_COLORS, DeleteDC, + GetDIBits, HBITMAP, + }, + System::LibraryLoader::GetModuleHandleW, + UI::WindowsAndMessaging::{ + GetIconInfo, GetSystemMetrics, HICON, ICONINFO, IMAGE_ICON, LR_DEFAULTCOLOR, LoadImageW, + SM_CXICON, SM_CYICON, + }, + }, + core::{Owned, PCWSTR}, +}; + +use crate::{Resource, ResourceId, ResourceTable}; + +/// Identifies an icon resource embedded in the executable. +#[cfg(windows)] +#[cfg_attr(docsrs, doc(cfg(windows)))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum IconResource<'a> { + /// An integer resource identifier (`MAKEINTRESOURCE`). + Id(u16), + /// A string resource name. + Name(&'a str), +} + +#[cfg(windows)] +impl From for IconResource<'_> { + fn from(id: u16) -> Self { + Self::Id(id) + } +} + +#[cfg(windows)] +impl<'a> From<&'a str> for IconResource<'a> { + fn from(name: &'a str) -> Self { + Self::Name(name) + } +} + +/// Loads the default window icon from the application icon resource, reporting failures. +/// +/// Used by [`crate::generate_context!`]; not public API. +#[cfg(windows)] +#[doc(hidden)] +pub fn default_window_icon_from_app_icon_resource() -> Option> { + // the window icon is drawn in the title bar and, as a fallback for the taskbar icon, + // at the system's large icon size (32x32 at 96 DPI, scaled with the system DPI), + // so pick the entry Windows would use for the taskbar instead of a larger one it has to shrink + // `GetSystemMetrics` returns 0 on failure + let metric = |index| match unsafe { GetSystemMetrics(index) } { + n if n > 0 => n as u32, + _ => 32, + }; + let (width, height) = (metric(SM_CXICON), metric(SM_CYICON)); + match Image::from_icon_resource( + crate::utils::platform::WINDOWS_APP_ICON_RESOURCE_ID, + width, + height, + ) { + Ok(icon) => Some(icon), + Err(e) => { + // a logger is usually not installed yet when `generate_context!` runs + #[cfg(debug_assertions)] + eprintln!("failed to load the default window icon from the application icon resource: {e}"); + log::warn!("failed to load the default window icon from the application icon resource: {e}"); + None + } + } +} + +#[cfg(windows)] +const BYTES_PER_PIXEL: usize = 4; + +/// Reads `hbm` as a top-down 32bpp BGRA bitmap of the given dimensions. +/// +/// # Safety +/// +/// `hbm` must be a valid bitmap handle and `width` and `height` must be positive. +#[cfg(windows)] +unsafe fn read_bgra(hbm: HBITMAP, width: i32, height: i32) -> crate::Result> { + let image_bytes = (width as usize) + .checked_mul(height as usize) + .and_then(|n| n.checked_mul(BYTES_PER_PIXEL)) + .ok_or_else(|| resource_error(ERROR_INVALID_PARAMETER, "image size overflows usize"))?; + let mut bgra = vec![0u8; image_bytes]; + + let mut bitmap_info = BITMAPINFO::default(); + bitmap_info.bmiHeader.biSize = std::mem::size_of::() as _; + bitmap_info.bmiHeader.biWidth = width; + // negative value for top-down + bitmap_info.bmiHeader.biHeight = -height; + bitmap_info.bmiHeader.biBitCount = (BYTES_PER_PIXEL * 8) as u16; + bitmap_info.bmiHeader.biPlanes = 1; + bitmap_info.bmiHeader.biCompression = BI_RGB.0; + + unsafe { + let hdc = CreateCompatibleDC(None); + let scan_lines = GetDIBits( + hdc, + hbm, + 0, + height as u32, + Some(bgra.as_mut_ptr() as _), + &mut bitmap_info, + DIB_RGB_COLORS, + ); + // capture the error before `DeleteDC` can overwrite it + let error = (scan_lines != height).then(|| { + last_error_or(&format!( + "GetDIBits copied {scan_lines} of {height} scan lines" + )) + }); + let _ = DeleteDC(hdc); + if let Some(error) = error { + return Err(crate::Error::ImageFromResource(error)); + } + } + + Ok(bgra) +} + +#[cfg(windows)] +fn resource_error(code: WIN32_ERROR, message: &str) -> crate::Error { + crate::Error::ImageFromResource(windows::core::Error::new(code.to_hresult(), message)) +} + +/// Returns the calling thread's last error, or a generic `E_FAIL` with `message` +/// when no error code was set (GDI functions do not always set one). +#[cfg(windows)] +fn last_error_or(message: &str) -> windows::core::Error { + let error = windows::core::Error::from_thread(); + if error.code().is_ok() { + windows::core::Error::new(E_FAIL, message) + } else { + error + } +} + +/// An RGBA Image in row-major order from top to bottom. +#[derive(Clone)] +pub struct Image<'a> { + rgba: Cow<'a, [u8]>, + width: u32, + height: u32, +} + +impl std::fmt::Debug for Image<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Image") + .field( + "rgba", + // Reduces the debug size compared to the derived default, as the default + // would format the raw bytes as numbers `[0, 0, 0, 0]` for 1 pixel. + // The custom format doesn't grow as much with larger images: + // `Image { rgba: Cow::Borrowed([u8; 4096]), width: 32, height: 32 }` + &format_args!( + "Cow::{}([u8; {}])", + match &self.rgba { + Cow::Borrowed(_) => "Borrowed", + Cow::Owned(_) => "Owned", + }, + self.rgba.len() + ), + ) + .field("width", &self.width) + .field("height", &self.height) + .finish() + } +} + +impl Resource for Image<'static> {} + +impl Image<'static> { + /// Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height. + /// + /// Similar to [`Self::new`] but avoids cloning the rgba data to get an owned Image. + pub const fn new_owned(rgba: Vec, width: u32, height: u32) -> Self { + Self { + rgba: Cow::Owned(rgba), + width, + height, + } + } +} + +impl<'a> Image<'a> { + /// Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height. + pub const fn new(rgba: &'a [u8], width: u32, height: u32) -> Self { + Self { + rgba: Cow::Borrowed(rgba), + width, + height, + } + } + + /// Creates a new image using the provided bytes. + /// + /// Only `ico` and `png` are supported (based on activated feature flag). + #[cfg(any(feature = "image-ico", feature = "image-png"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "image-ico", feature = "image-png"))))] + pub fn from_bytes(bytes: &[u8]) -> crate::Result { + let img = image::load_from_memory(bytes)?; + let (width, height) = (img.width(), img.height()); + Ok(Self { + rgba: Cow::Owned(img.into_rgba8().into_raw()), + width, + height, + }) + } + + /// Creates a new image using the provided path. + /// + /// Only `ico` and `png` are supported (based on activated feature flag). + #[cfg(any(feature = "image-ico", feature = "image-png"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "image-ico", feature = "image-png"))))] + pub fn from_path>(path: P) -> crate::Result { + let bytes = std::fs::read(path)?; + Self::from_bytes(&bytes) + } + + /// Creates a new image from the application icon embedded in the executable of the current process. + /// + /// The application icon is the one `tauri-build` embeds with the + /// [`WINDOWS_APP_ICON_RESOURCE_ID`](crate::utils::platform::WINDOWS_APP_ICON_RESOURCE_ID) id, + /// this could change in the future. + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + pub fn from_app_icon_resource(size: u32) -> crate::Result { + Image::from_icon_resource( + crate::utils::platform::WINDOWS_APP_ICON_RESOURCE_ID, + size, + size, + ) + } + + /// Create a new image from an icon resource embedded in the executable of the current process. + /// + /// Resources are looked up in the process executable (`GetModuleHandleW(NULL)`), + /// not in the DLL containing this code when tauri is built as a library. + /// + /// **Note**: This might take ~2ms for [`LoadImageW`] to load the image for the first time. + /// + /// ## Examples + /// + /// The resource can be identified by its integer id or by its name, see [`IconResource`]. + /// + /// ```no_run + /// # use tauri::image::Image; + /// # fn main() -> tauri::Result<()> { + /// let icon = Image::from_icon_resource(1, 32, 32)?; + /// let icon = Image::from_icon_resource("icon", 32, 32)?; + /// # Ok(()) + /// # } + /// ``` + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + pub fn from_icon_resource<'r>( + resource: impl Into>, + width: u32, + height: u32, + ) -> crate::Result { + let (width_i32, height_i32) = match (i32::try_from(width), i32::try_from(height)) { + (Ok(w), Ok(h)) if w > 0 && h > 0 => (w, h), + _ => { + return Err(resource_error( + ERROR_INVALID_PARAMETER, + "width and height must be between 1 and i32::MAX", + )); + } + }; + + // keeps the wide string alive for the `LoadImageW` call + let name: Vec; + let resource_id = match resource.into() { + // MAKEINTRESOURCE + IconResource::Id(id) => PCWSTR(id as usize as *const u16), + IconResource::Name(n) => { + name = n.encode_utf16().chain(std::iter::once(0)).collect(); + PCWSTR(name.as_ptr()) + } + }; + + let hicon = unsafe { + Owned::new(HICON( + LoadImageW( + Some( + GetModuleHandleW(PCWSTR::null()) + .map_err(crate::Error::ImageFromResource)? + .into(), + ), + resource_id, + IMAGE_ICON, + width_i32, + height_i32, + LR_DEFAULTCOLOR, + ) + .map_err(crate::Error::ImageFromResource)? + .0, + )) + }; + + let mut icon_info = ICONINFO::default(); + unsafe { GetIconInfo(*hicon, &mut icon_info).map_err(crate::Error::ImageFromResource)? }; + let hbm_mask = unsafe { Owned::new(icon_info.hbmMask) }; + let hbm_color = unsafe { Owned::new(icon_info.hbmColor) }; + + // monochrome icons only have a mask bitmap (AND mask stacked on top of the XOR mask) + if hbm_color.is_invalid() { + return Err(resource_error( + ERROR_NOT_SUPPORTED, + "monochrome icons are not supported", + )); + } + + let mut bgra = unsafe { read_bgra(*hbm_color, width_i32, height_i32)? }; + + // Color bitmaps without an alpha channel (e.g. 24bpp icons) read back with alpha = 0 on every pixel, + // so recover the alpha channel from the AND mask: a set bit means the pixel is transparent. + if bgra + .as_chunks::() + .0 + .iter() + .all(|px| px[3] == 0) + { + let mask = unsafe { read_bgra(*hbm_mask, width_i32, height_i32)? }; + for (px, mask) in bgra + .as_chunks_mut::() + .0 + .iter_mut() + .zip(mask.as_chunks::().0) + { + // the 1bpp mask expands to black (clear bit) or white (set bit) + px[3] = if mask[0] == 0 { 0xFF } else { 0 }; + } + } + + let rgba = { + for px in bgra.as_chunks_mut::().0 { + // Swap Blue and Red channels + px.swap(0, 2); + } + bgra + }; + + Ok(Image::new_owned(rgba, width, height)) + } + + /// Returns the RGBA data for this image, in row-major order from top to bottom. + pub fn rgba(&'a self) -> &'a [u8] { + &self.rgba + } + + /// Returns the width of this image. + pub fn width(&self) -> u32 { + self.width + } + + /// Returns the height of this image. + pub fn height(&self) -> u32 { + self.height + } + + /// Convert into a 'static owned [`Image`]. + /// This will allocate. + pub fn to_owned(self) -> Image<'static> { + Image { + rgba: match self.rgba { + Cow::Owned(v) => Cow::Owned(v), + Cow::Borrowed(v) => Cow::Owned(v.to_vec()), + }, + height: self.height, + width: self.width, + } + } +} + +impl<'a> From> for crate::runtime::Icon<'a> { + fn from(img: Image<'a>) -> Self { + Self { + rgba: img.rgba, + width: img.width, + height: img.height, + } + } +} + +#[cfg(desktop)] +impl TryFrom> for muda::Icon { + type Error = crate::Error; + + fn try_from(img: Image<'_>) -> Result { + muda::Icon::from_rgba(img.rgba.into_owned(), img.width, img.height).map_err(Into::into) + } +} + +#[cfg(all(desktop, feature = "tray-icon"))] +impl TryFrom> for tray_icon::Icon { + type Error = crate::Error; + + fn try_from(img: Image<'_>) -> Result { + tray_icon::Icon::from_rgba(img.rgba.into_owned(), img.width, img.height).map_err(Into::into) + } +} + +/// An image type that accepts file paths, raw bytes, previously loaded images and image objects. +/// +/// This type is meant to be used along the [transformImage](https://v2.tauri.app/reference/javascript/api/namespaceimage/#transformimage) API. +/// +/// # Stability +/// +/// The stability of the variants are not guaranteed, and matching against them is not recommended. +/// Use [`JsImage::into_img`] instead. +#[derive(serde::Deserialize)] +#[serde(untagged)] +#[non_exhaustive] +pub enum JsImage { + /// A reference to an image in the filesystem. This requires `image-ico` or `image-png` cargo features. + #[non_exhaustive] + Path(std::path::PathBuf), + /// ICO or PNG image in raw bytes. This requires `image-ico` or `image-png` cargo features. + #[non_exhaustive] + Bytes(Vec), + /// An image that was previously loaded with the API and is stored in the resource table. + #[non_exhaustive] + Resource(ResourceId), + /// Raw RGBA definition of an image. + #[non_exhaustive] + Rgba { + /// Image bytes. + rgba: Vec, + /// Image width. + width: u32, + /// Image height. + height: u32, + }, +} + +impl JsImage { + /// Converts this intermediate image format into an actual [`Image`]. + /// + /// 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. + pub fn into_img(self, resources_table: &ResourceTable) -> crate::Result>> { + match self { + Self::Resource(rid) => resources_table.get::>(rid), + #[cfg(any(feature = "image-ico", feature = "image-png"))] + Self::Path(path) => Image::from_path(path).map(Arc::new), + + #[cfg(any(feature = "image-ico", feature = "image-png"))] + Self::Bytes(bytes) => Image::from_bytes(&bytes).map(Arc::new), + + Self::Rgba { + rgba, + width, + height, + } => Ok(Arc::new(Image::new_owned(rgba, width, height))), + + #[cfg(not(any(feature = "image-ico", feature = "image-png")))] + _ => Err( + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "expected RGBA image data, found {}", + match self { + JsImage::Path(_) => "a file path", + JsImage::Bytes(_) => "raw bytes", + _ => unreachable!(), + } + ), + ) + .into(), + ), + } + } +} + +#[cfg(all(test, windows))] +mod tests { + use super::{IconResource, Image, default_window_icon_from_app_icon_resource}; + + /// The test executable has no icon resources, so every lookup must fail with an error + /// (instead of panicking or returning a stretched placeholder). + #[test] + fn from_icon_resource_missing_resource_is_an_error() { + for resource in [ + IconResource::Id(u16::MAX), + IconResource::Name("tauri-image-test-missing-icon"), + ] { + let error = Image::from_icon_resource(resource, 32, 32).unwrap_err(); + assert!( + matches!(error, crate::Error::ImageFromResource(_)), + "{resource:?}: {error:?}" + ); + } + + assert!(default_window_icon_from_app_icon_resource().is_none()); + } + + #[test] + fn from_icon_resource_rejects_invalid_sizes() { + for (width, height) in [(0, 32), (32, 0), (u32::MAX, 32), (32, i32::MAX as u32 + 1)] { + let error = Image::from_icon_resource(1, width, height).unwrap_err(); + assert!( + matches!(error, crate::Error::ImageFromResource(_)), + "{width}x{height}: {error:?}" + ); + } + } +} diff --git a/crates/tauri/src/image/plugin.rs b/crates/tauri/src/image/plugin.rs new file mode 100644 index 0000000..87ac6e7 --- /dev/null +++ b/crates/tauri/src/image/plugin.rs @@ -0,0 +1,88 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Serialize; + +use crate::Manager; +use crate::plugin::{Builder, TauriPlugin}; +use crate::{ResourceId, Runtime, Webview, command, image::Image}; + +#[command(root = "crate")] +fn new( + webview: Webview, + rgba: Vec, + width: u32, + height: u32, +) -> crate::Result { + let image = Image::new_owned(rgba, width, height); + let mut resources_table = webview.resources_table(); + let rid = resources_table.add(image); + Ok(rid) +} + +#[cfg(any(feature = "image-ico", feature = "image-png"))] +#[command(root = "crate")] +fn from_bytes(webview: Webview, bytes: Vec) -> crate::Result { + let image = Image::from_bytes(&bytes)?.to_owned(); + let mut resources_table = webview.resources_table(); + let rid = resources_table.add(image); + Ok(rid) +} + +#[cfg(not(any(feature = "image-ico", feature = "image-png")))] +#[command(root = "crate")] +fn from_bytes() -> std::result::Result<(), &'static str> { + Err("from_bytes is only supported if the `image-ico` or `image-png` Cargo features are enabled") +} + +#[cfg(any(feature = "image-ico", feature = "image-png"))] +#[command(root = "crate")] +fn from_path( + webview: Webview, + path: std::path::PathBuf, +) -> crate::Result { + let image = Image::from_path(path)?.to_owned(); + let mut resources_table = webview.resources_table(); + let rid = resources_table.add(image); + Ok(rid) +} + +#[cfg(not(any(feature = "image-ico", feature = "image-png")))] +#[command(root = "crate")] +fn from_path() -> std::result::Result<(), &'static str> { + Err("from_path is only supported if the `image-ico` or `image-png` Cargo features are enabled") +} + +#[command(root = "crate")] +fn rgba(webview: Webview, rid: ResourceId) -> crate::Result { + let resources_table = webview.resources_table(); + let image = resources_table.get::>(rid)?; + Ok(crate::ipc::Response::new(image.rgba().to_owned())) +} + +#[derive(Serialize)] +struct Size { + width: u32, + height: u32, +} + +#[command(root = "crate")] +fn size(webview: Webview, rid: ResourceId) -> crate::Result { + let resources_table = webview.resources_table(); + let image = resources_table.get::>(rid)?; + Ok(Size { + width: image.width(), + height: image.height(), + }) +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + Builder::new("image") + .invoke_handler(crate::generate_handler![ + #![plugin(image)] + new, from_bytes, from_path, rgba, size + ]) + .build() +} diff --git a/crates/tauri/src/ios.rs b/crates/tauri/src/ios.rs new file mode 100644 index 0000000..bee7d03 --- /dev/null +++ b/crates/tauri/src/ios.rs @@ -0,0 +1,49 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use swift_rs::{SRString, SwiftArg, swift}; + +use std::{ + ffi::c_void, + os::raw::{c_char, c_int, c_ulonglong}, +}; + +type PluginMessageCallbackFn = unsafe extern "C" fn(c_int, c_int, *const c_char); +pub struct PluginMessageCallback(pub PluginMessageCallbackFn); + +impl<'a> SwiftArg<'a> for PluginMessageCallback { + type ArgType = PluginMessageCallbackFn; + + unsafe fn as_arg(&'a self) -> Self::ArgType { + self.0 + } +} + +type ChannelSendDataCallbackFn = unsafe extern "C" fn(c_ulonglong, *const c_char); +pub struct ChannelSendDataCallback(pub ChannelSendDataCallbackFn); + +impl<'a> SwiftArg<'a> for ChannelSendDataCallback { + type ArgType = ChannelSendDataCallbackFn; + + unsafe fn as_arg(&'a self) -> Self::ArgType { + self.0 + } +} + +swift!(pub fn run_plugin_command( + id: i32, + name: &SRString, + method: &SRString, + data: &SRString, + callback: PluginMessageCallback, + send_channel_data_callback: ChannelSendDataCallback +)); +swift!(pub fn register_plugin( + name: &SRString, + plugin: *const c_void, + config: &SRString, + webview: *const c_void +)); +swift!(pub fn on_webview_created(webview: *const c_void, controller: *const c_void)); +swift!(pub fn log_stdout()); diff --git a/crates/tauri/src/ipc/authority.rs b/crates/tauri/src/ipc/authority.rs new file mode 100644 index 0000000..b05165b --- /dev/null +++ b/crates/tauri/src/ipc/authority.rs @@ -0,0 +1,1485 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::collections::BTreeMap; +use std::fmt::{Debug, Display}; +use std::sync::Arc; + +use serde::de::DeserializeOwned; + +#[cfg(feature = "dynamic-acl")] +use tauri_utils::acl::capability::CapabilityFile; +#[cfg(any(feature = "dynamic-acl", debug_assertions))] +use tauri_utils::acl::manifest::Manifest; +use tauri_utils::acl::{ + APP_ACL_KEY, ExecutionContext, Value, + resolved::{Resolved, ResolvedCommand, ResolvedScope, ScopeKey}, +}; + +use url::Url; + +use crate::{AppHandle, Manager, StateManager, Webview}; +use crate::{Runtime, ipc::InvokeError, sealed::ManagerBase}; + +use super::{CommandArg, CommandItem}; + +/// The runtime authority used to authorize IPC execution based on the Access Control List. +pub struct RuntimeAuthority { + #[cfg(any(feature = "dynamic-acl", debug_assertions))] + acl: BTreeMap, + has_app_acl: bool, + allowed_commands: BTreeMap>, + denied_commands: BTreeMap>, + pub(crate) scope_manager: ScopeManager, +} + +/// The origin trying to access the IPC. +pub enum Origin { + /// Local app origin. + Local, + /// Remote origin. + Remote { + /// Remote URL. + url: Url, + }, +} + +impl Display for Origin { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Local => write!(f, "local"), + Self::Remote { url } => write!(f, "remote: {url}"), + } + } +} + +impl Origin { + fn matches(&self, context: &ExecutionContext) -> bool { + match (self, context) { + (Self::Local, ExecutionContext::Local) => true, + (Self::Remote { url }, ExecutionContext::Remote { url: url_pattern }) => { + url_pattern.test(url) + } + _ => false, + } + } +} + +/// This is used internally by [`crate::generate_handler!`] for constructing [`RuntimeAuthority`] +/// to only include the raw ACL when it's needed +/// +/// ## Stability +/// +/// The output of this macro is managed internally by Tauri, +/// and should not be accessed directly on normal applications. +/// It may have breaking changes in the future. +#[cfg(any(feature = "dynamic-acl", debug_assertions))] +#[doc(hidden)] +#[macro_export] +macro_rules! runtime_authority { + ($acl:expr, $resolved_acl:expr) => { + $crate::ipc::RuntimeAuthority::new($acl, $resolved_acl) + }; +} + +/// This is used internally by [`crate::generate_handler!`] for constructing [`RuntimeAuthority`] +/// to only include the raw ACL when it's needed +/// +/// ## Stability +/// +/// The output of this macro is managed internally by Tauri, +/// and should not be accessed directly on normal applications. +/// It may have breaking changes in the future. +#[cfg(not(any(feature = "dynamic-acl", debug_assertions)))] +#[doc(hidden)] +#[macro_export] +macro_rules! runtime_authority { + ($_acl:expr, $resolved_acl:expr) => { + $crate::ipc::RuntimeAuthority::new($resolved_acl) + }; +} + +impl RuntimeAuthority { + /// Construct a new [`RuntimeAuthority`] from the ACL + /// + /// **Please prefer using the [`runtime_authority`] macro instead of calling this directly** + #[doc(hidden)] + pub fn new( + #[cfg(any(feature = "dynamic-acl", debug_assertions))] acl: BTreeMap, + resolved_acl: Resolved, + ) -> Self { + let command_cache = resolved_acl + .command_scope + .keys() + .map(|key| (*key, StateManager::new())) + .collect(); + Self { + #[cfg(any(feature = "dynamic-acl", debug_assertions))] + acl, + has_app_acl: resolved_acl.has_app_acl, + allowed_commands: resolved_acl.allowed_commands, + denied_commands: resolved_acl.denied_commands, + scope_manager: ScopeManager { + command_scope: resolved_acl.command_scope, + global_scope: resolved_acl.global_scope, + command_cache, + global_scope_cache: StateManager::new(), + }, + } + } + + pub(crate) fn has_app_manifest(&self) -> bool { + self.has_app_acl + } + + #[doc(hidden)] + pub fn __allow_command(&mut self, command: String, context: ExecutionContext) { + self.allowed_commands.insert( + command, + vec![ResolvedCommand { + context, + windows: vec!["*".parse().unwrap()], + ..Default::default() + }], + ); + } + + /// Adds the given capability to the runtime authority. + #[cfg(feature = "dynamic-acl")] + pub fn add_capability(&mut self, capability: impl super::RuntimeCapability) -> crate::Result<()> { + self.add_capability_inner(capability.build()) + } + + #[cfg(feature = "dynamic-acl")] + fn add_capability_inner(&mut self, capability: CapabilityFile) -> crate::Result<()> { + let mut capabilities = BTreeMap::new(); + match capability { + CapabilityFile::Capability(c) => { + capabilities.insert(c.identifier.clone(), c); + } + + CapabilityFile::List(capabilities_list) + | CapabilityFile::NamedList { + capabilities: capabilities_list, + } => { + capabilities.extend( + capabilities_list + .into_iter() + .map(|c| (c.identifier.clone(), c)), + ); + } + } + + let resolved = Resolved::resolve( + &self.acl, + capabilities, + tauri_utils::platform::Target::current(), + ) + .unwrap(); + + // fill global scope + for (plugin, global_scope) in resolved.global_scope { + let global_scope_entry = self.scope_manager.global_scope.entry(plugin).or_default(); + + global_scope_entry.allow.extend(global_scope.allow); + global_scope_entry.deny.extend(global_scope.deny); + + self.scope_manager.global_scope_cache = StateManager::new(); + } + + // denied commands + for (cmd_key, resolved_cmds) in resolved.denied_commands { + let entry = self.denied_commands.entry(cmd_key).or_default(); + entry.extend(resolved_cmds); + } + + // allowed commands + for (cmd_key, resolved_cmds) in resolved.allowed_commands { + // fill command scope + for resolved_cmd in &resolved_cmds { + if let Some(scope_id) = resolved_cmd.scope_id { + let command_scope = resolved.command_scope.get(&scope_id).unwrap(); + + let command_scope_entry = self + .scope_manager + .command_scope + .entry(scope_id) + .or_default(); + command_scope_entry + .allow + .extend(command_scope.allow.clone()); + command_scope_entry.deny.extend(command_scope.deny.clone()); + + self + .scope_manager + .command_cache + .insert(scope_id, StateManager::new()); + } + } + + let entry = self.allowed_commands.entry(cmd_key).or_default(); + entry.extend(resolved_cmds); + } + + Ok(()) + } + + #[cfg(debug_assertions)] + pub(crate) fn resolve_access_message( + &self, + key: &str, + command_name: &str, + window: &str, + webview: &str, + origin: &Origin, + ) -> String { + fn print_references(resolved: &[ResolvedCommand]) -> String { + resolved + .iter() + .map(|r| { + format!( + "capability: {}, permission: {}", + r.referenced_by.capability, r.referenced_by.permission + ) + }) + .collect::>() + .join(" || ") + } + + fn print_allowed_on(resolved: &[ResolvedCommand]) -> String { + if resolved.is_empty() { + "command not allowed on any window/webview/URL context".to_string() + } else { + let mut s = "allowed on: ".to_string(); + + let last_index = resolved.len() - 1; + for (index, cmd) in resolved.iter().enumerate() { + let windows = cmd + .windows + .iter() + .map(|w| format!("\"{}\"", w.as_str())) + .collect::>() + .join(", "); + let webviews = cmd + .webviews + .iter() + .map(|w| format!("\"{}\"", w.as_str())) + .collect::>() + .join(", "); + + s.push('['); + + if !windows.is_empty() { + s.push_str(&format!("windows: {windows}, ")); + } + + if !webviews.is_empty() { + s.push_str(&format!("webviews: {webviews}, ")); + } + + match &cmd.context { + ExecutionContext::Local => s.push_str("URL: local"), + ExecutionContext::Remote { url } => s.push_str(&format!("URL: {}", url.as_str())), + } + + s.push(']'); + + if index != last_index { + s.push_str(", "); + } + } + + s + } + } + + fn has_permissions_allowing_command( + manifest: &Manifest, + set: &crate::utils::acl::PermissionSet, + command: &str, + ) -> bool { + for permission_id in &set.permissions { + if permission_id == "default" { + if let Some(default) = &manifest.default_permission { + if has_permissions_allowing_command(manifest, default, command) { + return true; + } + } + } else if let Some(ref_set) = manifest.permission_sets.get(permission_id) { + if has_permissions_allowing_command(manifest, ref_set, command) { + return true; + } + } else if let Some(permission) = manifest.permissions.get(permission_id) { + if permission.commands.allow.contains(&command.into()) { + return true; + } + } + } + false + } + + let command = if key == APP_ACL_KEY { + command_name.to_string() + } else { + format!("plugin:{key}|{command_name}") + }; + + let command_pretty_name = if key == APP_ACL_KEY { + command_name.to_string() + } else { + format!("{key}.{command_name}") + }; + + let denied_on_origin = self + .denied_on(&command, origin) + .cloned() + .collect::>(); + + if !denied_on_origin.is_empty() { + format!( + "{command_pretty_name} explicitly denied on origin {origin}\n\nreferenced by: {}", + print_references(&denied_on_origin) + ) + } else { + let command_matches = self.allowed_commands.get(&command); + + if let Some(resolved) = self.allowed_commands.get(&command) { + let resolved_matching_origin = resolved + .iter() + .filter(|cmd| origin.matches(&cmd.context)) + .collect::>(); + if resolved_matching_origin + .iter() + .any(|cmd| cmd.webviews.iter().any(|w| w.matches(webview))) + || resolved_matching_origin + .iter() + .any(|cmd| cmd.windows.iter().any(|w| w.matches(window))) + { + "allowed".to_string() + } else { + format!( + "{command_pretty_name} not allowed on window \"{window}\", webview \"{webview}\", URL: {}\n\n{}\n\nreferenced by: {}", + match origin { + Origin::Local => "local", + Origin::Remote { url } => url.as_str(), + }, + print_allowed_on(resolved), + print_references(resolved) + ) + } + } else { + let permission_error_detail = if let Some((key, manifest)) = self + .acl + .get_key_value(key) + .or_else(|| self.acl.get_key_value(&format!("core:{key}"))) + { + let mut permissions_referencing_command = Vec::new(); + + if let Some(default) = &manifest.default_permission { + if has_permissions_allowing_command(manifest, default, command_name) { + permissions_referencing_command.push("default".into()); + } + } + for set in manifest.permission_sets.values() { + if has_permissions_allowing_command(manifest, set, command_name) { + permissions_referencing_command.push(set.identifier.clone()); + } + } + for permission in manifest.permissions.values() { + if permission.commands.allow.contains(&command_name.into()) { + permissions_referencing_command.push(permission.identifier.clone()); + } + } + + permissions_referencing_command.sort(); + + let associated_permissions = permissions_referencing_command + .into_iter() + .map(|permission| { + if key == APP_ACL_KEY { + permission + } else { + format!("{key}:{permission}") + } + }) + .collect::>() + .join(", "); + + if associated_permissions.is_empty() { + "Command not found".to_string() + } else { + format!("Permissions associated with this command: {associated_permissions}") + } + } else { + "Plugin not found".to_string() + }; + + if let Some(resolved_cmds) = command_matches { + format!( + "{command_pretty_name} not allowed on origin [{origin}]. Please create a capability that has this origin on the context field.\n\nFound matches for: {}\n\n{permission_error_detail}", + resolved_cmds + .iter() + .map(|resolved| { + let context = match &resolved.context { + ExecutionContext::Local => "[local]".to_string(), + ExecutionContext::Remote { url } => format!("[remote: {}]", url.as_str()), + }; + format!( + "- context: {context}, referenced by: capability: {}, permission: {}", + resolved.referenced_by.capability, resolved.referenced_by.permission + ) + }) + .collect::>() + .join("\n") + ) + } else { + format!("{command_pretty_name} not allowed. {permission_error_detail}") + } + } + } + } + + /// Returns the deny entries of the given command whose execution context matches the given origin. + /// + /// Deny entries carry the [`ExecutionContext`] of the capability that defined them, + /// so a command denied by a remote capability is not denied for the local app and vice-versa. + fn denied_on<'a>( + &'a self, + command: &str, + origin: &'a Origin, + ) -> impl Iterator + use<'a> { + self + .denied_commands + .get(command) + .into_iter() + .flatten() + .filter(move |cmd| origin.matches(&cmd.context)) + } + + /// Checks if the given command is explicitly denied for the given origin. + fn is_denied(&self, command: &str, origin: &Origin) -> bool { + self.denied_on(command, origin).next().is_some() + } + + /// Checks if the given IPC execution is allowed and returns the [`ResolvedCommand`] if it is. + /// + /// A command is denied when a capability that denies it matches the given origin + /// (execution context), window and webview labels are not taken into account for denial. + pub fn resolve_access( + &self, + command: &str, + window: &str, + webview: &str, + origin: &Origin, + ) -> Option> { + if self.is_denied(command, origin) { + None + } else { + self.allowed_commands.get(command).and_then(|resolved| { + let resolved_cmds = resolved + .iter() + .filter(|cmd| { + origin.matches(&cmd.context) + && (cmd.webviews.iter().any(|w| w.matches(webview)) + || cmd.windows.iter().any(|w| w.matches(window))) + }) + .cloned() + .collect::>(); + if resolved_cmds.is_empty() { + None + } else { + Some(resolved_cmds) + } + }) + } + } +} + +/// List of allowed and denied objects that match either the command-specific or plugin global scope criteria. +#[derive(Debug)] +pub struct ScopeValue { + allow: Arc>>, + deny: Arc>>, +} + +impl ScopeValue { + fn clone(&self) -> Self { + Self { + allow: self.allow.clone(), + deny: self.deny.clone(), + } + } + + /// What this access scope allows. + pub fn allows(&self) -> &Vec> { + &self.allow + } + + /// What this access scope denies. + pub fn denies(&self) -> &Vec> { + &self.deny + } +} + +/// Access scope for a command that can be retrieved directly in the command function. +#[derive(Debug)] +pub struct CommandScope { + allow: Vec>, + deny: Vec>, +} + +impl CommandScope { + pub(crate) fn resolve( + webview: &Webview, + scope_ids: Vec, + ) -> crate::Result { + let mut allow = Vec::new(); + let mut deny = Vec::new(); + + for scope_id in scope_ids { + let scope = webview + .manager() + .runtime_authority + .lock() + .unwrap() + .scope_manager + .get_command_scope_typed::(webview.app_handle(), &scope_id)?; + + for s in scope.allows() { + allow.push(s.clone()); + } + for s in scope.denies() { + deny.push(s.clone()); + } + } + + Ok(CommandScope { allow, deny }) + } + + /// What this access scope allows. + pub fn allows(&self) -> &Vec> { + &self.allow + } + + /// What this access scope denies. + pub fn denies(&self) -> &Vec> { + &self.deny + } +} + +impl CommandScope { + /// Ensure all deny scopes were not matched and any allow scopes were. + /// + /// This **WILL** return `true` if the allow scopes are empty and the deny + /// scopes did not trigger. If you require at least one allow scope, then + /// ensure the allow scopes are not empty before calling this method. + /// + /// ``` + /// # use tauri::ipc::CommandScope; + /// # fn command(scope: CommandScope<()>) -> Result<(), &'static str> { + /// if scope.allows().is_empty() { + /// return Err("you need to specify at least 1 allow scope!"); + /// } + /// # Ok(()) + /// # } + /// ``` + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use url::Url; + /// # use tauri::{ipc::{CommandScope, ScopeObjectMatch}, command}; + /// # + /// #[derive(Debug, Clone, Serialize, Deserialize)] + /// # pub struct Scope; + /// # + /// # impl ScopeObjectMatch for Scope { + /// # type Input = str; + /// # + /// # fn matches(&self, input: &str) -> bool { + /// # true + /// # } + /// # } + /// # + /// # fn do_work(_: String) -> Result { + /// # Ok("Output".into()) + /// # } + /// # + /// #[command] + /// fn my_command(scope: CommandScope, input: String) -> Result { + /// if scope.matches(&input) { + /// do_work(input) + /// } else { + /// Err("Scope didn't match input") + /// } + /// } + /// ``` + pub fn matches(&self, input: &T::Input) -> bool { + // first make sure the input doesn't match any existing deny scope + if self.deny.iter().any(|s| s.matches(input)) { + return false; + } + + // if there are allow scopes, ensure the input matches at least 1 + if self.allow.is_empty() { + true + } else { + self.allow.iter().any(|s| s.matches(input)) + } + } +} + +impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for CommandScope { + /// Grabs the [`ResolvedScope`] from the [`CommandItem`] and returns the associated [`CommandScope`]. + fn from_command(command: CommandItem<'a, R>) -> Result { + if let Some(resolved) = &command.acl { + let scope_ids = resolved + .iter() + .filter_map(|cmd| cmd.scope_id) + .collect::>(); + CommandScope::resolve(&command.message.webview, scope_ids).map_err(Into::into) + } else { + Ok(CommandScope { + allow: Default::default(), + deny: Default::default(), + }) + } + } +} + +/// Global access scope that can be retrieved directly in the command function. +#[derive(Debug)] +pub struct GlobalScope(ScopeValue); + +impl GlobalScope { + pub(crate) fn resolve(webview: &Webview, plugin: &str) -> crate::Result { + webview + .manager() + .runtime_authority + .lock() + .unwrap() + .scope_manager + .get_global_scope_typed(webview.app_handle(), plugin) + .map(Self) + } + + /// What this access scope allows. + pub fn allows(&self) -> &Vec> { + &self.0.allow + } + + /// What this access scope denies. + pub fn denies(&self) -> &Vec> { + &self.0.deny + } +} + +impl<'a, R: Runtime, T: ScopeObject> CommandArg<'a, R> for GlobalScope { + /// Grabs the [`ResolvedScope`] from the [`CommandItem`] and returns the associated [`GlobalScope`]. + fn from_command(command: CommandItem<'a, R>) -> Result { + GlobalScope::resolve( + &command.message.webview, + command.plugin.unwrap_or(APP_ACL_KEY), + ) + .map_err(InvokeError::from_error) + } +} + +#[derive(Debug)] +pub struct ScopeManager { + command_scope: BTreeMap, + global_scope: BTreeMap, + command_cache: BTreeMap, + global_scope_cache: StateManager, +} + +/// Marks a type as a scope object. +/// +/// Usually you will just rely on [`serde::de::DeserializeOwned`] instead of implementing it manually, +/// though this is useful if you need to do some initialization logic on the type itself. +pub trait ScopeObject: Sized + Send + Sync + Debug + 'static { + /// The error type. + type Error: std::error::Error + Send + Sync; + /// Deserialize the raw scope value. + fn deserialize(app: &AppHandle, raw: Value) -> Result; +} + +impl ScopeObject for T { + type Error = serde_json::Error; + fn deserialize(_app: &AppHandle, raw: Value) -> Result { + serde_json::from_value(raw.into()) + } +} + +/// A [`ScopeObject`] whose validation can be represented as a `bool`. +/// +/// # Example +/// +/// ``` +/// # use serde::{Deserialize, Serialize}; +/// # use tauri::{ipc::ScopeObjectMatch, Url}; +/// # +/// #[derive(Debug, Clone, Serialize, Deserialize)] +/// #[serde(rename_all = "camelCase")] +/// pub enum Scope { +/// Domain(Url), +/// StartsWith(String), +/// } +/// +/// impl ScopeObjectMatch for Scope { +/// type Input = str; +/// +/// fn matches(&self, input: &str) -> bool { +/// match self { +/// Scope::Domain(url) => { +/// let parsed: Url = match input.parse() { +/// Ok(parsed) => parsed, +/// Err(_) => return false, +/// }; +/// +/// let domain = parsed.domain(); +/// +/// domain.is_some() && domain == url.domain() +/// } +/// Scope::StartsWith(start) => input.starts_with(start), +/// } +/// } +/// } +/// ``` +pub trait ScopeObjectMatch: ScopeObject { + /// The type of input expected to validate against the scope. + /// + /// This will be borrowed, so if you want to match on a `&str` this type should be `str`. + type Input: ?Sized; + + /// Check if the input matches against the scope. + fn matches(&self, input: &Self::Input) -> bool; +} + +impl ScopeManager { + pub(crate) fn get_global_scope_typed( + &self, + app: &AppHandle, + key: &str, + ) -> crate::Result> { + match self.global_scope_cache.try_get::>() { + Some(cached) => Ok((*cached).clone()), + None => { + let mut allow = Vec::new(); + let mut deny = Vec::new(); + + if let Some(global_scope) = self.global_scope.get(key) { + for allowed in &global_scope.allow { + allow + .push(Arc::new(T::deserialize(app, allowed.clone()).map_err( + |e| crate::Error::CannotDeserializeScope(Box::new(e)), + )?)); + } + for denied in &global_scope.deny { + deny + .push(Arc::new(T::deserialize(app, denied.clone()).map_err( + |e| crate::Error::CannotDeserializeScope(Box::new(e)), + )?)); + } + } + + let scope = ScopeValue { + allow: Arc::new(allow), + deny: Arc::new(deny), + }; + self.global_scope_cache.set(scope.clone()); + Ok(scope) + } + } + } + + fn get_command_scope_typed( + &self, + app: &AppHandle, + key: &ScopeKey, + ) -> crate::Result> { + let cache = self.command_cache.get(key).unwrap(); + match cache.try_get::>() { + Some(cached) => Ok((*cached).clone()), + None => { + let resolved_scope = self + .command_scope + .get(key) + .unwrap_or_else(|| panic!("missing command scope for key {key}")); + + let mut allow = Vec::new(); + let mut deny = Vec::new(); + + for allowed in &resolved_scope.allow { + allow + .push(Arc::new(T::deserialize(app, allowed.clone()).map_err( + |e| crate::Error::CannotDeserializeScope(Box::new(e)), + )?)); + } + for denied in &resolved_scope.deny { + deny + .push(Arc::new(T::deserialize(app, denied.clone()).map_err( + |e| crate::Error::CannotDeserializeScope(Box::new(e)), + )?)); + } + + let value = ScopeValue { + allow: Arc::new(allow), + deny: Arc::new(deny), + }; + + let _ = cache.set(value.clone()); + Ok(value) + } + } + } +} + +#[cfg(test)] +mod tests { + use glob::Pattern; + use tauri_utils::acl::{ + ExecutionContext, + resolved::{Resolved, ResolvedCommand}, + }; + + use crate::ipc::Origin; + + use super::RuntimeAuthority; + + #[test] + fn window_glob_pattern_matches() { + let command = "my-command"; + let window = "main-*"; + let webview = "other-*"; + + let resolved_cmd = vec![ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + ..Default::default() + }]; + let allowed_commands = [(command.to_string(), resolved_cmd.clone())] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + ..Default::default() + }, + ); + + assert_eq!( + authority.resolve_access( + command, + &window.replace('*', "something"), + webview, + &Origin::Local + ), + Some(resolved_cmd) + ); + } + + #[test] + fn webview_glob_pattern_matches() { + let command = "my-command"; + let window = "other-*"; + let webview = "main-*"; + + let resolved_cmd = vec![ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + webviews: vec![Pattern::new(webview).unwrap()], + ..Default::default() + }]; + let allowed_commands = [(command.to_string(), resolved_cmd.clone())] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + ..Default::default() + }, + ); + + assert_eq!( + authority.resolve_access( + command, + window, + &webview.replace('*', "something"), + &Origin::Local + ), + Some(resolved_cmd) + ); + } + + #[test] + fn remote_domain_matches() { + let url = "https://tauri.app"; + let command = "my-command"; + let window = "main"; + let webview = "main"; + + let resolved_cmd = vec![ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + context: ExecutionContext::Remote { + url: url.parse().unwrap(), + }, + ..Default::default() + }]; + let allowed_commands = [(command.to_string(), resolved_cmd.clone())] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + ..Default::default() + }, + ); + + assert_eq!( + authority.resolve_access( + command, + window, + webview, + &Origin::Remote { + url: url.parse().unwrap() + } + ), + Some(resolved_cmd) + ); + } + + #[test] + fn remote_domain_glob_pattern_matches() { + let url = "http://tauri.*"; + let command = "my-command"; + let window = "main"; + let webview = "main"; + + let resolved_cmd = vec![ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + context: ExecutionContext::Remote { + url: url.parse().unwrap(), + }, + ..Default::default() + }]; + let allowed_commands = [(command.to_string(), resolved_cmd.clone())] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + ..Default::default() + }, + ); + + assert_eq!( + authority.resolve_access( + command, + window, + webview, + &Origin::Remote { + url: url.replace('*', "studio").parse().unwrap() + } + ), + Some(resolved_cmd) + ); + } + + #[test] + fn remote_context_denied() { + let command = "my-command"; + let window = "main"; + let webview = "main"; + + let resolved_cmd = vec![ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + ..Default::default() + }]; + let allowed_commands = [(command.to_string(), resolved_cmd)].into_iter().collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + ..Default::default() + }, + ); + + assert!( + authority + .resolve_access( + command, + window, + webview, + &Origin::Remote { + url: "https://tauri.app".parse().unwrap() + } + ) + .is_none() + ); + } + + #[test] + fn denied_command_takes_precedence() { + let command = "my-command"; + let window = "main"; + let webview = "main"; + let windows = vec![Pattern::new(window).unwrap()]; + let allowed_commands = [( + command.to_string(), + vec![ResolvedCommand { + windows: windows.clone(), + ..Default::default() + }], + )] + .into_iter() + .collect(); + let denied_commands = [( + command.to_string(), + vec![ResolvedCommand { + windows, + ..Default::default() + }], + )] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + denied_commands, + ..Default::default() + }, + ); + + assert!( + authority + .resolve_access(command, window, webview, &Origin::Local) + .is_none() + ); + } + + #[test] + fn denied_command_is_scoped_to_the_execution_context() { + let url = "https://tauri.app"; + let command = "my-command"; + let window = "main"; + let webview = "main"; + let windows = vec![Pattern::new(window).unwrap()]; + + // the command is allowed on both the local app and the remote URL + let allowed_commands = [( + command.to_string(), + vec![ + ResolvedCommand { + windows: windows.clone(), + ..Default::default() + }, + ResolvedCommand { + windows: windows.clone(), + context: ExecutionContext::Remote { + url: url.parse().unwrap(), + }, + ..Default::default() + }, + ], + )] + .into_iter() + .collect(); + + // but it is only denied on the remote URL + let denied_commands = [( + command.to_string(), + vec![ResolvedCommand { + windows, + context: ExecutionContext::Remote { + url: url.parse().unwrap(), + }, + ..Default::default() + }], + )] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + denied_commands, + ..Default::default() + }, + ); + + // the remote origin the deny entry was defined for is denied + assert!( + authority + .resolve_access( + command, + window, + webview, + &Origin::Remote { + url: url.parse().unwrap() + } + ) + .is_none() + ); + + // the local app must not be affected by a deny entry of a remote capability + let resolved = authority + .resolve_access(command, window, webview, &Origin::Local) + .expect("local origin must not be denied by a remote capability"); + assert_eq!(resolved.len(), 1); + assert_eq!(resolved[0].context, ExecutionContext::Local); + } + + #[test] + fn denied_command_on_local_context_does_not_deny_remote() { + let url = "https://tauri.app"; + let command = "my-command"; + let window = "main"; + let webview = "main"; + let windows = vec![Pattern::new(window).unwrap()]; + + // the command is allowed on both the local app and the remote URL + let allowed_commands = [( + command.to_string(), + vec![ + ResolvedCommand { + windows: windows.clone(), + ..Default::default() + }, + ResolvedCommand { + windows: windows.clone(), + context: ExecutionContext::Remote { + url: url.parse().unwrap(), + }, + ..Default::default() + }, + ], + )] + .into_iter() + .collect(); + + // but it is only denied on the local app + let denied_commands = [( + command.to_string(), + vec![ResolvedCommand { + windows, + ..Default::default() + }], + )] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + denied_commands, + ..Default::default() + }, + ); + + // the remote origin must not be affected by a deny entry of a local capability + let resolved = authority + .resolve_access( + command, + window, + webview, + &Origin::Remote { + url: url.parse().unwrap(), + }, + ) + .expect("remote origin must not be denied by a local capability"); + assert_eq!(resolved.len(), 1); + assert_eq!( + resolved[0].context, + ExecutionContext::Remote { + url: url.parse().unwrap() + } + ); + + // the local app is denied even though it has an allow entry + assert!( + authority + .resolve_access(command, window, webview, &Origin::Local) + .is_none() + ); + } + + #[cfg(debug_assertions)] + #[test] + fn resolve_access_message_denied_on_origin() { + use tauri_utils::acl::resolved::ResolvedCommandReference; + + let plugin_name = "myplugin"; + let command_name = "my-command"; + let command = format!("plugin:{plugin_name}|{command_name}"); + let window = "main"; + let webview = "main"; + let remote_url = "http://localhost:8080"; + let windows = vec![Pattern::new(window).unwrap()]; + + let remote_context = ExecutionContext::Remote { + url: remote_url.parse().unwrap(), + }; + + let allowed_commands = [( + command.clone(), + vec![ + ResolvedCommand { + windows: windows.clone(), + referenced_by: ResolvedCommandReference { + capability: "maincap".to_string(), + permission: "allow-command".to_string(), + }, + ..Default::default() + }, + ResolvedCommand { + windows: windows.clone(), + context: remote_context.clone(), + referenced_by: ResolvedCommandReference { + capability: "remotecap".to_string(), + permission: "allow-command".to_string(), + }, + ..Default::default() + }, + ], + )] + .into_iter() + .collect(); + + // one capability denies the command locally, another denies it on the remote URL + let denied_commands = [( + command, + vec![ + ResolvedCommand { + windows: windows.clone(), + referenced_by: ResolvedCommandReference { + capability: "localcap".to_string(), + permission: "deny-command".to_string(), + }, + ..Default::default() + }, + ResolvedCommand { + windows, + context: remote_context, + referenced_by: ResolvedCommandReference { + capability: "remotecap".to_string(), + permission: "deny-command".to_string(), + }, + ..Default::default() + }, + ], + )] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + Default::default(), + Resolved { + allowed_commands, + denied_commands, + ..Default::default() + }, + ); + + // only the capability denying the local context is referenced + assert_eq!( + authority.resolve_access_message(plugin_name, command_name, window, webview, &Origin::Local), + "myplugin.my-command explicitly denied on origin local\n\nreferenced by: capability: localcap, permission: deny-command" + ); + + // only the capability denying the remote URL is referenced + assert_eq!( + authority.resolve_access_message( + plugin_name, + command_name, + window, + webview, + &Origin::Remote { + url: remote_url.parse().unwrap() + } + ), + "myplugin.my-command explicitly denied on origin remote: http://localhost:8080/\n\nreferenced by: capability: remotecap, permission: deny-command" + ); + + // a remote URL that matches no deny entry is not reported as explicitly denied + let message = authority.resolve_access_message( + plugin_name, + command_name, + window, + webview, + &Origin::Remote { + url: "http://localhost:123".parse().unwrap(), + }, + ); + assert!( + !message.contains("explicitly denied"), + "unexpected message: {message}" + ); + assert!( + message.starts_with("myplugin.my-command not allowed"), + "unexpected message: {message}" + ); + } + + #[cfg(debug_assertions)] + #[test] + fn resolve_access_message() { + use tauri_utils::acl::manifest::Manifest; + + let plugin_name = "myplugin"; + let command_allowed_on_window = "my-command-window"; + let command_allowed_on_webview_window = "my-command-webview-window"; + let window = "main-*"; + let webview = "webview-*"; + let remote_url = "http://localhost:8080"; + + let referenced_by = tauri_utils::acl::resolved::ResolvedCommandReference { + capability: "maincap".to_string(), + permission: "allow-command".to_string(), + }; + + let resolved_window_cmd = ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + referenced_by: referenced_by.clone(), + ..Default::default() + }; + let resolved_webview_window_cmd = ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + webviews: vec![Pattern::new(webview).unwrap()], + referenced_by: referenced_by.clone(), + ..Default::default() + }; + let resolved_webview_window_remote_cmd = ResolvedCommand { + windows: vec![Pattern::new(window).unwrap()], + webviews: vec![Pattern::new(webview).unwrap()], + referenced_by, + context: ExecutionContext::Remote { + url: remote_url.parse().unwrap(), + }, + ..Default::default() + }; + + let allowed_commands = [ + ( + format!("plugin:{plugin_name}|{command_allowed_on_window}"), + vec![resolved_window_cmd], + ), + ( + format!("plugin:{plugin_name}|{command_allowed_on_webview_window}"), + vec![ + resolved_webview_window_cmd, + resolved_webview_window_remote_cmd, + ], + ), + ] + .into_iter() + .collect(); + + let authority = RuntimeAuthority::new( + [( + plugin_name.to_string(), + Manifest { + default_permission: None, + permissions: Default::default(), + permission_sets: Default::default(), + global_scope_schema: None, + }, + )] + .into_iter() + .collect(), + Resolved { + allowed_commands, + ..Default::default() + }, + ); + + // unknown plugin + assert_eq!( + authority.resolve_access_message( + "unknown-plugin", + command_allowed_on_window, + window, + webview, + &Origin::Local + ), + "unknown-plugin.my-command-window not allowed. Plugin not found" + ); + + // unknown command + assert_eq!( + authority.resolve_access_message( + plugin_name, + "unknown-command", + window, + webview, + &Origin::Local + ), + "myplugin.unknown-command not allowed. Command not found" + ); + + // window/webview do not match + assert_eq!( + authority.resolve_access_message( + plugin_name, + command_allowed_on_window, + "other-window", + "any-webview", + &Origin::Local + ), + "myplugin.my-command-window not allowed on window \"other-window\", webview \"any-webview\", URL: local\n\nallowed on: [windows: \"main-*\", URL: local]\n\nreferenced by: capability: maincap, permission: allow-command" + ); + + // window matches, but not origin + assert_eq!( + authority.resolve_access_message( + plugin_name, + command_allowed_on_window, + window, + "any-webview", + &Origin::Remote { + url: "http://localhst".parse().unwrap() + } + ), + "myplugin.my-command-window not allowed on window \"main-*\", webview \"any-webview\", URL: http://localhst/\n\nallowed on: [windows: \"main-*\", URL: local]\n\nreferenced by: capability: maincap, permission: allow-command" + ); + + // window/webview do not match + assert_eq!( + authority.resolve_access_message( + plugin_name, + command_allowed_on_webview_window, + "other-window", + "other-webview", + &Origin::Local + ), + "myplugin.my-command-webview-window not allowed on window \"other-window\", webview \"other-webview\", URL: local\n\nallowed on: [windows: \"main-*\", webviews: \"webview-*\", URL: local], [windows: \"main-*\", webviews: \"webview-*\", URL: http://localhost:8080]\n\nreferenced by: capability: maincap, permission: allow-command || capability: maincap, permission: allow-command" + ); + + // window/webview matches, but not origin + assert_eq!( + authority.resolve_access_message( + plugin_name, + command_allowed_on_webview_window, + window, + webview, + &Origin::Remote { + url: "http://localhost:123".parse().unwrap() + } + ), + "myplugin.my-command-webview-window not allowed on window \"main-*\", webview \"webview-*\", URL: http://localhost:123/\n\nallowed on: [windows: \"main-*\", webviews: \"webview-*\", URL: local], [windows: \"main-*\", webviews: \"webview-*\", URL: http://localhost:8080]\n\nreferenced by: capability: maincap, permission: allow-command || capability: maincap, permission: allow-command" + ); + } +} diff --git a/crates/tauri/src/ipc/capability_builder.rs b/crates/tauri/src/ipc/capability_builder.rs new file mode 100644 index 0000000..53dfcc4 --- /dev/null +++ b/crates/tauri/src/ipc/capability_builder.rs @@ -0,0 +1,170 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Serialize; +use tauri_utils::{ + acl::{ + Scopes, + capability::{Capability, CapabilityFile, PermissionEntry}, + }, + platform::Target, +}; + +/// A capability that can be added at runtime. +pub trait RuntimeCapability { + /// Creates the capability file. + fn build(self) -> CapabilityFile; +} + +impl> RuntimeCapability for T { + fn build(self) -> CapabilityFile { + self.as_ref().parse().expect("invalid capability") + } +} + +/// A builder for a [`Capability`]. +pub struct CapabilityBuilder(Capability); + +impl CapabilityBuilder { + /// Creates a new capability builder with a unique identifier. + pub fn new(identifier: impl Into) -> Self { + Self(Capability { + identifier: identifier.into(), + description: "".into(), + remote: None, + local: true, + windows: Vec::new(), + webviews: Vec::new(), + permissions: Vec::new(), + platforms: None, + }) + } + + /// Allows this capability to be used by a remote URL. + pub fn remote(mut self, url: String) -> Self { + self + .0 + .remote + .get_or_insert_with(Default::default) + .urls + .push(url); + self + } + + /// Whether this capability is applied on local app URLs or not. Defaults to `true`. + pub fn local(mut self, local: bool) -> Self { + self.0.local = local; + self + } + + /// Link this capability to the given window label. + pub fn window(mut self, window: impl Into) -> Self { + self.0.windows.push(window.into()); + self + } + + /// Link this capability to the a list of window labels. + pub fn windows(mut self, windows: impl IntoIterator>) -> Self { + self.0.windows.extend(windows.into_iter().map(|w| w.into())); + self + } + + /// Link this capability to the given webview label. + pub fn webview(mut self, webview: impl Into) -> Self { + self.0.webviews.push(webview.into()); + self + } + + /// Link this capability to the a list of window labels. + pub fn webviews(mut self, webviews: impl IntoIterator>) -> Self { + self + .0 + .webviews + .extend(webviews.into_iter().map(|w| w.into())); + self + } + + /// Add a new permission to this capability. + pub fn permission(mut self, permission: impl Into) -> Self { + let permission = permission.into(); + self.0.permissions.push(PermissionEntry::PermissionRef( + permission + .clone() + .try_into() + .unwrap_or_else(|_| panic!("invalid permission identifier '{permission}'")), + )); + self + } + + /// Add a new scoped permission to this capability. + pub fn permission_scoped( + mut self, + permission: impl Into, + allowed: Vec, + denied: Vec, + ) -> Self { + let permission = permission.into(); + let identifier = permission + .clone() + .try_into() + .unwrap_or_else(|_| panic!("invalid permission identifier '{permission}'")); + + let allowed_scope = allowed + .into_iter() + .map(|a| { + serde_json::to_value(a) + .expect("failed to serialize scope") + .into() + }) + .collect(); + let denied_scope = denied + .into_iter() + .map(|a| { + serde_json::to_value(a) + .expect("failed to serialize scope") + .into() + }) + .collect(); + let scope = Scopes { + allow: Some(allowed_scope), + deny: Some(denied_scope), + }; + + self + .0 + .permissions + .push(PermissionEntry::ExtendedPermission { identifier, scope }); + self + } + + /// Adds a target platform for this capability. + /// + /// By default all platforms are applied. + pub fn platform(mut self, platform: Target) -> Self { + self + .0 + .platforms + .get_or_insert_with(Default::default) + .push(platform); + self + } + + /// Adds target platforms for this capability. + /// + /// By default all platforms are applied. + pub fn platforms(mut self, platforms: impl IntoIterator) -> Self { + self + .0 + .platforms + .get_or_insert_with(Default::default) + .extend(platforms); + self + } +} + +impl RuntimeCapability for CapabilityBuilder { + fn build(self) -> CapabilityFile { + CapabilityFile::Capability(self.0) + } +} diff --git a/crates/tauri/src/ipc/channel.rs b/crates/tauri/src/ipc/channel.rs new file mode 100644 index 0000000..16153e5 --- /dev/null +++ b/crates/tauri/src/ipc/channel.rs @@ -0,0 +1,530 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::HashMap, + str::FromStr, + sync::{ + Arc, Mutex, + atomic::{AtomicU32, AtomicUsize, Ordering}, + }, +}; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::{ + Manager, Runtime, State, Webview, command, + ipc::{CommandArg, CommandItem}, + plugin::{Builder as PluginBuilder, TauriPlugin}, +}; + +use super::{ + CallbackFn, InvokeError, InvokeResponseBody, IpcResponse, Request, Response, + format_callback::format_raw_js, +}; + +pub const IPC_PAYLOAD_PREFIX: &str = "__CHANNEL__:"; +// TODO: Change this to `channel` in v3 +pub const CHANNEL_PLUGIN_NAME: &str = "__TAURI_CHANNEL__"; +// TODO: Change this to `plugin:channel|fetch` in v3 +pub const FETCH_CHANNEL_DATA_COMMAND: &str = "plugin:__TAURI_CHANNEL__|fetch"; +const CHANNEL_ID_HEADER_NAME: &str = "Tauri-Channel-Id"; + +/// Maximum size a JSON we should send directly without going through the fetch process +// 8192 byte JSON payload runs roughly 2x faster through eval than through fetch on WebView2 v135 +const MAX_JSON_DIRECT_EXECUTE_THRESHOLD: usize = 8192; +// 1024 byte payload runs roughly 30% faster through eval than through fetch on macOS +const MAX_RAW_DIRECT_EXECUTE_THRESHOLD: usize = 1024; + +static CHANNEL_COUNTER: AtomicU32 = AtomicU32::new(0); + +/// Maps channel ids to pending data that must be sent to the JavaScript side via the IPC. +/// +/// Scoped per webview: each webview has its own id sequence and lookups only +/// ever touch the calling webview's entries. +#[derive(Default, Clone)] +pub struct ChannelDataIpcQueue(Arc>>); + +/// Pending channel data of one webview. +#[derive(Default)] +struct WebviewChannelDataQueue { + next_id: u32, + entries: HashMap, +} + +impl ChannelDataIpcQueue { + /// Stores the body for the given webview and returns its id. + /// + /// Ids are sequential per webview and only address that webview's entries. + fn insert(&self, webview_label: &str, body: InvokeResponseBody) -> u32 { + let mut cache = self.0.lock().unwrap(); + let queue = cache.entry(webview_label.to_string()).or_default(); + let data_id = loop { + let candidate = queue.next_id; + queue.next_id = queue.next_id.wrapping_add(1); + if !queue.entries.contains_key(&candidate) { + break candidate; + } + }; + queue.entries.insert(data_id, body); + data_id + } + + /// Removes and returns the entry with the given id from the given webview's queue. + fn remove(&self, webview_label: &str, data_id: u32) -> Option { + self + .0 + .lock() + .unwrap() + .get_mut(webview_label)? + .entries + .remove(&data_id) + } + + /// Drops all entries of the given webview. + pub(crate) fn remove_webview_entries(&self, webview_label: &str) { + self.0.lock().unwrap().remove(webview_label); + } +} + +/// An IPC channel, used to stream values from Rust to the frontend. +/// +/// A command can only resolve once, so a channel is how you push an arbitrary number of messages +/// to the JavaScript side after the command returned: download progress, log lines, streamed +/// responses and so on. Each message is a `TSend` value, which must implement [`IpcResponse`] +/// (automatically implemented for every [`serde::Serialize`] type) and is delivered to the +/// `onmessage` handler of the matching `Channel` on the JavaScript side. +/// +/// The usual flow is to create the channel on the frontend and pass it as a command argument, +/// since [`Channel`] implements [`CommandArg`]: +/// +/// ```javascript +/// import { Channel, invoke } from '@tauri-apps/api/core' +/// +/// const onProgress = new Channel() +/// onProgress.onmessage = (message) => console.log(message) +/// await invoke('download', { url, onProgress }) +/// ``` +/// +/// A channel can also be created on the Rust side with [`Channel::new`], or resolved from an id +/// the frontend sent inside a bigger payload with [`JavaScriptChannelId::channel_on`]. +/// +/// Channels are cheap to clone (every clone refers to the same JavaScript callback) and can be +/// stored in the app state or moved to another thread to send messages later. When the last clone +/// of a channel created from the frontend is dropped, the JavaScript side is notified that no more +/// messages will arrive and the callback is unregistered. +/// +/// # Ordering +/// +/// The channel automatically orders the messages: every message carries the index it was sent with, +/// and the JavaScript side buffers out-of-order messages until the missing ones arrive, so the +/// `onmessage` handler always observes messages in the same order [`Channel::send`] was called. +/// See [`Builder::channel_interceptor`](crate::Builder::channel_interceptor) if you need to +/// intercept or replace this delivery mechanism. +/// +/// # Raw payloads +/// +/// Any [`serde::Serialize`] value is sent as JSON. To stream binary data without the JSON overhead, +/// send an [`InvokeResponseBody::Raw`] through a `Channel` (the default type +/// parameter) or a [`Response`]: the payload is then received in JavaScript as an `ArrayBuffer`. +/// Note that a `Channel>` does *not* do this - `Vec` is `Serialize`, +/// so it is sent as a JSON array of numbers. +/// +/// # Examples +/// +/// Streaming progress to the frontend from a command: +/// +/// ```rust +/// use tauri::ipc::Channel; +/// +/// #[derive(Clone, serde::Serialize)] +/// #[serde(rename_all = "camelCase")] +/// struct DownloadProgress { +/// downloaded: usize, +/// content_length: usize, +/// } +/// +/// #[tauri::command] +/// fn download(url: String, on_progress: Channel) -> tauri::Result<()> { +/// let content_length = 1000; +/// for downloaded in (0..=content_length).step_by(100) { +/// on_progress.send(DownloadProgress { downloaded, content_length })?; +/// } +/// Ok(()) +/// } +/// ``` +/// +/// Streaming binary chunks, received as `ArrayBuffer`s in JavaScript: +/// +/// ```rust +/// use tauri::ipc::{Channel, InvokeResponseBody}; +/// +/// #[tauri::command] +/// fn read_file(on_chunk: Channel) -> tauri::Result<()> { +/// for chunk in [vec![0u8; 16], vec![1u8; 16]] { +/// on_chunk.send(InvokeResponseBody::Raw(chunk))?; +/// } +/// Ok(()) +/// } +/// ``` +pub struct Channel { + inner: Arc, + phantom: std::marker::PhantomData, +} + +#[cfg(feature = "specta")] +const _: () = { + #[derive(specta::Type)] + #[specta(remote = super::Channel)] + #[allow(dead_code, non_camel_case_types)] + struct TAURI_CHANNEL(std::marker::PhantomData); +}; + +impl Clone for Channel { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + phantom: self.phantom, + } + } +} + +type OnDropFn = Option>; +type OnMessageFn = Box crate::Result<()> + Send + Sync>; + +struct ChannelInner { + id: u32, + on_message: OnMessageFn, + on_drop: OnDropFn, +} + +impl Drop for ChannelInner { + fn drop(&mut self) { + if let Some(on_drop) = &self.on_drop { + on_drop(); + } + } +} + +impl Serialize for Channel { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{IPC_PAYLOAD_PREFIX}{}", self.inner.id)) + } +} + +/// The ID of a channel that was defined on the JavaScript layer. +/// +/// Useful when expecting [`Channel`] as part of a JSON object instead of a top-level command argument. +/// +/// # Examples +/// +/// ```rust +/// use tauri::{ipc::JavaScriptChannelId, Runtime, Webview}; +/// +/// #[derive(serde::Deserialize)] +/// #[serde(rename_all = "camelCase")] +/// struct Button { +/// label: String, +/// on_click: JavaScriptChannelId, +/// } +/// +/// #[tauri::command] +/// fn add_button(webview: Webview, button: Button) { +/// let channel = button.on_click.channel_on(webview); +/// channel.send("clicked").unwrap(); +/// } +/// ``` +pub struct JavaScriptChannelId(CallbackFn); + +impl FromStr for JavaScriptChannelId { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + s.strip_prefix(IPC_PAYLOAD_PREFIX) + .ok_or("invalid channel string") + .and_then(|id| id.parse().map_err(|_| "invalid channel ID")) + .map(|id| Self(CallbackFn(id))) + } +} + +impl JavaScriptChannelId { + /// Gets a [`Channel`] for this channel ID on the given [`Webview`]. + pub fn channel_on(&self, webview: Webview) -> Channel { + let callback_fn = self.0; + let callback_id = callback_fn.0; + + let counter = Arc::new(AtomicUsize::new(0)); + let counter_clone = counter.clone(); + let webview_clone = webview.clone(); + + Channel::new_with_id( + callback_id, + Box::new(move |body| { + let current_index = counter.fetch_add(1, Ordering::Relaxed); + + if let Some(interceptor) = &webview.manager.channel_interceptor { + if interceptor(&webview, callback_fn, current_index, &body) { + return Ok(()); + } + } + + match body { + // Don't go through the fetch process if the payload is small + InvokeResponseBody::Json(json_string) + if json_string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD => + { + webview.eval(format_raw_js( + callback_id, + format!("{{ message: {json_string}, index: {current_index} }}"), + ))?; + } + InvokeResponseBody::Raw(bytes) if bytes.len() < MAX_RAW_DIRECT_EXECUTE_THRESHOLD => { + let bytes_as_json_array = serde_json::to_string(&bytes)?; + webview.eval(format_raw_js(callback_id, format!("{{ message: new Uint8Array({bytes_as_json_array}).buffer, index: {current_index} }}")))?; + } + // use the fetch API to speed up larger response payloads + _ => { + let data_id = webview + .state::() + .insert(webview.label(), body); + + webview.eval(format!( + "window.__TAURI_INTERNALS__.invoke('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': '{data_id}' }} }}).then((response) => window.__TAURI_INTERNALS__.runCallback({callback_id}, {{ message: response, index: {current_index} }})).catch(console.error)", + ))?; + } + } + + Ok(()) + }), + Some(Box::new(move || { + let current_index = counter_clone.load(Ordering::Relaxed); + let _ = webview_clone.eval(format_raw_js( + callback_id, + format!("{{ end: true, index: {current_index} }}"), + )); + })), + ) + } +} + +impl<'de> Deserialize<'de> for JavaScriptChannelId { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let value: String = Deserialize::deserialize(deserializer)?; + Self::from_str(&value).map_err(|_| { + serde::de::Error::custom(format!( + "invalid channel value `{value}`, expected a string in the `{IPC_PAYLOAD_PREFIX}ID` format" + )) + }) + } +} + +impl Channel { + /// Creates a new channel with the given message handler. + /// + /// This does not involve the frontend: the closure is called with the body of every message sent + /// through [`Channel::send`], and it is up to you to forward it. Use it to create a channel that + /// a plugin or a mobile command expects, or to consume channel messages in Rust. + /// + /// To push messages to a channel that was created by the frontend, receive the [`Channel`] as a + /// command argument (see [`CommandArg`]) or deserialize a [`JavaScriptChannelId`] and call + /// [`JavaScriptChannelId::channel_on`] with the target [`Webview`], which wires the messages + /// through the IPC for you. + /// + /// # Examples + /// + /// ```rust + /// use tauri::ipc::{Channel, InvokeResponseBody}; + /// + /// let channel = Channel::new(|message: InvokeResponseBody| { + /// match message { + /// InvokeResponseBody::Json(json) => println!("channel message: {json}"), + /// InvokeResponseBody::Raw(bytes) => println!("channel message: {} bytes", bytes.len()), + /// } + /// Ok(()) + /// }); + /// + /// channel.send("hello").unwrap(); + /// ``` + pub fn new crate::Result<()> + Send + Sync + 'static>( + on_message: F, + ) -> Self { + Self::new_with_id( + CHANNEL_COUNTER.fetch_add(1, Ordering::Relaxed), + Box::new(on_message), + None, + ) + } + + fn new_with_id(id: u32, on_message: OnMessageFn, on_drop: OnDropFn) -> Self { + #[allow(clippy::let_and_return)] + let channel = Self { + inner: Arc::new(ChannelInner { + id, + on_message, + on_drop, + }), + phantom: Default::default(), + }; + + #[cfg(mobile)] + crate::plugin::mobile::register_channel(Channel { + inner: channel.inner.clone(), + phantom: Default::default(), + }); + + channel + } + + // This is used from the IPC handler + pub(crate) fn from_callback_fn(webview: Webview, callback: CallbackFn) -> Self { + let callback_id = callback.0; + Channel::new_with_id( + callback_id, + Box::new(move |body| { + match body { + // Don't go through the fetch process if the payload is small + InvokeResponseBody::Json(json_string) + if json_string.len() < MAX_JSON_DIRECT_EXECUTE_THRESHOLD => + { + webview.eval(format_raw_js(callback_id, json_string))?; + } + InvokeResponseBody::Raw(bytes) if bytes.len() < MAX_RAW_DIRECT_EXECUTE_THRESHOLD => { + let bytes_as_json_array = serde_json::to_string(&bytes)?; + webview.eval(format_raw_js( + callback_id, + format!("new Uint8Array({bytes_as_json_array}).buffer"), + ))?; + } + // use the fetch API to speed up larger response payloads + _ => { + let data_id = webview + .state::() + .insert(webview.label(), body); + + webview.eval(format!( + "window.__TAURI_INTERNALS__.invoke('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': '{data_id}' }} }}).then((response) => window.__TAURI_INTERNALS__.runCallback({callback_id}, response)).catch(console.error)", + ))?; + } + } + + Ok(()) + }), + None, + ) + } + + /// The channel identifier. + pub fn id(&self) -> u32 { + self.inner.id + } + + /// Sends the given data through the channel. + pub fn send(&self, data: TSend) -> crate::Result<()> + where + TSend: IpcResponse, + { + (self.inner.on_message)(data.body()?) + } +} + +impl<'de, R: Runtime, TSend> CommandArg<'de, R> for Channel { + /// Grabs the [`Webview`] from the [`CommandItem`] and returns the associated [`Channel`]. + fn from_command(command: CommandItem<'de, R>) -> Result { + let name = command.name; + let arg = command.key; + let webview = command.message.webview(); + let value: String = + Deserialize::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e))?; + JavaScriptChannelId::from_str(&value) + .map(|id| id.channel_on(webview)) + .map_err(|_| { + InvokeError::from(format!( + "invalid channel value `{value}`, expected a string in the `{IPC_PAYLOAD_PREFIX}ID` format" + )) + }) + } +} + +#[command(root = "crate")] +fn fetch( + webview: Webview, + request: Request<'_>, + cache: State<'_, ChannelDataIpcQueue>, +) -> Result { + if let Some(id) = request + .headers() + .get(CHANNEL_ID_HEADER_NAME) + .and_then(|v| v.to_str().ok()) + .and_then(|id| id.parse().ok()) + { + if let Some(data) = cache.remove(webview.label(), id) { + Ok(Response::new(data)) + } else { + Err("data not found") + } + } else { + Err("missing channel id header") + } +} + +pub fn plugin() -> TauriPlugin { + PluginBuilder::new(CHANNEL_PLUGIN_NAME) + .invoke_handler(crate::generate_handler![ + #![plugin(__TAURI_CHANNEL__)] + fetch + ]) + .build() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn json_body(s: &str) -> InvokeResponseBody { + InvokeResponseBody::Json(s.to_string()) + } + + #[test] + fn queue_entries_are_scoped_to_the_owning_webview() { + let queue = ChannelDataIpcQueue::default(); + let id = queue.insert("main", json_body("{}")); + + // foreign webviews cannot fetch the entry + assert!(queue.remove("settings", id).is_none()); + // the owner can, once + assert!(queue.remove("main", id).is_some()); + assert!(queue.remove("main", id).is_none()); + } + + #[test] + fn ids_are_scoped_per_webview() { + let queue = ChannelDataIpcQueue::default(); + let a = queue.insert("a", json_body("1")); + let b = queue.insert("b", json_body("2")); + + // each webview has its own id sequence, so the same id exists twice + // without the entries being visible across webviews + assert_eq!(a, b); + assert!(queue.remove("b", a).is_some()); + assert!(queue.remove("a", a).is_some()); + } + + #[test] + fn purge_removes_only_the_closed_webview_entries() { + let queue = ChannelDataIpcQueue::default(); + let a = queue.insert("a", json_body("1")); + let b = queue.insert("b", json_body("2")); + + queue.remove_webview_entries("a"); + + assert!(queue.remove("a", a).is_none()); + assert!(queue.remove("b", b).is_some()); + } +} diff --git a/crates/tauri/src/ipc/command.rs b/crates/tauri/src/ipc/command.rs new file mode 100644 index 0000000..206f8ee --- /dev/null +++ b/crates/tauri/src/ipc/command.rs @@ -0,0 +1,332 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The Tauri custom commands types and traits. +//! +//! You usually don't need to create these items yourself. These are created from [command](../attr.command.html) +//! attribute macro along the way and used by [`crate::generate_handler`] macro. + +use crate::{ + Runtime, + ipc::{InvokeBody, InvokeError, InvokeMessage}, +}; +use serde::{ + Deserialize, Deserializer, + de::{Error, Visitor}, +}; + +use tauri_utils::acl::resolved::ResolvedCommand; + +/// Represents a custom command. +pub struct CommandItem<'a, R: Runtime> { + /// Name of the plugin if this command targets one. + pub plugin: Option<&'static str>, + + /// The name of the command, e.g. `handler` on `#[command] fn handler(value: u64)` + pub name: &'static str, + + /// The key of the command item, e.g. `value` on `#[command] fn handler(value: u64)` + pub key: &'static str, + + /// The [`InvokeMessage`] that was passed to this command. + pub message: &'a InvokeMessage, + + /// The resolved ACL for this command. + pub acl: &'a Option>, +} + +/// Trait implemented by command arguments to derive a value from a [`CommandItem`]. +/// +/// # Command Arguments +/// +/// A command argument is any type that represents an item parsable from a [`CommandItem`]. Most +/// implementations will use the data stored in [`InvokeMessage`] since [`CommandItem`] is mostly a +/// wrapper around it. +/// +/// # Provided Implementations +/// +/// Tauri implements [`CommandArg`] automatically for a number of types. +/// * [`crate::AppHandle`] +/// * [`crate::Window`] +/// * [`crate::Webview`] +/// * [`crate::WebviewWindow`] +/// * [`crate::State`] +/// * [`crate::ipc::Channel`] +/// * [`crate::ipc::Request`] +/// * Gives access to the raw request body and headers instead of a deserialized argument. +/// * [`crate::ipc::CommandScope`] and [`crate::ipc::GlobalScope`] +/// * The scope defined by the capabilities that allowed the command, see the +/// [scope documentation](https://v2.tauri.app/security/scope/). +/// * `T where T: serde::Deserialize` +/// * Any type that implements `Deserialize` can automatically be used as a [`CommandArg`]. +/// +/// Note that the types above are resolved from the command message and not from the command +/// arguments sent by the frontend, so they do not need to be provided on the JavaScript side. +pub trait CommandArg<'de, R: Runtime>: Sized { + /// Derives an instance of `Self` from the [`CommandItem`]. + /// + /// If the derivation fails, the corresponding message will be rejected using [`InvokeMessage#reject`]. + fn from_command(command: CommandItem<'de, R>) -> Result; +} + +/// Automatically implement [`CommandArg`] for any type that can be deserialized. +impl<'de, D: Deserialize<'de>, R: Runtime> CommandArg<'de, R> for D { + fn from_command(command: CommandItem<'de, R>) -> Result { + let name = command.name; + let arg = command.key; + #[cfg(feature = "tracing")] + let _span = tracing::trace_span!("ipc::request::deserialize_arg", arg = arg).entered(); + Self::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e).into()) + } +} + +/// Pass the result of [`serde_json::Value::get`] into [`serde_json::Value`]'s deserializer. +/// +/// Returns an error if the [`CommandItem`]'s key does not exist in the value. +macro_rules! pass { + ($fn:ident, $($arg:ident: $argt:ty),+) => { + fn $fn>(self, $($arg: $argt),*) -> Result { + self.deserialize_json()?.$fn($($arg),*) + } + } +} + +impl<'a, R: Runtime> CommandItem<'a, R> { + fn deserialize_json(self) -> serde_json::Result<&'a serde_json::Value> { + if self.key.is_empty() { + return Err(serde_json::Error::custom(format!( + "command {} has an argument with no name with a non-optional value", + self.name + ))); + } + + match &self.message.payload { + InvokeBody::Raw(_body) => Err(serde_json::Error::custom(format!( + "command {} expected a value for key {} but the IPC call used a bytes payload", + self.name, self.key + ))), + InvokeBody::Json(v) => match v.get(self.key) { + Some(value) => Ok(value), + None => Err(serde_json::Error::custom(format!( + "command {} missing required key {}", + self.name, self.key + ))), + }, + } + } +} + +/// A [`Deserializer`] wrapper around [`CommandItem`]. +/// +/// If the key doesn't exist, an error will be returned if the deserialized type is not expecting +/// an optional item. If the key does exist, the value will be called with +/// [`Value`](serde_json::Value)'s [`Deserializer`] implementation. +impl<'de, R: Runtime> Deserializer<'de> for CommandItem<'de, R> { + type Error = serde_json::Error; + + pass!(deserialize_any, visitor: V); + pass!(deserialize_bool, visitor: V); + pass!(deserialize_i8, visitor: V); + pass!(deserialize_i16, visitor: V); + pass!(deserialize_i32, visitor: V); + pass!(deserialize_i64, visitor: V); + pass!(deserialize_u8, visitor: V); + pass!(deserialize_u16, visitor: V); + pass!(deserialize_u32, visitor: V); + pass!(deserialize_u64, visitor: V); + pass!(deserialize_f32, visitor: V); + pass!(deserialize_f64, visitor: V); + pass!(deserialize_char, visitor: V); + pass!(deserialize_str, visitor: V); + pass!(deserialize_string, visitor: V); + pass!(deserialize_bytes, visitor: V); + pass!(deserialize_byte_buf, visitor: V); + + fn deserialize_option>(self, visitor: V) -> Result { + match &self.message.payload { + InvokeBody::Raw(_body) => Err(serde_json::Error::custom(format!( + "command {} expected a value for key {} but the IPC call used a bytes payload", + self.name, self.key + ))), + InvokeBody::Json(v) => match v.get(self.key) { + Some(value) => value.deserialize_option(visitor), + None => visitor.visit_none(), + }, + } + } + + pass!(deserialize_unit, visitor: V); + pass!(deserialize_unit_struct, name: &'static str, visitor: V); + pass!(deserialize_newtype_struct, name: &'static str, visitor: V); + pass!(deserialize_seq, visitor: V); + pass!(deserialize_tuple, len: usize, visitor: V); + + pass!( + deserialize_tuple_struct, + name: &'static str, + len: usize, + visitor: V + ); + + pass!(deserialize_map, visitor: V); + + pass!( + deserialize_struct, + name: &'static str, + fields: &'static [&'static str], + visitor: V + ); + + pass!( + deserialize_enum, + name: &'static str, + fields: &'static [&'static str], + visitor: V + ); + + pass!(deserialize_identifier, visitor: V); + pass!(deserialize_ignored_any, visitor: V); +} + +/// [Autoref-based stable specialization](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md) +/// +/// Nothing in this module is considered stable. +#[doc(hidden)] +pub mod private { + use crate::{ + Runtime, + ipc::{InvokeError, InvokeResolver, InvokeResponseBody, IpcResponse}, + }; + use std::future::Future; + #[cfg(feature = "tracing")] + pub use tracing; + + // ===== impl IpcResponse ===== + + pub struct ResponseTag; + + pub trait ResponseKind { + #[inline(always)] + fn blocking_kind(&self) -> ResponseTag { + ResponseTag + } + + #[inline(always)] + fn async_kind(&self) -> ResponseTag { + ResponseTag + } + } + + impl ResponseKind for &T {} + + impl ResponseTag { + #[inline(always)] + pub fn block(self, value: T, resolver: InvokeResolver) + where + R: Runtime, + T: IpcResponse, + { + resolver.respond(Ok(value)) + } + + #[inline(always)] + pub async fn future(self, value: T) -> Result + where + T: IpcResponse, + { + Ok(value.body()?) + } + } + + // ===== Result> ===== + + pub struct ResultTag; + + pub trait ResultKind { + #[inline(always)] + fn blocking_kind(&self) -> ResultTag { + ResultTag + } + + #[inline(always)] + fn async_kind(&self) -> ResultTag { + ResultTag + } + } + + impl> ResultKind for Result {} + + impl ResultTag { + #[inline(always)] + pub fn block(self, value: Result, resolver: InvokeResolver) + where + R: Runtime, + T: IpcResponse, + E: Into, + { + resolver.respond(value.map_err(Into::into)) + } + + #[inline(always)] + pub async fn future(self, value: Result) -> Result + where + T: IpcResponse, + E: Into, + { + Ok(value.map_err(Into::into)?.body()?) + } + } + + // ===== Future ===== + + pub struct FutureTag; + + pub trait FutureKind { + #[inline(always)] + fn async_kind(&self) -> FutureTag { + FutureTag + } + } + impl> FutureKind for &F {} + + impl FutureTag { + #[inline(always)] + pub async fn future(self, value: F) -> Result + where + T: IpcResponse, + F: Future + Send + 'static, + { + Ok(value.await.body()?) + } + } + + // ===== Future>> ===== + + pub struct ResultFutureTag; + + pub trait ResultFutureKind { + #[inline(always)] + fn async_kind(&self) -> ResultFutureTag { + ResultFutureTag + } + } + + impl, F: Future>> ResultFutureKind + for F + { + } + + impl ResultFutureTag { + #[inline(always)] + pub async fn future(self, value: F) -> Result + where + T: IpcResponse, + E: Into, + F: Future> + Send, + { + let response = value.await.map_err(Into::into)?; + Ok(response.body()?) + } + } +} diff --git a/crates/tauri/src/ipc/format_callback.rs b/crates/tauri/src/ipc/format_callback.rs new file mode 100644 index 0000000..daa77e6 --- /dev/null +++ b/crates/tauri/src/ipc/format_callback.rs @@ -0,0 +1,344 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Serialize; +use serde_json::value::RawValue; +use serialize_to_javascript::Serialized; + +use super::CallbackFn; + +/// The information about this is quite limited. On Chrome/Edge and Firefox, [the maximum string size is approximately 1 GB](https://stackoverflow.com/a/34958490). +/// +/// [From MDN:](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length#description) +/// +/// ECMAScript 2016 (ed. 7) established a maximum length of 2^53 - 1 elements. Previously, no maximum length was specified. +/// +/// In Firefox, strings have a maximum length of 2\*\*30 - 2 (~1GB). In versions prior to Firefox 65, the maximum length was 2\*\*28 - 1 (~256MB). +const MAX_JSON_STR_LEN: usize = usize::pow(2, 30) - 2; + +/// Minimum size JSON needs to be in order to convert it to JSON.parse with [`format_json`]. +// TODO: this number should be benchmarked and checked for optimal range, I set 10 KiB arbitrarily +// we don't want to lose the gained object parsing time to extra allocations preparing it +const MIN_JSON_PARSE_LEN: usize = 10_240; + +/// Transforms & escapes a JSON value. +/// +/// If it's an object or array, JSON.parse('{json}') is used, with the '{json}' string properly escaped. +/// The return value of this function can be safely used on [`eval`](crate::Window#method.eval) calls. +/// +/// Single quotes chosen because double quotes are already used in JSON. With single quotes, we only +/// need to escape strings that include backslashes or single quotes. If we used double quotes, then +/// there would be no cases that a string doesn't need escaping. +/// +/// The function takes a closure to handle the escaped string in order to avoid unnecessary allocations. +/// +/// # Safety +/// +/// The ability to safely escape JSON into a JSON.parse('{json}') relies entirely on 2 things. +/// +/// 1. `serde_json`'s ability to correctly escape and format json into a string. +/// 2. JavaScript engines not accepting anything except another unescaped, literal single quote +/// character to end a string that was opened with it. +fn serialize_js_with String>( + json_string: String, + options: serialize_to_javascript::Options, + cb: F, +) -> crate::Result { + // get a raw &str representation of a serialized json value. + + let raw = RawValue::from_string(json_string)?; + + // from here we know json.len() > 1 because an empty string is not a valid json value. + let json = raw.get(); + let first = json.as_bytes()[0]; + + #[cfg(debug_assertions)] + if first == b'"' { + assert!( + json.len() < MAX_JSON_STR_LEN, + "passing a string larger than the max JavaScript literal string size" + ) + } + + let return_val = if json.len() > MIN_JSON_PARSE_LEN && (first == b'{' || first == b'[') { + let serialized = Serialized::new(&raw, &options).into_string(); + // only use JSON.parse('{arg}') for arrays and objects less than the limit + // smaller literals do not benefit from being parsed from json + if serialized.len() < MAX_JSON_STR_LEN { + cb(&serialized) + } else { + cb(json) + } + } else { + cb(json) + }; + + Ok(return_val) +} + +/// Formats a function name and a serializable argument to be evaluated as callback. +/// +/// See [`format_raw`] for more information. +pub fn format(function_name: CallbackFn, arg: &T) -> crate::Result { + format_raw(function_name, serde_json::to_string(arg)?) +} + +/// Formats a function name and a raw JSON string argument to be evaluated as callback. +/// +/// This will serialize primitive JSON types (e.g. booleans, strings, numbers, etc.) as JavaScript literals, +/// but will serialize arrays and objects whose serialized JSON string is smaller than 1 GB and larger +/// than 10 KiB with `JSON.parse('...')`. +/// See [json-parse-benchmark](https://github.com/GoogleChromeLabs/json-parse-benchmark). +pub fn format_raw(function_name: CallbackFn, json_string: String) -> crate::Result { + let callback_id = function_name.0; + serialize_js_with(json_string, Default::default(), |arg| { + format_raw_js(callback_id, arg) + }) +} + +/// Formats a function name and a JavaScript string argument to be evaluated as callback. +pub fn format_raw_js(callback_id: u32, js: impl AsRef) -> String { + fn format_inner(callback_id: u32, js: &str) -> String { + format!("window.__TAURI_INTERNALS__.runCallback({callback_id}, {js})") + } + format_inner(callback_id, js.as_ref()) +} + +/// Formats a serializable Result type to its Promise response. +/// +/// See [`format_result_raw`] for more information. +pub fn format_result( + result: Result, + success_callback: CallbackFn, + error_callback: CallbackFn, +) -> crate::Result { + match result { + Ok(res) => format(success_callback, &res), + Err(err) => format(error_callback, &err), + } +} + +/// Formats a Result type of raw JSON strings to its Promise response. +/// Useful for Promises handling. +/// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value. +/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value. +/// +/// * `result` the Result to check +/// * `success_callback` the function name of the Ok callback. Usually the `resolve` of the JS Promise. +/// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise. +/// +/// Note that the callback strings are automatically generated by the `invoke` helper. +pub fn format_result_raw( + raw_result: Result, + success_callback: CallbackFn, + error_callback: CallbackFn, +) -> crate::Result { + match raw_result { + Ok(res) => format_raw(success_callback, res), + Err(err) => format_raw(error_callback, err), + } +} + +#[cfg(test)] +mod test { + use super::*; + use quickcheck::{Arbitrary, Gen}; + use quickcheck_macros::quickcheck; + + impl Arbitrary for CallbackFn { + fn arbitrary(g: &mut Gen) -> CallbackFn { + CallbackFn(u32::arbitrary(g)) + } + } + + #[derive(Debug, Clone)] + struct JsonStr(String); + + impl Arbitrary for JsonStr { + fn arbitrary(g: &mut Gen) -> Self { + if bool::arbitrary(g) { + Self(format!( + "{{ {}: {} }}", + serde_json::to_string(&String::arbitrary(g)).unwrap(), + serde_json::to_string(&String::arbitrary(g)).unwrap() + )) + } else { + Self(serde_json::to_string(&String::arbitrary(g)).unwrap()) + } + } + } + + fn serialize_js(value: &T) -> crate::Result { + serialize_js_with(serde_json::to_string(value)?, Default::default(), |v| { + v.into() + }) + } + + fn serialize_js_raw(value: impl Into) -> crate::Result { + serialize_js_with(value.into(), Default::default(), |v| v.into()) + } + + #[test] + fn test_serialize_js() { + assert_eq!(serialize_js(&()).unwrap(), "null"); + assert_eq!(serialize_js(&5i32).unwrap(), "5"); + + #[derive(serde::Serialize)] + struct JsonObj { + value: String, + } + + let raw_str = "T".repeat(MIN_JSON_PARSE_LEN); + assert_eq!(serialize_js(&raw_str).unwrap(), format!("\"{raw_str}\"")); + + assert_eq!( + serialize_js(&JsonObj { + value: raw_str.clone() + }) + .unwrap(), + format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')") + ); + + assert_eq!( + serialize_js(&JsonObj { + value: format!("\"{raw_str}\"") + }) + .unwrap(), + format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')") + ); + + let dangerous_json = RawValue::from_string( + r#"{"test":"don\\🚀🐱‍👤\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don't forget to escape me!","test3":"\\🚀🐱‍👤\\\\'''\\\\🚀🐱‍👤\\\\🚀🐱‍👤\\'''''"}"#.into() + ).unwrap(); + + let definitely_escaped_dangerous_json = format!( + "JSON.parse('{}')", + dangerous_json + .get() + .replace('\\', "\\\\") + .replace('\'', "\\'") + ); + let escape_single_quoted_json_test = + serialize_to_javascript::Serialized::new(&dangerous_json, &Default::default()).into_string(); + + let result = r#"JSON.parse('{"test":"don\\\\🚀🐱‍👤\\\\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don\'t forget to escape me!","test3":"\\\\🚀🐱‍👤\\\\\\\\\'\'\'\\\\\\\\🚀🐱‍👤\\\\\\\\🚀🐱‍👤\\\\\'\'\'\'\'"}')"#; + assert_eq!(definitely_escaped_dangerous_json, result); + assert_eq!(escape_single_quoted_json_test, result); + } + + // check arbitrary strings in the format callback function + #[quickcheck] + fn qc_formatting(f: CallbackFn, a: String) -> bool { + // call format callback + let fc = format(f, &a).unwrap(); + fc.contains(&format!( + "window.__TAURI_INTERNALS__.runCallback({}, JSON.parse('{}'))", + f.0, + serde_json::Value::String(a.clone()), + )) || fc.contains(&format!( + r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#, + f.0, + serde_json::Value::String(a), + )) + } + + // check arbitrary strings in format_result + #[quickcheck] + fn qc_format_res(result: Result, c: CallbackFn, ec: CallbackFn) -> bool { + let resp = format_result(result.clone(), c, ec).expect("failed to format callback result"); + let (function, value) = match result { + Ok(v) => (c, v), + Err(e) => (ec, e), + }; + + resp.contains(&format!( + r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#, + function.0, + serde_json::Value::String(value), + )) + } + + #[test] + fn test_serialize_js_raw() { + assert_eq!(serialize_js_raw("null").unwrap(), "null"); + assert_eq!(serialize_js_raw("5").unwrap(), "5"); + assert_eq!( + serialize_js_raw("{ \"x\": [1, 2, 3] }").unwrap(), + "{ \"x\": [1, 2, 3] }" + ); + + #[derive(serde::Serialize)] + struct JsonObj { + value: String, + } + + let raw_str = "T".repeat(MIN_JSON_PARSE_LEN); + assert_eq!( + serialize_js_raw(format!("\"{raw_str}\"")).unwrap(), + format!("\"{raw_str}\"") + ); + + assert_eq!( + serialize_js_raw(format!("{{\"value\":\"{raw_str}\"}}")).unwrap(), + format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')") + ); + + assert_eq!( + serialize_js(&JsonObj { + value: format!("\"{raw_str}\"") + }) + .unwrap(), + format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')") + ); + + let dangerous_json = RawValue::from_string( + r#"{"test":"don\\🚀🐱‍👤\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don't forget to escape me!","test3":"\\🚀🐱‍👤\\\\'''\\\\🚀🐱‍👤\\\\🚀🐱‍👤\\'''''"}"#.into() + ).unwrap(); + + let definitely_escaped_dangerous_json = format!( + "JSON.parse('{}')", + dangerous_json + .get() + .replace('\\', "\\\\") + .replace('\'', "\\'") + ); + let escape_single_quoted_json_test = + serialize_to_javascript::Serialized::new(&dangerous_json, &Default::default()).into_string(); + + let result = r#"JSON.parse('{"test":"don\\\\🚀🐱‍👤\\\\\'t forget to escape me!🚀🐱‍👤","te🚀🐱‍👤st2":"don\'t forget to escape me!","test3":"\\\\🚀🐱‍👤\\\\\\\\\'\'\'\\\\\\\\🚀🐱‍👤\\\\\\\\🚀🐱‍👤\\\\\'\'\'\'\'"}')"#; + assert_eq!(definitely_escaped_dangerous_json, result); + assert_eq!(escape_single_quoted_json_test, result); + } + + // check arbitrary strings in the format callback function + #[quickcheck] + fn qc_formatting_raw(f: CallbackFn, a: JsonStr) -> bool { + let a = a.0; + // call format callback + let fc = format_raw(f, a.clone()).unwrap(); + fc.contains(&format!( + r#"window.__TAURI_INTERNALS__.runCallback({}, JSON.parse('{}'))"#, + f.0, a + )) || fc.contains(&format!( + r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#, + f.0, a + )) + } + + // check arbitrary strings in format_result + #[quickcheck] + fn qc_format_raw_res(result: Result, c: CallbackFn, ec: CallbackFn) -> bool { + let result = result.map(|v| v.0).map_err(|e| e.0); + let resp = format_result_raw(result.clone(), c, ec).expect("failed to format callback result"); + let (function, value) = match result { + Ok(v) => (c, v), + Err(e) => (ec, e), + }; + + resp.contains(&format!( + r#"window.__TAURI_INTERNALS__.runCallback({}, {})"#, + function.0, value + )) + } +} diff --git a/crates/tauri/src/ipc/mod.rs b/crates/tauri/src/ipc/mod.rs new file mode 100644 index 0000000..824ec6b --- /dev/null +++ b/crates/tauri/src/ipc/mod.rs @@ -0,0 +1,606 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Types and functions related to Inter Procedure Call(IPC). +//! +//! This module includes utilities to send messages to the JS layer of the webview. + +use std::{ + future::Future, + sync::{Arc, Mutex}, +}; + +use http::HeaderMap; +use serde::{ + Deserialize, Serialize, + de::{DeserializeOwned, IntoDeserializer}, +}; +use serde_json::Value as JsonValue; +pub use serialize_to_javascript::Options as SerializeOptions; +use tauri_macros::default_runtime; +use tauri_utils::acl::resolved::ResolvedCommand; + +use crate::{Runtime, StateManager, webview::Webview}; + +mod authority; +#[cfg(feature = "dynamic-acl")] +mod capability_builder; +pub(crate) mod channel; +mod command; +pub(crate) mod format_callback; +pub(crate) mod protocol; + +pub use authority::{ + CommandScope, GlobalScope, Origin, RuntimeAuthority, ScopeObject, ScopeObjectMatch, ScopeValue, +}; +#[cfg(feature = "dynamic-acl")] +pub use capability_builder::{CapabilityBuilder, RuntimeCapability}; +pub use channel::{Channel, JavaScriptChannelId}; +pub use command::{CommandArg, CommandItem, private}; + +/// A closure that is run every time Tauri receives a message it doesn't explicitly handle. +pub type InvokeHandler = dyn Fn(Invoke) -> bool + Send + Sync + 'static; + +/// A closure that is responsible for respond a JS message. +pub type InvokeResponder = + dyn Fn(&Webview, &str, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static; +/// Similar to [`InvokeResponder`] but taking owned arguments. +pub type OwnedInvokeResponder = + dyn FnOnce(Webview, String, InvokeResponse, CallbackFn, CallbackFn) + Send + 'static; + +/// Possible values of an IPC payload. +/// +/// ### Android +/// On Android, [InvokeBody::Raw] is not supported. The enum will always contain [InvokeBody::Json]. +/// When targeting Android Devices, consider passing raw bytes as a base64 [[std::string::String]], which is still more efficient than passing them as a number array in [InvokeBody::Json] +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] +pub enum InvokeBody { + /// Json payload. + Json(JsonValue), + /// Bytes payload. + Raw(Vec), +} + +impl Default for InvokeBody { + fn default() -> Self { + Self::Json(Default::default()) + } +} + +impl From for InvokeBody { + fn from(value: JsonValue) -> Self { + Self::Json(value) + } +} + +impl From> for InvokeBody { + fn from(value: Vec) -> Self { + Self::Raw(value) + } +} + +impl InvokeBody { + #[cfg(mobile)] + pub(crate) fn into_json(self) -> JsonValue { + match self { + Self::Json(v) => v, + Self::Raw(v) => { + JsonValue::Array(v.into_iter().map(|n| JsonValue::Number(n.into())).collect()) + } + } + } +} + +/// Possible values of an IPC response. +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] +pub enum InvokeResponseBody { + /// Json payload. + Json(String), + /// Bytes payload. + Raw(Vec), +} + +impl From for InvokeResponseBody { + fn from(value: String) -> Self { + Self::Json(value) + } +} + +impl From> for InvokeResponseBody { + fn from(value: Vec) -> Self { + Self::Raw(value) + } +} + +impl From for InvokeResponseBody { + fn from(value: InvokeBody) -> Self { + match value { + InvokeBody::Json(v) => Self::Json(serde_json::to_string(&v).unwrap()), + InvokeBody::Raw(v) => Self::Raw(v), + } + } +} + +impl IpcResponse for InvokeResponseBody { + fn body(self) -> crate::Result { + Ok(self) + } +} + +impl InvokeResponseBody { + /// Attempts to deserialize the response. + pub fn deserialize(self) -> serde_json::Result { + match self { + Self::Json(v) => serde_json::from_str(&v), + Self::Raw(v) => T::deserialize(v.into_deserializer()), + } + } +} + +/// The IPC request. +/// +/// Includes the `body` and `headers` parameters of a Tauri command invocation. +/// This allows commands to accept raw bytes - on all platforms except Android. +#[derive(Debug)] +pub struct Request<'a> { + body: &'a InvokeBody, + headers: &'a HeaderMap, +} + +impl Request<'_> { + /// The request body. + pub fn body(&self) -> &InvokeBody { + self.body + } + + /// The request headers. + pub fn headers(&self) -> &HeaderMap { + self.headers + } +} + +impl<'a, R: Runtime> CommandArg<'a, R> for Request<'a> { + /// Returns the invoke [`Request`]. + fn from_command(command: CommandItem<'a, R>) -> Result { + Ok(Self { + body: command.message.payload(), + headers: command.message.headers(), + }) + } +} + +/// Marks a type as a response to an IPC call. +pub trait IpcResponse { + /// Resolve the IPC response body. + fn body(self) -> crate::Result; +} + +impl IpcResponse for T { + fn body(self) -> crate::Result { + serde_json::to_string(&self) + .map(Into::into) + .map_err(Into::into) + } +} + +/// The IPC response. +pub struct Response { + body: InvokeResponseBody, +} + +impl IpcResponse for Response { + fn body(self) -> crate::Result { + Ok(self.body) + } +} + +impl Response { + /// Defines a response with the given body. + pub fn new(body: impl Into) -> Self { + Self { body: body.into() } + } +} + +/// The message and resolver given to a custom command. +/// +/// This struct is used internally by macros and is explicitly **NOT** stable. +#[default_runtime(crate::Wry, wry)] +pub struct Invoke { + /// The message passed. + pub message: InvokeMessage, + + /// The resolver of the message. + pub resolver: InvokeResolver, + + /// Resolved ACL for this IPC invoke. + pub acl: Option>, +} + +/// Error response from an [`InvokeMessage`]. +#[derive(Debug)] +pub struct InvokeError(pub serde_json::Value); + +impl InvokeError { + /// Create an [`InvokeError`] as a string of the [`std::error::Error`] message. + #[inline(always)] + pub fn from_error(error: E) -> Self { + Self(serde_json::Value::String(error.to_string())) + } + + /// Create an [`InvokeError`] as a string of the [`anyhow::Error`] message. + #[inline(always)] + pub fn from_anyhow(error: anyhow::Error) -> Self { + Self(serde_json::Value::String(format!("{error:#}"))) + } +} + +impl From for InvokeError { + #[inline] + fn from(value: T) -> Self { + serde_json::to_value(value) + .map(Self) + .unwrap_or_else(Self::from_error) + } +} + +impl From for InvokeError { + #[inline(always)] + fn from(error: crate::Error) -> Self { + Self(serde_json::Value::String(error.to_string())) + } +} + +/// Response from a [`InvokeMessage`] passed to the [`InvokeResolver`]. +#[derive(Debug)] +pub enum InvokeResponse { + /// Resolve the promise. + Ok(InvokeResponseBody), + /// Reject the promise. + Err(InvokeError), +} + +impl> From> for InvokeResponse { + #[inline] + fn from(result: Result) -> Self { + match result { + Ok(ok) => match ok.body() { + Ok(value) => Self::Ok(value), + Err(err) => Self::Err(InvokeError::from_error(err)), + }, + Err(err) => Self::Err(err.into()), + } + } +} + +impl From for InvokeResponse { + fn from(error: InvokeError) -> Self { + Self::Err(error) + } +} + +/// Resolver of a invoke message. +#[default_runtime(crate::Wry, wry)] +pub struct InvokeResolver { + webview: Webview, + responder: Arc>>>>, + cmd: String, + pub(crate) callback: CallbackFn, + pub(crate) error: CallbackFn, +} + +impl Clone for InvokeResolver { + fn clone(&self) -> Self { + Self { + webview: self.webview.clone(), + responder: self.responder.clone(), + cmd: self.cmd.clone(), + callback: self.callback, + error: self.error, + } + } +} + +impl InvokeResolver { + pub(crate) fn new( + webview: Webview, + responder: Arc>>>>, + cmd: String, + callback: CallbackFn, + error: CallbackFn, + ) -> Self { + Self { + webview, + responder, + cmd, + callback, + error, + } + } + + /// Reply to the invoke promise with an async task. + pub fn respond_async(self, task: F) + where + T: IpcResponse, + F: Future> + Send + 'static, + { + crate::async_runtime::spawn(async move { + Self::return_task( + self.webview, + self.responder, + task, + self.cmd, + self.callback, + self.error, + ) + .await; + }); + } + + /// Reply to the invoke promise with an async task which is already serialized. + pub fn respond_async_serialized(self, task: F) + where + F: Future> + Send + 'static, + { + // Dynamic dispatch the call in dev for a faster compile time + // TODO: Revisit this and see if we can do this for the release build as well if the performance hit is not a problem + #[cfg(debug_assertions)] + { + self.respond_async_serialized_dyn(Box::pin(task)) + } + #[cfg(not(debug_assertions))] + { + self.respond_async_serialized_inner(task) + } + } + + /// Dynamic dispatch the [`Self::respond_async_serialized`] call + #[cfg(debug_assertions)] + fn respond_async_serialized_dyn( + self, + task: std::pin::Pin< + Box> + Send + 'static>, + >, + ) { + self.respond_async_serialized_inner(task) + } + + /// Reply to the invoke promise with an async task which is already serialized. + fn respond_async_serialized_inner(self, task: F) + where + F: Future> + Send + 'static, + { + crate::async_runtime::spawn(async move { + let response = match task.await { + Ok(ok) => InvokeResponse::Ok(ok), + Err(err) => InvokeResponse::Err(err), + }; + Self::return_result( + self.webview, + self.responder, + response, + self.cmd, + self.callback, + self.error, + ) + }); + } + + /// Reply to the invoke promise with a serializable value. + pub fn respond(self, value: Result) { + Self::return_result( + self.webview, + self.responder, + value.into(), + self.cmd, + self.callback, + self.error, + ) + } + + /// Resolve the invoke promise with a value. + pub fn resolve(self, value: T) { + self.respond(Ok(value)) + } + + /// Reject the invoke promise with a value. + pub fn reject(self, value: T) { + Self::return_result( + self.webview, + self.responder, + Result::<(), _>::Err(value).into(), + self.cmd, + self.callback, + self.error, + ) + } + + /// Reject the invoke promise with an [`InvokeError`]. + pub fn invoke_error(self, error: InvokeError) { + Self::return_result( + self.webview, + self.responder, + error.into(), + self.cmd, + self.callback, + self.error, + ) + } + + /// Asynchronously executes the given task + /// and evaluates its Result to the JS promise described by the `success_callback` and `error_callback` function names. + /// + /// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value. + /// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value. + pub async fn return_task( + webview: Webview, + responder: Arc>>>>, + task: F, + cmd: String, + success_callback: CallbackFn, + error_callback: CallbackFn, + ) where + T: IpcResponse, + F: Future> + Send + 'static, + { + let result = task.await; + Self::return_closure( + webview, + responder, + || result, + cmd, + success_callback, + error_callback, + ) + } + + pub(crate) fn return_closure Result>( + webview: Webview, + responder: Arc>>>>, + f: F, + cmd: String, + success_callback: CallbackFn, + error_callback: CallbackFn, + ) { + Self::return_result( + webview, + responder, + f().into(), + cmd, + success_callback, + error_callback, + ) + } + + pub(crate) fn return_result( + webview: Webview, + responder: Arc>>>>, + response: InvokeResponse, + cmd: String, + success_callback: CallbackFn, + error_callback: CallbackFn, + ) { + (responder.lock().unwrap().take().expect("resolver consumed"))( + webview, + cmd, + response, + success_callback, + error_callback, + ); + } +} + +/// An invoke message. +#[default_runtime(crate::Wry, wry)] +#[derive(Debug)] +pub struct InvokeMessage { + /// The webview that received the invoke message. + pub(crate) webview: Webview, + /// The IPC command. + pub(crate) command: String, + /// The JSON argument passed on the invoke message. + pub(crate) payload: InvokeBody, + /// The request headers. + pub(crate) headers: HeaderMap, +} + +impl Clone for InvokeMessage { + fn clone(&self) -> Self { + Self { + webview: self.webview.clone(), + command: self.command.clone(), + payload: self.payload.clone(), + headers: self.headers.clone(), + } + } +} + +impl InvokeMessage { + /// Create an new [`InvokeMessage`] from a payload send by a webview. + pub(crate) fn new( + webview: Webview, + command: String, + payload: InvokeBody, + headers: HeaderMap, + ) -> Self { + Self { + webview, + command, + payload, + headers, + } + } + + /// The invoke command. + #[inline(always)] + pub fn command(&self) -> &str { + &self.command + } + + /// The webview that received the invoke. + #[inline(always)] + pub fn webview(&self) -> Webview { + self.webview.clone() + } + + /// A reference to webview that received the invoke. + #[inline(always)] + pub fn webview_ref(&self) -> &Webview { + &self.webview + } + + /// A reference to the payload the invoke received. + #[inline(always)] + pub fn payload(&self) -> &InvokeBody { + &self.payload + } + + // TODO: make private or remove in v3 + /// The state manager associated with the application + #[deprecated(note = "Use `Manager::state` to access the state: `self.webview_ref().state()`")] + #[inline(always)] + pub fn state(&self) -> Arc { + self.webview.manager.state.clone() + } + + // TODO: make private or remove in v3 + /// A reference to the state manager associated with application. + #[deprecated(note = "Use `Manager::state` to access the state: `self.webview_ref().state()`")] + #[inline(always)] + pub fn state_ref(&self) -> &StateManager { + &self.webview.manager.state + } + + /// The request headers. + #[inline(always)] + pub fn headers(&self) -> &HeaderMap { + &self.headers + } +} + +/// The `Callback` type is the return value of the `transformCallback` JavaScript function. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct CallbackFn(pub u32); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserialize_invoke_response_body() { + let json = InvokeResponseBody::Json("[1, 123, 1231]".to_string()); + assert_eq!(json.deserialize::>().unwrap(), vec![1, 123, 1231]); + + let json = InvokeResponseBody::Json("\"string value\"".to_string()); + assert_eq!(json.deserialize::().unwrap(), "string value"); + + let json = InvokeResponseBody::Json("\"string value\"".to_string()); + assert!(json.deserialize::>().is_err()); + + let values = vec![1, 2, 3, 4, 5, 6, 1]; + let raw = InvokeResponseBody::Raw(values.clone()); + assert_eq!(raw.deserialize::>().unwrap(), values); + } +} diff --git a/crates/tauri/src/ipc/protocol.rs b/crates/tauri/src/ipc/protocol.rs new file mode 100644 index 0000000..cdfe325 --- /dev/null +++ b/crates/tauri/src/ipc/protocol.rs @@ -0,0 +1,753 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{borrow::Cow, sync::Arc}; + +use crate::{ + Runtime, + ipc::InvokeResponseBody, + manager::AppManager, + webview::{InvokeRequest, UriSchemeProtocolHandler}, +}; +use http::{ + HeaderValue, Method, Request, StatusCode, + header::{ + ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS, + CONTENT_TYPE, + }, +}; +use url::Url; + +use super::{CallbackFn, InvokeResponse}; + +const TAURI_CALLBACK_HEADER_NAME: &str = "Tauri-Callback"; +const TAURI_ERROR_HEADER_NAME: &str = "Tauri-Error"; +const TAURI_INVOKE_KEY_HEADER_NAME: &str = "Tauri-Invoke-Key"; + +const TAURI_RESPONSE_HEADER_NAME: &str = "Tauri-Response"; +const TAURI_RESPONSE_HEADER_ERROR: &str = "error"; +const TAURI_RESPONSE_HEADER_OK: &str = "ok"; + +pub fn message_handler( + manager: Arc>, +) -> crate::runtime::webview::WebviewIpcHandler { + Box::new(move |webview, request| handle_ipc_message(request, &manager, &webview.label)) +} + +pub fn get(manager: Arc>) -> UriSchemeProtocolHandler { + Box::new(move |label, request, responder| { + #[cfg(feature = "tracing")] + let span = tracing::trace_span!( + "ipc::request", + kind = "custom-protocol", + request = tracing::field::Empty + ) + .entered(); + + let respond = move |mut response: http::Response>| { + response + .headers_mut() + .insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*")); + response.headers_mut().insert( + ACCESS_CONTROL_EXPOSE_HEADERS, + HeaderValue::from_static(TAURI_RESPONSE_HEADER_NAME), + ); + responder.respond(response); + }; + + match *request.method() { + Method::POST => { + if let Some(webview) = manager.get_webview(label) { + match parse_invoke_request(&manager, request) { + Ok(request) => { + #[cfg(feature = "tracing")] + span.record( + "request", + match &request.body { + super::InvokeBody::Json(j) => serde_json::to_string(j).unwrap(), + super::InvokeBody::Raw(b) => serde_json::to_string(b).unwrap(), + }, + ); + #[cfg(feature = "tracing")] + let request_span = tracing::trace_span!("ipc::request::handle", cmd = request.cmd); + + webview.on_message( + request, + Box::new(move |_webview, _cmd, response, _callback, _error| { + #[cfg(feature = "tracing")] + let _respond_span = tracing::trace_span!( + parent: &request_span, + "ipc::request::respond" + ) + .entered(); + + #[cfg(feature = "tracing")] + let response_span = match &response { + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => tracing::trace_span!( + "ipc::request::response", + response = v, + mime_type = tracing::field::Empty + ) + .entered(), + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => tracing::trace_span!( + "ipc::request::response", + response = format!("{v:?}"), + mime_type = tracing::field::Empty + ) + .entered(), + InvokeResponse::Err(e) => tracing::trace_span!( + "ipc::request::response", + error = format!("{e:?}"), + mime_type = tracing::field::Empty + ) + .entered(), + }; + + let response_header = match &response { + InvokeResponse::Ok(_) => TAURI_RESPONSE_HEADER_OK, + InvokeResponse::Err(_) => TAURI_RESPONSE_HEADER_ERROR, + }; + + let (mut response, mime_type) = match response { + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => ( + http::Response::new(v.into_bytes().into()), + mime::APPLICATION_JSON, + ), + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => ( + http::Response::new(v.into()), + mime::APPLICATION_OCTET_STREAM, + ), + InvokeResponse::Err(e) => ( + http::Response::new(serde_json::to_vec(&e.0).unwrap().into()), + mime::APPLICATION_JSON, + ), + }; + + response + .headers_mut() + .insert(TAURI_RESPONSE_HEADER_NAME, response_header.parse().unwrap()); + + #[cfg(feature = "tracing")] + response_span.record("mime_type", mime_type.essence_str()); + + response.headers_mut().insert( + CONTENT_TYPE, + HeaderValue::from_str(mime_type.essence_str()).unwrap(), + ); + + respond(response); + }), + ); + } + Err(e) => { + respond( + http::Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + .body(e.into_bytes().into()) + .unwrap(), + ); + } + } + } else { + respond( + http::Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + .body("failed to acquire webview reference".as_bytes().into()) + .unwrap(), + ); + } + } + + Method::OPTIONS => { + let mut r = http::Response::new(Vec::new().into()); + r.headers_mut() + .insert(ACCESS_CONTROL_ALLOW_HEADERS, HeaderValue::from_static("*")); + + respond(r); + } + + _ => { + let mut r = http::Response::new("only POST and OPTIONS are allowed".as_bytes().into()); + *r.status_mut() = StatusCode::METHOD_NOT_ALLOWED; + r.headers_mut().insert( + CONTENT_TYPE, + HeaderValue::from_str(mime::TEXT_PLAIN.essence_str()).unwrap(), + ); + respond(r); + } + } + }) +} + +fn handle_ipc_message(request: Request, manager: &AppManager, label: &str) { + let Some(webview) = manager.get_webview(label) else { + return; + }; + + #[cfg(feature = "tracing")] + let _span = tracing::trace_span!( + "ipc::request", + kind = "post-message", + uri = request.uri().to_string(), + request = request.body() + ) + .entered(); + + use serde::{Deserialize, Deserializer}; + + #[derive(Default)] + pub(crate) struct HeaderMap(http::HeaderMap); + + impl<'de> Deserialize<'de> for HeaderMap { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let map = std::collections::HashMap::::deserialize(deserializer)?; + let mut headers = http::HeaderMap::default(); + for (key, value) in map { + if let (Ok(key), Ok(value)) = ( + http::header::HeaderName::from_bytes(key.as_bytes()), + http::HeaderValue::from_str(&value), + ) { + headers.insert(key, value); + } else { + return Err(serde::de::Error::custom(format!( + "invalid header `{key}` `{value}`" + ))); + } + } + Ok(Self(headers)) + } + } + + #[derive(Deserialize, Default)] + #[serde(rename_all = "camelCase")] + struct RequestOptions { + #[serde(default)] + headers: HeaderMap, + #[serde(default)] + custom_protocol_ipc_blocked: bool, + } + + #[derive(Deserialize)] + struct Message { + cmd: String, + callback: CallbackFn, + error: CallbackFn, + payload: serde_json::Value, + options: Option, + #[serde(rename = "__TAURI_INVOKE_KEY__")] + invoke_key: String, + } + + #[allow(unused_mut)] + let mut invoke_message: Option> = None; + + #[cfg(feature = "isolation")] + { + #[derive(Deserialize)] + struct IsolationMessage<'a> { + cmd: String, + callback: CallbackFn, + error: CallbackFn, + payload: crate::utils::pattern::isolation::RawIsolationPayload<'a>, + options: Option, + #[serde(rename = "__TAURI_INVOKE_KEY__")] + invoke_key: String, + } + + if let crate::Pattern::Isolation { crypto_keys, .. } = &*manager.pattern { + #[cfg(feature = "tracing")] + let _span = tracing::trace_span!("ipc::request::decrypt_isolation_payload").entered(); + + invoke_message.replace( + serde_json::from_str::>(request.body()) + .map_err(Into::into) + .and_then(|message| { + let is_raw = + message.payload.content_type() == &mime::APPLICATION_OCTET_STREAM.to_string(); + let payload = crypto_keys.decrypt(message.payload)?; + Ok(Message { + cmd: message.cmd, + callback: message.callback, + error: message.error, + payload: if is_raw { + payload.into() + } else { + serde_json::from_slice(&payload)? + }, + options: message.options, + invoke_key: message.invoke_key, + }) + }), + ); + } + } + + let message = invoke_message.unwrap_or_else(|| { + #[cfg(feature = "tracing")] + let _span = tracing::trace_span!("ipc::request::deserialize").entered(); + serde_json::from_str::(request.body()).map_err(Into::into) + }); + + match message { + Ok(message) => { + let options = message.options.unwrap_or_default(); + + let request = InvokeRequest { + cmd: message.cmd, + callback: message.callback, + error: message.error, + url: Url::parse(&request.uri().to_string()).expect("invalid IPC request URL"), + body: message.payload.into(), + headers: options.headers.0, + invoke_key: message.invoke_key, + }; + + #[cfg(feature = "tracing")] + let request_span = tracing::trace_span!("ipc::request::handle", cmd = request.cmd); + + webview.on_message( + request, + Box::new(move |webview, cmd, response, callback, error| { + use crate::ipc::Channel; + + #[cfg(feature = "tracing")] + let _respond_span = tracing::trace_span!( + parent: &request_span, + "ipc::request::respond" + ) + .entered(); + + fn responder_eval( + webview: &crate::Webview, + js: crate::Result, + error: CallbackFn, + ) { + let eval_js = js.unwrap_or_else(|e| { + crate::ipc::format_callback::format(error, &e.to_string()) + .expect("unable to serialize response error string to json") + }); + let _ = webview.eval(eval_js); + } + + let can_use_channel_for_response = cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND + && !options.custom_protocol_ipc_blocked; + + #[cfg(feature = "tracing")] + let mime_type = match &response { + InvokeResponse::Ok(InvokeResponseBody::Json(_)) => mime::APPLICATION_JSON, + InvokeResponse::Ok(InvokeResponseBody::Raw(_)) => mime::APPLICATION_OCTET_STREAM, + InvokeResponse::Err(_) => mime::APPLICATION_JSON, + }; + + #[cfg(feature = "tracing")] + let _response_span = match &response { + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => tracing::trace_span!( + "ipc::request::response", + response = v, + mime_type = mime_type.essence_str() + ) + .entered(), + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => tracing::trace_span!( + "ipc::request::response", + response = format!("{v:?}"), + mime_type = mime_type.essence_str() + ) + .entered(), + InvokeResponse::Err(e) => tracing::trace_span!( + "ipc::request::response", + response = format!("{e:?}"), + mime_type = mime_type.essence_str() + ) + .entered(), + }; + + match response { + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => { + if !(cfg!(target_os = "macos") || cfg!(target_os = "ios")) + && (v.starts_with('{') || v.starts_with('[')) + && can_use_channel_for_response + { + let _ = + Channel::from_callback_fn(webview, callback).send(InvokeResponseBody::Json(v)); + } else { + responder_eval( + &webview, + crate::ipc::format_callback::format_result_raw( + Result::<_, String>::Ok(v), + callback, + error, + ), + error, + ) + } + } + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => { + if cfg!(target_os = "macos") + || cfg!(target_os = "ios") + || !can_use_channel_for_response + { + responder_eval( + &webview, + crate::ipc::format_callback::format_result( + Result::<_, ()>::Ok(v), + callback, + error, + ), + error, + ); + } else { + let _ = + Channel::from_callback_fn(webview, callback).send(InvokeResponseBody::Raw(v)); + } + } + InvokeResponse::Err(e) => responder_eval( + &webview, + crate::ipc::format_callback::format_result( + Result::<(), _>::Err(&e.0), + callback, + error, + ), + error, + ), + } + }), + ); + } + Err(e) => { + #[cfg(feature = "tracing")] + tracing::trace!("ipc.request.error {e}"); + + let _ = webview.eval(format!( + r#"console.error({})"#, + serde_json::Value::String(e.to_string()) + )); + } + } +} + +fn parse_invoke_request( + #[allow(unused_variables)] manager: &AppManager, + request: http::Request>, +) -> std::result::Result { + #[allow(unused_mut)] + let (parts, mut body) = request.into_parts(); + + // skip leading `/` + let cmd = percent_encoding::percent_decode(&parts.uri.path().as_bytes()[1..]) + .decode_utf8_lossy() + .to_string(); + + // on Android we cannot read the request body + // so we must ignore it because some commands use the IPC for faster response + let has_payload = !body.is_empty(); + + #[allow(unused_mut)] + let mut content_type = parts + .headers + .get(http::header::CONTENT_TYPE) + .and_then(|h| h.to_str().ok()) + .map(|mime| mime.parse()) + .unwrap_or(Ok(mime::APPLICATION_OCTET_STREAM)) + .map_err(|_| "unknown content type")?; + + #[cfg(feature = "isolation")] + if let crate::Pattern::Isolation { crypto_keys, .. } = &*manager.pattern { + // if the platform does not support request body, we ignore it + if has_payload { + #[cfg(feature = "tracing")] + let _span = tracing::trace_span!("ipc::request::decrypt_isolation_payload").entered(); + + (body, content_type) = crate::utils::pattern::isolation::RawIsolationPayload::try_from(&body) + .and_then(|raw| { + 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())?; + } + } + + let invoke_key = parts + .headers + .get(TAURI_INVOKE_KEY_HEADER_NAME) + .ok_or("missing Tauri-Invoke-Key header")? + .to_str() + .map_err(|_| "Tauri invoke key header value must be a string")? + .to_owned(); + + let url = Url::parse( + parts + .headers + .get("Origin") + .ok_or("missing Origin header")? + .to_str() + .map_err(|_| "Origin header value must be a string")?, + ) + .map_err(|_| "Origin header is not a valid URL")?; + + let callback = CallbackFn( + parts + .headers + .get(TAURI_CALLBACK_HEADER_NAME) + .ok_or("missing Tauri-Callback header")? + .to_str() + .map_err(|_| "Tauri callback header value must be a string")? + .parse() + .map_err(|_| "Tauri callback header value must be a numeric string")?, + ); + let error = CallbackFn( + parts + .headers + .get(TAURI_ERROR_HEADER_NAME) + .ok_or("missing Tauri-Error header")? + .to_str() + .map_err(|_| "Tauri error header value must be a string")? + .parse() + .map_err(|_| "Tauri error header value must be a numeric string")?, + ); + + #[cfg(feature = "tracing")] + let span = tracing::trace_span!("ipc::request::deserialize").entered(); + + let body = if content_type == mime::APPLICATION_OCTET_STREAM { + body.into() + } else if content_type == mime::APPLICATION_JSON { + // if the platform does not support request body, we ignore it + if has_payload { + serde_json::from_slice::(&body) + .map_err(|e| e.to_string())? + .into() + } else { + serde_json::Value::Object(Default::default()).into() + } + } else { + return Err(format!("content type {content_type} is not implemented")); + }; + + #[cfg(feature = "tracing")] + drop(span); + + let payload = InvokeRequest { + cmd, + callback, + error, + url, + body, + headers: parts.headers, + invoke_key, + }; + + Ok(payload) +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + use crate::{StateManager, Wry, ipc::InvokeBody, manager::AppManager, plugin::PluginStore}; + use http::header::*; + use serde_json::json; + use tauri_macros::generate_context; + + #[test] + fn parse_invoke_request() { + let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate, test = true); + let manager: AppManager = AppManager::with_handlers( + context, + PluginStore::default(), + Box::new(|_| false), + None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + None, + Default::default(), + Default::default(), + StateManager::new(), + Default::default(), + #[cfg(all(desktop, feature = "tray-icon"))] + Default::default(), + Default::default(), + Default::default(), + Default::default(), + "".into(), + None, + crate::generate_invoke_key().unwrap(), + ); + + let cmd = "write_something"; + let url = "tauri://localhost"; + let invoke_key = "1234ahdsjkl123"; + let callback = 12378123; + let error = 6243; + let mut headers = HeaderMap::from_iter(vec![ + ( + CONTENT_TYPE, + HeaderValue::from_str(mime::APPLICATION_OCTET_STREAM.as_ref()).unwrap(), + ), + ( + HeaderName::from_str(TAURI_INVOKE_KEY_HEADER_NAME).unwrap(), + HeaderValue::from_str(invoke_key).unwrap(), + ), + ( + HeaderName::from_str(TAURI_CALLBACK_HEADER_NAME).unwrap(), + HeaderValue::from_str(&callback.to_string()).unwrap(), + ), + ( + HeaderName::from_str(TAURI_ERROR_HEADER_NAME).unwrap(), + HeaderValue::from_str(&error.to_string()).unwrap(), + ), + (ORIGIN, HeaderValue::from_str("tauri://localhost").unwrap()), + ]); + + let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}")); + *request.headers_mut().unwrap() = headers.clone(); + + let body = vec![123, 31, 45]; + let request = request.body(body.clone()).unwrap(); + let invoke_request = super::parse_invoke_request(&manager, request).unwrap(); + + assert_eq!(invoke_request.cmd, cmd); + assert_eq!(invoke_request.callback.0, callback); + assert_eq!(invoke_request.error.0, error); + assert_eq!(invoke_request.invoke_key, invoke_key); + assert_eq!(invoke_request.url, url.parse().unwrap()); + assert_eq!(invoke_request.headers, headers); + assert_eq!(invoke_request.body, InvokeBody::Raw(body)); + + let body = json!({ + "key": 1, + "anotherKey": "asda", + }); + + headers.insert( + CONTENT_TYPE, + HeaderValue::from_str(mime::APPLICATION_JSON.as_ref()).unwrap(), + ); + + let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}")); + *request.headers_mut().unwrap() = headers.clone(); + + let request = request.body(serde_json::to_vec(&body).unwrap()).unwrap(); + let invoke_request = super::parse_invoke_request(&manager, request).unwrap(); + + assert_eq!(invoke_request.headers, headers); + assert_eq!(invoke_request.body, InvokeBody::Json(body)); + } + + #[test] + #[cfg(feature = "isolation")] + fn parse_invoke_request_isolation() { + let context = generate_context!( + "test/fixture/isolation/src-tauri/tauri.conf.json", + crate, + test = false + ); + + let crate::pattern::Pattern::Isolation { crypto_keys, .. } = &context.pattern else { + unreachable!() + }; + + let mut nonce = [0u8; 12]; + getrandom::fill(&mut nonce).unwrap(); + + let body_raw = vec![1, 41, 65, 12, 78]; + let body_bytes = crypto_keys.aes_gcm().encrypt(&nonce, &body_raw).unwrap(); + let isolation_payload_raw = json!({ + "nonce": nonce, + "payload": body_bytes, + "contentType": mime::APPLICATION_OCTET_STREAM.to_string(), + }); + + let body_json = json!({ + "key": 1, + "anotherKey": "string" + }); + let body_bytes = crypto_keys + .aes_gcm() + .encrypt(&nonce, &serde_json::to_vec(&body_json).unwrap()) + .unwrap(); + let isolation_payload_json = json!({ + "nonce": nonce, + "payload": body_bytes, + "contentType": mime::APPLICATION_JSON.to_string(), + }); + + let manager: AppManager = AppManager::with_handlers( + context, + PluginStore::default(), + Box::new(|_| false), + None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + None, + Default::default(), + Default::default(), + StateManager::new(), + Default::default(), + #[cfg(all(desktop, feature = "tray-icon"))] + Default::default(), + Default::default(), + Default::default(), + Default::default(), + "".into(), + None, + crate::generate_invoke_key().unwrap(), + ); + + let cmd = "write_something"; + let url = "tauri://localhost"; + let invoke_key = "1234ahdsjkl123"; + let callback = 12378123; + let error = 6243; + + let headers = HeaderMap::from_iter(vec![ + ( + CONTENT_TYPE, + HeaderValue::from_str(mime::APPLICATION_JSON.as_ref()).unwrap(), + ), + ( + HeaderName::from_str(TAURI_INVOKE_KEY_HEADER_NAME).unwrap(), + HeaderValue::from_str(invoke_key).unwrap(), + ), + ( + HeaderName::from_str(TAURI_CALLBACK_HEADER_NAME).unwrap(), + HeaderValue::from_str(&callback.to_string()).unwrap(), + ), + ( + HeaderName::from_str(TAURI_ERROR_HEADER_NAME).unwrap(), + HeaderValue::from_str(&error.to_string()).unwrap(), + ), + (ORIGIN, HeaderValue::from_str("tauri://localhost").unwrap()), + ]); + + let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}")); + *request.headers_mut().unwrap() = headers.clone(); + let body = serde_json::to_vec(&isolation_payload_raw).unwrap(); + let request = request.body(body).unwrap(); + let invoke_request = super::parse_invoke_request(&manager, request).unwrap(); + + assert_eq!(invoke_request.cmd, cmd); + assert_eq!(invoke_request.callback.0, callback); + assert_eq!(invoke_request.error.0, error); + assert_eq!(invoke_request.invoke_key, invoke_key); + assert_eq!(invoke_request.url, url.parse().unwrap()); + assert_eq!(invoke_request.headers, headers); + assert_eq!(invoke_request.body, InvokeBody::Raw(body_raw)); + + let mut request = Request::builder().uri(format!("ipc://localhost/{cmd}")); + *request.headers_mut().unwrap() = headers.clone(); + let body = serde_json::to_vec(&isolation_payload_json).unwrap(); + let request = request.body(body).unwrap(); + let invoke_request = super::parse_invoke_request(&manager, request).unwrap(); + + assert_eq!(invoke_request.headers, headers); + assert_eq!(invoke_request.body, InvokeBody::Json(body_json)); + } +} diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs new file mode 100644 index 0000000..6156c26 --- /dev/null +++ b/crates/tauri/src/lib.rs @@ -0,0 +1,1267 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Tauri is a framework for building tiny, blazing fast binaries for all major desktop platforms. +//! Developers can integrate any front-end framework that compiles to HTML, JS and CSS for building their user interface. +//! The backend of the application is a rust-sourced binary with an API that the front-end can interact with. +//! +//! # Cargo features +//! +//! The following are a list of [Cargo features](https://doc.rust-lang.org/stable/cargo/reference/manifest.html#the-features-section) that can be enabled or disabled: +//! +//! - **wry** *(enabled by default)*: Enables the [wry](https://github.com/tauri-apps/wry) runtime. Only disable it if you want a custom 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. +//! It unlocks the multiwebview APIs, where a [`Window`] (the native window) and a [`Webview`] (the web content it hosts) +//! are separate objects instead of the single [`WebviewWindow`] you get by default: +//! - `window::WindowBuilder` and `Window::builder` to create a window that has no webview attached to it; +//! - `webview::WebviewBuilder` and `Window::add_child` to attach one or more webviews to an existing window; +//! - [`Webview::reparent`] to move a webview to another window (it always fails on a [`WebviewWindow`] webview +//! when this feature is disabled) and [`Webview::set_auto_resize`] to make a child webview follow the window size; +//! - the [`Manager`] getters for these objects: `get_window`, `get_focused_window`, `windows`, `get_webview` and `webviews`. +//! +//! See the [multiwebview example](https://github.com/tauri-apps/tauri/tree/dev/examples/multiwebview). +//! - **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. +//! - **objc-exception**: This feature flag is no-op since 2.3.0. +//! - **linux-libxdo**: Enables linking to libxdo which enables Cut, Copy, Paste and SelectAll menu items to work on Linux. +//! - **isolation**: Enables the isolation pattern. Enabled by default if the `app > security > pattern > use` config option is set to `isolation` on the `tauri.conf.json` file. +//! - **custom-protocol**: Feature managed by the Tauri CLI. When enabled, Tauri assumes a production environment instead of a development one. +//! - **devtools**: Enables the developer tools (Web inspector) and [`window::Window#method.open_devtools`]. Enabled by default on debug builds. +//! On macOS it uses private APIs, so you can't enable it if your app will be published to the App Store. +//! - **native-tls**: Provides TLS support to connect over HTTPS. +//! - **native-tls-vendored**: Compile and statically link to a vendored copy of OpenSSL. +//! - **rustls-tls**: Provides TLS support to connect over HTTPS using rustls. +//! - **process-relaunch-dangerous-allow-symlink-macos**: Allows the [`process::current_binary`] function to allow symlinks on macOS (this is dangerous, see the Security section in the documentation website). +//! - **tray-icon**: Enables application tray icon APIs. Enabled by default if the `trayIcon` config is defined on the `tauri.conf.json` file. +//! - **macos-private-api**: Enables features only available in **macOS**'s private APIs, currently the `transparent` window functionality and the `fullScreenEnabled` preference setting to `true`. Enabled by default if the `tauri > macosPrivateApi` config flag is set to `true` on the `tauri.conf.json` file. +//! - **webview-data-url**: Enables usage of data URLs on the webview. +//! - **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`]. +//! - **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`], [`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 +//! +//! The following are a list of [Cargo features](https://doc.rust-lang.org/stable/cargo/reference/manifest.html#the-features-section) that enables commands for Tauri's API package. +//! These features are automatically enabled by the Tauri CLI based on the `allowlist` configuration under `tauri.conf.json`. +//! +//! ### Protocol allowlist +//! +//! - **protocol-asset**: Enables the `asset` custom protocol. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" +)] +#![warn(missing_docs, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +/// Setups the binding that initializes an iOS plugin. +#[cfg(target_os = "ios")] +#[macro_export] +macro_rules! ios_plugin_binding { + ($fn_name: ident) => { + tauri::swift_rs::swift!(fn $fn_name() -> *const ::std::ffi::c_void); + } +} +#[cfg(target_os = "macos")] +#[doc(hidden)] +pub use embed_plist; +pub use error::{Error, Result}; +use ipc::RuntimeAuthority; +#[cfg(feature = "dynamic-acl")] +use ipc::RuntimeCapability; +pub use resources::{Resource, ResourceId, ResourceTable}; +#[cfg(target_os = "ios")] +#[doc(hidden)] +pub use swift_rs; +pub use tauri_macros::include_image; +#[cfg(mobile)] +pub use tauri_macros::mobile_entry_point; +pub use tauri_macros::{command, generate_handler}; + +use tauri_utils::assets::AssetsIter; +pub use url::Url; + +pub(crate) mod app; +pub mod async_runtime; +mod error; +mod event; +pub mod ipc; +mod manager; +mod pattern; +pub mod plugin; +pub(crate) mod protocol; +mod resources; +mod vibrancy; +pub mod webview; +pub mod window; +use tauri_runtime as runtime; +pub mod image; +#[cfg(target_os = "ios")] +mod ios; +#[cfg(desktop)] +#[cfg_attr(docsrs, doc(cfg(desktop)))] +pub mod menu; +/// Path APIs. +pub mod path; +pub mod process; +/// The allowlist scopes. +pub mod scope; +mod state; + +#[cfg(all(desktop, feature = "tray-icon"))] +#[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] +pub mod tray; +pub use tauri_utils as utils; + +pub use http; + +/// A Tauri [`Runtime`] wrapper around wry. +#[cfg(feature = "wry")] +#[cfg_attr(docsrs, doc(cfg(feature = "wry")))] +pub type Wry = tauri_runtime_wry::Wry; +/// A Tauri [`RuntimeHandle`] wrapper around wry. +#[cfg(feature = "wry")] +#[cfg_attr(docsrs, doc(cfg(feature = "wry")))] +pub type WryHandle = tauri_runtime_wry::WryHandle; + +#[cfg(all(feature = "wry", target_os = "android"))] +#[cfg_attr(docsrs, doc(cfg(all(feature = "wry", target_os = "android"))))] +#[doc(hidden)] +#[macro_export] +macro_rules! android_binding { + ($domain:ident, $app_name:ident, $main:ident, $wry:path) => { + use $wry::{ + android_setup, + prelude::{JClass, JNIEnv, JString}, + }; + + ::tauri::wry::android_binding!($domain, $app_name, $wry); + + ::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!( + app_tauri, + plugin, + PluginManager, + handlePluginResponse, + [i32, JString, JString], + ); + ::tauri::tao::platform::android::prelude::android_fn!( + app_tauri, + plugin, + PluginManager, + sendChannelData, + [i64, JString], + ); + + // this function is a glue between PluginManager.kt > handlePluginResponse and Rust + #[allow(non_snake_case)] + pub fn handlePluginResponse( + mut env: JNIEnv, + _: JClass, + id: i32, + success: JString, + error: JString, + ) { + ::tauri::handle_android_plugin_response(&mut env, id, success, error); + } + + // this function is a glue between PluginManager.kt > sendChannelData and Rust + #[allow(non_snake_case)] + pub fn sendChannelData(mut env: JNIEnv, _: JClass, id: i64, data: JString) { + ::tauri::send_channel_data(&mut env, id, data); + } + }; +} + +#[cfg(all(feature = "wry", target_os = "android"))] +#[doc(hidden)] +pub use plugin::mobile::{handle_android_plugin_response, send_channel_data}; +#[cfg(all(feature = "wry", target_os = "android"))] +#[doc(hidden)] +pub use tauri_runtime_wry::{tao, wry}; + +/// A task to run on the main thread. +pub type SyncTask = Box; + +use serde::Serialize; +use std::{ + borrow::Cow, + collections::HashMap, + fmt::{self, Debug}, + sync::MutexGuard, +}; +use utils::assets::{AssetKey, CspHash, EmbeddedAssets}; + +#[cfg(feature = "wry")] +#[cfg_attr(docsrs, doc(cfg(feature = "wry")))] +pub use tauri_runtime_wry::webview_version; + +#[cfg(target_os = "macos")] +#[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] +pub use runtime::ActivationPolicy; + +pub use self::utils::TitleBarStyle; + +use self::event::EventName; +pub use self::event::{Event, EventId, EventTarget}; +use self::manager::EmitPayload; +pub use { + self::app::{ + App, AppHandle, AssetResolver, Builder, CloseRequestApi, ExitRequestApi, RESTART_EXIT_CODE, + RunEvent, UriSchemeContext, UriSchemeResponder, WebviewEvent, WindowEvent, + }, + self::manager::Asset, + self::runtime::{ + DeviceEventFilter, UserAttentionType, + dpi::{ + LogicalPosition, LogicalRect, LogicalSize, LogicalUnit, PhysicalPosition, PhysicalRect, + PhysicalSize, PhysicalUnit, Pixel, PixelUnit, Position, Rect, Size, + }, + window::{CursorIcon, DragDropEvent, WindowSizeConstraints}, + }, + self::state::{State, StateManager}, + self::utils::{ + Env, PackageInfo, Theme, + config::{Config, WebviewUrl}, + }, + self::webview::{Webview, WebviewWindow, WebviewWindowBuilder}, + self::window::{Monitor, Window}, + scope::*, +}; + +#[cfg(feature = "unstable")] +#[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] +pub use {self::webview::WebviewBuilder, self::window::WindowBuilder}; + +/// The Tauri version. +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg(target_os = "ios")] +#[doc(hidden)] +pub fn log_stdout() { + #[cfg(target_os = "ios")] + unsafe { + crate::ios::log_stdout(); + } +} + +/// The user event type. +#[derive(Debug, Clone)] +pub enum EventLoopMessage { + /// An event from a menu item, could be on the window menu bar, application menu bar (on macOS) or tray icon menu. + #[cfg(desktop)] + MenuEvent(menu::MenuEvent), + /// An event from a menu item, could be on the window menu bar, application menu bar (on macOS) or tray icon menu. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + TrayIconEvent(tray::TrayIconEvent), +} + +/// The webview runtime interface. A wrapper around [`runtime::Runtime`] with the proper user event type associated. +pub trait Runtime: runtime::Runtime {} +/// The webview runtime handle. A wrapper around [`runtime::RuntimeHandle`] with the proper user event type associated. +pub trait RuntimeHandle: runtime::RuntimeHandle {} + +impl> Runtime for W {} +impl> RuntimeHandle for R {} + +/// Reads the config file at compile time and generates a [`Context`] based on its content. +/// +/// The default config file path is a `tauri.conf.json` file inside the Cargo manifest directory of +/// the crate being built. +/// +/// # Custom Config Path +/// +/// You may pass a string literal to this macro to specify a custom path for the Tauri config file. +/// If the path is relative, it will be search for relative to the Cargo manifest of the compiling +/// crate. +/// +/// # Note +/// +/// This macro should not be called if you are using [`tauri-build`] to generate the context from +/// inside your build script as it will just cause excess computations that will be discarded. Use +/// either the [`tauri-build`] method or this macro - not both. +/// +/// [`tauri-build`]: https://docs.rs/tauri-build +pub use tauri_macros::generate_context; + +/// Include a [`Context`] that was generated by [`tauri-build`] inside your build script. +/// +/// You should either use [`tauri-build`] and this macro to include the compile time generated code, +/// or [`generate_context!`]. Do not use both at the same time, as they generate the same code and +/// will cause excess computations that will be discarded. +/// +/// [`tauri-build`]: https://docs.rs/tauri-build +#[macro_export] +macro_rules! tauri_build_context { + () => { + include!(concat!(env!("OUT_DIR"), "/tauri-build-context.rs")) + }; +} + +pub use pattern::Pattern; + +/// Whether we are running in development mode or not. +pub const fn is_dev() -> bool { + !cfg!(feature = "custom-protocol") +} + +/// Represents a container of file assets that are retrievable during runtime. +pub trait Assets: Send + Sync + 'static { + /// Initialize the asset provider. + fn setup(&self, app: &App) { + let _ = app; + } + + /// Get the content of the passed [`AssetKey`]. + fn get(&self, key: &AssetKey) -> Option>; + + /// Iterator for the assets. + fn iter(&self) -> Box>; + + /// Gets the hashes for the CSP tag of the HTML on the given path. + fn csp_hashes(&self, html_path: &AssetKey) -> Box> + '_>; +} + +impl Assets for EmbeddedAssets { + fn get(&self, key: &AssetKey) -> Option> { + EmbeddedAssets::get(self, key) + } + + fn iter(&self) -> Box> { + EmbeddedAssets::iter(self) + } + + fn csp_hashes(&self, html_path: &AssetKey) -> Box> + '_> { + EmbeddedAssets::csp_hashes(self, html_path) + } +} + +/// User supplied data required inside of a Tauri application. +/// +/// # Stability +/// This is the output of the [`generate_context`] macro, and is not considered part of the stable API. +/// Unless you know what you are doing and are prepared for this type to have breaking changes, do not create it yourself. +#[tauri_macros::default_runtime(Wry, wry)] +pub struct Context { + pub(crate) config: Config, + #[cfg(dev)] + pub(crate) config_parent: Option, + /// Asset provider. + pub assets: Box>, + pub(crate) default_window_icon: Option>, + pub(crate) app_icon: Option>, + #[cfg(all(desktop, feature = "tray-icon"))] + pub(crate) tray_icon: Option>, + pub(crate) package_info: PackageInfo, + pub(crate) pattern: Pattern, + pub(crate) runtime_authority: RuntimeAuthority, + pub(crate) plugin_global_api_scripts: Option<&'static [&'static str]>, +} + +/// Temporary struct that overrides the Debug formatting for the `app_icon` field. +/// +/// It reduces the output size compared to the default, as that would format the binary +/// data as a slice of numbers `[65, 66, 67]`. This instead shows the length of the Vec. +/// +/// For example: `Some([u8; 493])` +pub(crate) struct DebugAppIcon<'a>(&'a Option>); + +impl std::fmt::Debug for DebugAppIcon<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Option::None => f.write_str("None"), + Option::Some(icon) => f + .debug_tuple("Some") + .field(&format_args!("[u8; {}]", icon.len())) + .finish(), + } + } +} + +impl fmt::Debug for Context { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("Context"); + d.field("config", &self.config) + .field("default_window_icon", &self.default_window_icon) + .field("app_icon", &DebugAppIcon(&self.app_icon)) + .field("package_info", &self.package_info) + .field("pattern", &self.pattern) + .field("plugin_global_api_scripts", &self.plugin_global_api_scripts); + + #[cfg(all(desktop, feature = "tray-icon"))] + d.field("tray_icon", &self.tray_icon); + + d.finish() + } +} + +impl Context { + /// The config the application was prepared with. + #[inline(always)] + pub fn config(&self) -> &Config { + &self.config + } + + /// A mutable reference to the config the application was prepared with. + #[inline(always)] + pub fn config_mut(&mut self) -> &mut Config { + &mut self.config + } + + /// The assets to be served directly by Tauri. + #[inline(always)] + pub fn assets(&self) -> &dyn Assets { + self.assets.as_ref() + } + + /// Replace the [`Assets`] implementation and returns the previous value so you can use it as a fallback if desired. + #[inline(always)] + pub fn set_assets(&mut self, assets: Box>) -> Box> { + std::mem::replace(&mut self.assets, assets) + } + + /// The default window icon Tauri should use when creating windows. + #[inline(always)] + pub fn default_window_icon(&self) -> Option<&image::Image<'_>> { + self.default_window_icon.as_ref() + } + + /// Set the default window icon Tauri should use when creating windows. + #[inline(always)] + pub fn set_default_window_icon(&mut self, icon: Option>) { + self.default_window_icon = icon; + } + + /// The icon to use on the tray icon. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + #[inline(always)] + pub fn tray_icon(&self) -> Option<&image::Image<'_>> { + self.tray_icon.as_ref() + } + + /// Set the icon to use on the tray icon. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "tray-icon"))))] + #[inline(always)] + pub fn set_tray_icon(&mut self, icon: Option>) { + self.tray_icon = icon; + } + + /// Package information. + #[inline(always)] + pub fn package_info(&self) -> &PackageInfo { + &self.package_info + } + + /// A mutable reference to the package information. + #[inline(always)] + pub fn package_info_mut(&mut self) -> &mut PackageInfo { + &mut self.package_info + } + + /// The application pattern. + #[inline(always)] + pub fn pattern(&self) -> &Pattern { + &self.pattern + } + + /// A mutable reference to the resolved ACL. + /// + /// # Stability + /// + /// This API is unstable. + #[doc(hidden)] + #[inline(always)] + pub fn runtime_authority_mut(&mut self) -> &mut RuntimeAuthority { + &mut self.runtime_authority + } + + /// Create a new [`Context`] from the minimal required items. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub fn new( + config: Config, + assets: Box>, + default_window_icon: Option>, + app_icon: Option>, + package_info: PackageInfo, + pattern: Pattern, + runtime_authority: RuntimeAuthority, + plugin_global_api_scripts: Option<&'static [&'static str]>, + ) -> Self { + Self { + config, + #[cfg(dev)] + config_parent: None, + assets, + default_window_icon, + app_icon, + #[cfg(all(desktop, feature = "tray-icon"))] + tray_icon: None, + package_info, + pattern, + runtime_authority, + plugin_global_api_scripts, + } + } + + #[cfg(dev)] + #[doc(hidden)] + pub fn with_config_parent(&mut self, config_parent: impl AsRef) { + self + .config_parent + .replace(config_parent.as_ref().to_owned()); + } +} + +// TODO: expand these docs +/// Manages a running application. +pub trait Manager: sealed::ManagerBase { + /// The application handle associated with this manager. + fn app_handle(&self) -> &AppHandle { + self.managed_app_handle() + } + + /// The [`Config`] the manager was created with. + fn config(&self) -> &Config { + self.manager().config() + } + + /// The [`PackageInfo`] the manager was created with. + fn package_info(&self) -> &PackageInfo { + self.manager().package_info() + } + + /// Fetch a single window from the manager. + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + fn get_window(&self, label: &str) -> Option> { + self.manager().get_window(label) + } + + /// Fetch the focused window. Returns `None` if there is not any focused window. + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + fn get_focused_window(&self) -> Option> { + self.manager().get_focused_window() + } + + /// Fetch all managed windows. + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + fn windows(&self) -> HashMap> { + self.manager().windows() + } + + /// Fetch a single webview from the manager. + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + fn get_webview(&self, label: &str) -> Option> { + self.manager().get_webview(label) + } + + /// Fetch all managed webviews. + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + fn webviews(&self) -> HashMap> { + self.manager().webviews() + } + + /// Fetch a single webview window from the manager. + fn get_webview_window(&self, label: &str) -> Option> { + self.manager().get_webview(label).and_then(|webview| { + let window = webview.window(); + if window.is_webview_window() { + Some(WebviewWindow { window, webview }) + } else { + None + } + }) + } + + /// Fetch all managed webview windows. + fn webview_windows(&self) -> HashMap> { + self + .manager() + .webviews() + .into_iter() + .filter_map(|(label, webview)| { + let window = webview.window(); + if window.is_webview_window() { + Some((label, WebviewWindow { window, webview })) + } else { + None + } + }) + .collect::>() + } + + /// Add `state` to the state managed by the application. + /// + /// If the state for the `T` type has previously been set, the state is unchanged and false is returned. Otherwise true is returned. + /// + /// Managed state can be retrieved by any command handler via the + /// [`State`] guard. In particular, if a value of type `T` + /// is managed by Tauri, adding `State` to the list of arguments in a + /// command handler instructs Tauri to retrieve the managed value. + /// Additionally, [`state`](Self#method.state) can be used to retrieve the value manually. + /// + /// # Mutability + /// + /// Since the managed state is global and must be [`Send`] + [`Sync`], mutations can only happen through interior mutability: + /// + /// ```rust,no_run + /// use std::{collections::HashMap, sync::Mutex}; + /// use tauri::State; + /// // here we use Mutex to achieve interior mutability + /// struct Storage { + /// store: Mutex>, + /// } + /// struct Connection; + /// struct DbConnection { + /// db: Mutex>, + /// } + /// + /// #[tauri::command] + /// fn connect(connection: State) { + /// // initialize the connection, mutating the state with interior mutability + /// *connection.db.lock().unwrap() = Some(Connection {}); + /// } + /// + /// #[tauri::command] + /// fn storage_insert(key: u64, value: String, storage: State) { + /// // mutate the storage behind the Mutex + /// storage.store.lock().unwrap().insert(key, value); + /// } + /// + /// tauri::Builder::default() + /// .manage(Storage { store: Default::default() }) + /// .manage(DbConnection { db: Default::default() }) + /// .invoke_handler(tauri::generate_handler![connect, storage_insert]) + /// // on an actual app, remove the string argument + /// .run(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while running tauri application"); + /// ``` + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::{Manager, State}; + /// + /// struct MyInt(isize); + /// struct MyString(String); + /// + /// #[tauri::command] + /// fn int_command(state: State) -> String { + /// format!("The stateful int is: {}", state.0) + /// } + /// + /// #[tauri::command] + /// fn string_command<'r>(state: State<'r, MyString>) { + /// println!("state: {}", state.0); + /// } + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// app.manage(MyInt(0)); + /// app.manage(MyString("tauri".into())); + /// // `MyInt` is already managed, so `manage()` returns false + /// assert!(!app.manage(MyInt(1))); + /// // read the `MyInt` managed state with the turbofish syntax + /// let int = app.state::(); + /// assert_eq!(int.0, 0); + /// // read the `MyString` managed state with the `State` guard + /// let val: State = app.state(); + /// assert_eq!(val.0, "tauri"); + /// Ok(()) + /// }) + /// .invoke_handler(tauri::generate_handler![int_command, string_command]) + /// // on an actual app, remove the string argument + /// .run(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) + /// .expect("error while running tauri application"); + /// ``` + fn manage(&self, state: T) -> bool + where + T: Send + Sync + 'static, + { + self.manager().state.set(state) + } + + /// Removes the state managed by the application for T. Returns the state if it was actually removed. + /// + ///

+ /// + /// This method is *UNSAFE* and calling it will cause previously obtained references through + /// [Manager::state] and [State::inner] to become dangling references. + /// + /// It is currently deprecated and may be removed in the future. + /// + /// If you really want to unmanage a state, use [std::sync::Mutex] and [Option::take] to wrap the state instead. + /// + /// See [tauri-apps/tauri#12721] for more information. + /// + /// [tauri-apps/tauri#12721]: https://github.com/tauri-apps/tauri/issues/12721 + /// + ///
+ #[deprecated( + since = "2.3.0", + note = "This method is unsafe, since it can cause dangling references." + )] + fn unmanage(&self) -> Option + where + T: Send + Sync + 'static, + { + // The caller decides to break the safety here, then OK, just let it go. + unsafe { self.manager().state.unmanage() } + } + + /// Retrieves the managed state for the type `T`. + /// + /// # Panics + /// + /// Panics if the state for the type `T` has not been previously [managed](Self::manage). + /// Use [try_state](Self::try_state) for a non-panicking version. + fn state(&self) -> State<'_, T> + where + T: Send + Sync + 'static, + { + self.manager().state.try_get().unwrap_or_else(|| { + panic!( + "state() called before manage() for {}", + std::any::type_name::() + ) + }) + } + + /// Attempts to retrieve the managed state for the type `T`. + /// + /// Returns `Some` if the state has previously been [managed](Self::manage). Otherwise returns `None`. + fn try_state(&self) -> Option> + where + T: Send + Sync + 'static, + { + self.manager().state.try_get() + } + + /// Get a reference to the resources table of this manager. + fn resources_table(&self) -> MutexGuard<'_, ResourceTable>; + + /// Gets the managed [`Env`]. + fn env(&self) -> Env { + use std::ops::Deref; + self.state::().deref().clone() + } + + /// Gets the scope for the asset protocol. + #[cfg(feature = "protocol-asset")] + fn asset_protocol_scope(&self) -> scope::fs::Scope { + self.state::().asset_protocol.clone() + } + + /// The path resolver. + fn path(&self) -> &crate::path::PathResolver { + self.state::>().inner() + } + + /// Adds a capability to the app. + /// + /// Note that by default every capability file in the `src-tauri/capabilities` folder + /// are automatically enabled unless specific capabilities are configured in [`tauri.conf.json > app > security > capabilities`], + /// so you should use a different director for the runtime-added capabilities or use [tauri_build::Attributes::capabilities_path_pattern]. + /// + /// # Examples + /// ``` + /// use tauri::Manager; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// #[cfg(feature = "beta")] + /// app.add_capability(include_str!("../capabilities/beta/cap.json")); + /// + /// #[cfg(feature = "stable")] + /// app.add_capability(include_str!("../capabilities/stable/cap.json")); + /// Ok(()) + /// }); + /// ``` + /// + /// The above example assumes the following directory layout: + /// ```md + /// ├── capabilities + /// │ ├── app (default capabilities used by any app flavor) + /// | | |-- cap.json + /// │ ├── beta (capabilities only added to a `beta` flavor) + /// | | |-- cap.json + /// │ ├── stable (capabilities only added to a `stable` flavor) + /// | |-- cap.json + /// ``` + /// + /// For this layout to be properly parsed by Tauri, we need to change the build script to + /// + /// ```skip + /// // only pick up capabilities in the capabilities/app folder by default + /// let attributes = tauri_build::Attributes::new().capabilities_path_pattern("./capabilities/app/*.json"); + /// tauri_build::try_build(attributes).unwrap(); + /// ``` + /// + /// [`tauri.conf.json > app > security > capabilities`]: https://tauri.app/reference/config/#capabilities + /// [tauri_build::Attributes::capabilities_path_pattern]: https://docs.rs/tauri-build/2/tauri_build/struct.Attributes.html#method.capabilities_path_pattern + #[cfg(feature = "dynamic-acl")] + fn add_capability(&self, capability: impl RuntimeCapability) -> Result<()> { + self + .manager() + .runtime_authority + .lock() + .unwrap() + .add_capability(capability) + } +} + +/// Listen to events. +pub trait Listener: sealed::ManagerBase { + /// Listen to an emitted event on this manager. + /// + /// # Examples + /// ``` + /// use tauri::{Manager, Listener, Emitter}; + /// + /// #[tauri::command] + /// fn synchronize(window: tauri::Window) { + /// // emits the synchronized event to all windows + /// window.emit("synchronized", ()); + /// } + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// app.listen("synchronized", |event| { + /// println!("app is in sync"); + /// }); + /// Ok(()) + /// }) + /// .invoke_handler(tauri::generate_handler![synchronize]); + /// ``` + /// # Panics + /// Will panic if `event` contains characters other than alphanumeric, `-`, `/`, `:` and `_` + fn listen(&self, event: impl Into, handler: F) -> EventId + where + F: Fn(Event) + Send + 'static; + + /// Listen to an event on this manager only once. + /// + /// See [`Self::listen`] for more information. + /// # Panics + /// Will panic if `event` contains characters other than alphanumeric, `-`, `/`, `:` and `_` + fn once(&self, event: impl Into, handler: F) -> EventId + where + F: FnOnce(Event) + Send + 'static; + + /// Remove an event listener. + /// + /// # Examples + /// ``` + /// use tauri::{Manager, Listener}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle().clone(); + /// let handler = app.listen_any("ready", move |event| { + /// println!("app is ready"); + /// + /// // we no longer need to listen to the event + /// // we also could have used `app.once_global` instead + /// handle.unlisten(event.id()); + /// }); + /// + /// // stop listening to the event when you do not need it anymore + /// app.unlisten(handler); + /// + /// + /// Ok(()) + /// }); + /// ``` + fn unlisten(&self, id: EventId); + + /// Listen to an emitted event to any [target](EventTarget). + /// + /// # Examples + /// ``` + /// use tauri::{Manager, Emitter, Listener}; + /// + /// #[tauri::command] + /// fn synchronize(window: tauri::Window) { + /// // emits the synchronized event to all windows + /// window.emit("synchronized", ()); + /// } + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// app.listen_any("synchronized", |event| { + /// println!("app is in sync"); + /// }); + /// Ok(()) + /// }) + /// .invoke_handler(tauri::generate_handler![synchronize]); + /// ``` + /// # Panics + /// Will panic if `event` contains characters other than alphanumeric, `-`, `/`, `:` and `_` + fn listen_any(&self, event: impl Into, handler: F) -> EventId + where + F: Fn(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager().listen(event, EventTarget::Any, handler) + } + + /// Listens once to an emitted event to any [target](EventTarget) . + /// + /// See [`Self::listen_any`] for more information. + /// # Panics + /// Will panic if `event` contains characters other than alphanumeric, `-`, `/`, `:` and `_` + fn once_any(&self, event: impl Into, handler: F) -> EventId + where + F: FnOnce(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager().once(event, EventTarget::Any, handler) + } +} + +/// Emit events. +pub trait Emitter: sealed::ManagerBase { + /// Emits an event to all [targets](EventTarget). + /// + /// # Examples + /// ``` + /// use tauri::Emitter; + /// + /// #[tauri::command] + /// fn synchronize(app: tauri::AppHandle) { + /// // emits the synchronized event to all webviews + /// app.emit("synchronized", ()); + /// } + /// ``` + fn emit(&self, event: &str, payload: S) -> Result<()> { + let event = EventName::new(event)?; + let payload = EmitPayload::Serialize(&payload); + self.manager().emit(event, payload) + } + + /// Similar to [`Emitter::emit`] but the payload is json serialized. + fn emit_str(&self, event: &str, payload: String) -> Result<()> { + let event = EventName::new(event)?; + let payload = EmitPayload::<()>::Str(payload); + self.manager().emit(event, payload) + } + + /// Emits an event to all [targets](EventTarget) matching the given target. + /// + /// # Examples + /// ``` + /// use tauri::{Emitter, EventTarget}; + /// + /// #[tauri::command] + /// fn download(app: tauri::AppHandle) { + /// for i in 1..100 { + /// std::thread::sleep(std::time::Duration::from_millis(150)); + /// // emit a download progress event to all listeners + /// app.emit_to(EventTarget::any(), "download-progress", i); + /// // emit an event to listeners that used App::listen or AppHandle::listen + /// app.emit_to(EventTarget::app(), "download-progress", i); + /// // emit an event to any webview/window/webviewWindow matching the given label + /// app.emit_to("updater", "download-progress", i); // similar to using EventTarget::labeled + /// app.emit_to(EventTarget::labeled("updater"), "download-progress", i); + /// // emit an event to listeners that used WebviewWindow::listen + /// app.emit_to(EventTarget::webview_window("updater"), "download-progress", i); + /// } + /// } + /// ``` + fn emit_to(&self, target: I, event: &str, payload: S) -> Result<()> + where + I: Into, + S: Serialize + Clone, + { + let event = EventName::new(event)?; + let payload = EmitPayload::Serialize(&payload); + self.manager().emit_to(target.into(), event, payload) + } + + /// Similar to [`Emitter::emit_to`] but the payload is json serialized. + fn emit_str_to(&self, target: I, event: &str, payload: String) -> Result<()> + where + I: Into, + { + let event = EventName::new(event)?; + let payload = EmitPayload::<()>::Str(payload); + self.manager().emit_to(target.into(), event, payload) + } + + /// Emits an event to all [targets](EventTarget) based on the given filter. + /// + /// # Examples + /// ``` + /// use tauri::{Emitter, EventTarget}; + /// + /// #[tauri::command] + /// fn download(app: tauri::AppHandle) { + /// for i in 1..100 { + /// std::thread::sleep(std::time::Duration::from_millis(150)); + /// // emit a download progress event to the updater window + /// app.emit_filter("download-progress", i, |t| match t { + /// EventTarget::WebviewWindow { label } => label == "main", + /// _ => false, + /// }); + /// } + /// } + /// ``` + fn emit_filter(&self, event: &str, payload: S, filter: F) -> Result<()> + where + S: Serialize + Clone, + F: Fn(&EventTarget) -> bool, + { + let event = EventName::new(event)?; + let payload = EmitPayload::Serialize(&payload); + self.manager().emit_filter(event, payload, filter) + } + + /// Similar to [`Emitter::emit_filter`] but the payload is json serialized. + fn emit_str_filter(&self, event: &str, payload: String, filter: F) -> Result<()> + where + F: Fn(&EventTarget) -> bool, + { + let event = EventName::new(event)?; + let payload = EmitPayload::<()>::Str(payload); + self.manager().emit_filter(event, payload, filter) + } +} + +/// Prevent implementation details from leaking out of the [`Manager`] trait. +pub(crate) mod sealed { + use super::Runtime; + use crate::{app::AppHandle, manager::AppManager}; + use std::sync::Arc; + + /// A running [`Runtime`] or a dispatcher to it. + pub enum RuntimeOrDispatch<'r, R: Runtime> { + /// Reference to the running [`Runtime`]. + Runtime(&'r R), + + /// Handle to the running [`Runtime`]. + RuntimeHandle(R::Handle), + + /// A dispatcher to the running [`Runtime`]. + Dispatch(R::WindowDispatcher), + } + + /// Managed handle to the application runtime. + pub trait ManagerBase { + fn manager(&self) -> &AppManager; + 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>; + } +} + +#[cfg(desktop)] +struct UnsafeSend(T); +#[cfg(desktop)] +unsafe impl Send for UnsafeSend {} + +#[cfg(desktop)] +impl UnsafeSend { + fn take(self) -> T { + self.0 + } +} + +#[cfg(desktop)] +macro_rules! run_main_thread { + ($handle:ident, $ex:expr) => {{ + use std::sync::mpsc::channel; + let (tx, rx) = channel(); + let task = move || { + let f = $ex; + let _ = tx.send(f()); + }; + $handle + .run_on_main_thread(task) + .and_then(|_| rx.recv().map_err(|_| crate::Error::FailedToReceiveMessage)) + }}; +} + +#[cfg(desktop)] +pub(crate) use run_main_thread; + +#[cfg(any(test, feature = "test"))] +#[cfg_attr(docsrs, doc(cfg(feature = "test")))] +pub mod test; + +#[cfg(feature = "specta")] +const _: () = { + use specta::{TypeMap, datatype::DataType, function::FunctionArg}; + + impl FunctionArg for crate::State<'_, T> { + fn to_datatype(_: &mut TypeMap) -> Option { + None + } + } + + impl FunctionArg for crate::AppHandle { + fn to_datatype(_: &mut TypeMap) -> Option { + None + } + } + + impl FunctionArg for crate::Window { + fn to_datatype(_: &mut TypeMap) -> Option { + None + } + } + + impl FunctionArg for crate::Webview { + fn to_datatype(_: &mut TypeMap) -> Option { + None + } + } + + impl FunctionArg for crate::WebviewWindow { + fn to_datatype(_: &mut TypeMap) -> Option { + None + } + } +}; + +#[cfg(test)] +mod tests { + use cargo_toml::Manifest; + use std::{env::var, fs::read_to_string, path::PathBuf, sync::OnceLock}; + + static MANIFEST: OnceLock = OnceLock::new(); + const CHECKED_FEATURES: &str = include_str!(concat!(env!("OUT_DIR"), "/checked_features")); + + fn get_manifest() -> &'static Manifest { + MANIFEST.get_or_init(|| { + let manifest_dir = PathBuf::from(var("CARGO_MANIFEST_DIR").unwrap()); + Manifest::from_path(manifest_dir.join("Cargo.toml")).expect("failed to parse Cargo manifest") + }) + } + + #[test] + fn features_are_documented() { + let manifest_dir = PathBuf::from(var("CARGO_MANIFEST_DIR").unwrap()); + let lib_code = read_to_string(manifest_dir.join("src/lib.rs")).expect("failed to read lib.rs"); + + for f in get_manifest().features.keys() { + if !(f.starts_with("__") || f == "default" || lib_code.contains(&format!("*{f}**"))) { + panic!("Feature {f} is not documented"); + } + } + } + + #[test] + fn aliased_features_exist() { + let checked_features = CHECKED_FEATURES.split(','); + let manifest = get_manifest(); + for checked_feature in checked_features { + if !manifest.features.iter().any(|(f, _)| f == checked_feature) { + panic!( + "Feature {checked_feature} was checked in the alias build step but it does not exist in crates/tauri/Cargo.toml" + ); + } + } + } +} + +#[cfg(test)] +mod test_utils { + use proptest::prelude::*; + + pub fn assert_send() {} + pub fn assert_sync() {} + + #[allow(dead_code)] + pub fn assert_not_allowlist_error(res: anyhow::Result) { + if let Err(e) = res { + assert!(!e.to_string().contains("not on the allowlist")); + } + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(10000))] + #[test] + // check to see if spawn executes a function. + fn check_spawn_task(task in "[a-z]+") { + // create dummy task function + let dummy_task = async move { + let _ = format!("{task}-run-dummy-task"); + }; + // call spawn + crate::async_runtime::spawn(dummy_task); + } + } +} + +/// Simple dependency-free string encoder using [Z85]. +mod z85 { + const TABLE: &[u8; 85] = + b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#"; + + /// Encode bytes with [Z85]. + /// + /// # Panics + /// + /// Will panic if the input bytes are not a multiple of 4. + pub fn encode(bytes: &[u8]) -> String { + assert_eq!(bytes.len() % 4, 0); + + let mut buf = String::with_capacity(bytes.len() * 5 / 4); + for chunk in bytes.as_chunks::<4>().0 { + let mut chars = [0u8; 5]; + let mut chunk = u32::from_be_bytes(*chunk) as usize; + for byte in chars.iter_mut().rev() { + *byte = TABLE[chunk % 85]; + chunk /= 85; + } + + buf.push_str(std::str::from_utf8(&chars).unwrap()); + } + + buf + } + + #[cfg(test)] + mod tests { + #[test] + fn encode() { + assert_eq!( + super::encode(&[0x86, 0x4F, 0xD2, 0x6F, 0xB5, 0x59, 0xF7, 0x5B]), + "HelloWorld" + ); + } + } +} + +/// Generate a random 128-bit [Z85] encoded [`String`]. +/// +/// [Z85]: https://rfc.zeromq.org/spec/32/ +pub(crate) fn generate_invoke_key() -> Result { + let mut bytes = [0u8; 16]; + getrandom::fill(&mut bytes)?; + Ok(z85::encode(&bytes)) +} diff --git a/crates/tauri/src/manager/menu.rs b/crates/tauri/src/manager/menu.rs new file mode 100644 index 0000000..10c6b0b --- /dev/null +++ b/crates/tauri/src/manager/menu.rs @@ -0,0 +1,100 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::HashMap, + sync::{Arc, Mutex, MutexGuard}, +}; + +use crate::{ + AppHandle, Runtime, Window, + menu::{Menu, MenuEvent, MenuId}, +}; + +pub struct MenuManager { + /// A set containing a reference to the active menus, including + /// the app-wide menu and the window-specific menus + /// + /// This should be mainly used to access [`Menu::haccel`] + /// to setup the accelerator handling in the event loop + pub menus: Arc>>>, + /// The menu set to all windows. + pub menu: Mutex>>, + /// Menu event listeners to all windows. + pub global_event_listeners: Mutex>>>, + /// Menu event listeners to specific windows. + pub event_listeners: Mutex>>>, +} + +impl MenuManager { + /// App-wide menu. + pub fn menu_lock(&self) -> MutexGuard<'_, Option>> { + self.menu.lock().expect("poisoned menu mutex") + } + + pub fn menus_stash_lock(&self) -> MutexGuard<'_, HashMap>> { + self.menus.lock().expect("poisoned menu mutex") + } + + pub fn is_menu_in_use>(&self, id: &I) -> bool { + self + .menu_lock() + .as_ref() + .map(|m| id.eq(m.id())) + .unwrap_or(false) + } + + pub fn insert_menu_into_stash(&self, menu: &Menu) { + self + .menus_stash_lock() + .insert(menu.id().clone(), menu.clone()); + } + + pub(crate) fn prepare_window_menu_creation_handler( + &self, + window_menu: Option<&crate::window::WindowMenu>, + #[allow(unused)] theme: Option, + ) -> Option) + use> { + let window_menu = window_menu?; + + self + .menus_stash_lock() + .insert(window_menu.menu.id().clone(), window_menu.menu.clone()); + + #[cfg(target_os = "macos")] + return None; + + #[cfg_attr(target_os = "macos", allow(unused_variables, unreachable_code))] + { + let menu = window_menu.menu.clone(); + Some(move |raw: tauri_runtime::window::RawWindow<'_>| { + #[cfg(target_os = "windows")] + { + let theme = theme + .map(crate::menu::map_to_menu_theme) + .unwrap_or(muda::MenuTheme::Auto); + let _ = unsafe { menu.inner().init_for_hwnd_with_theme(raw.hwnd as _, theme) }; + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + let _ = menu + .inner() + .init_for_gtk_window(raw.gtk_window, raw.default_vbox); + }) + } + } + + pub fn on_menu_event, MenuEvent) + Send + Sync + 'static>(&self, handler: F) { + self + .global_event_listeners + .lock() + .unwrap() + .push(Box::new(handler)); + } +} diff --git a/crates/tauri/src/manager/mod.rs b/crates/tauri/src/manager/mod.rs new file mode 100644 index 0000000..b17e4cb --- /dev/null +++ b/crates/tauri/src/manager/mod.rs @@ -0,0 +1,1040 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + borrow::Cow, + collections::HashMap, + fmt, + sync::{Arc, Mutex, MutexGuard, atomic::AtomicBool}, +}; + +use serde::Serialize; +use url::Url; + +use tauri_macros::default_runtime; +use tauri_utils::{ + assets::{AssetKey, CspHash, SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN}, + config::{Csp, CspDirectiveSources}, +}; + +use crate::{ + Assets, Context, DebugAppIcon, EventName, Pattern, Runtime, StateManager, Webview, Window, + app::{ + AppHandle, ChannelInterceptor, GlobalWebviewEventListener, GlobalWindowEventListener, + OnPageLoad, + }, + event::{EmitArgs, Event, EventId, EventTarget, Listeners}, + ipc::{Invoke, InvokeHandler, RuntimeAuthority}, + plugin::PluginStore, + resources::ResourceTable, + 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"))] +mod tray; +pub mod webview; +pub mod window; + +#[derive(Default)] +/// Spaced and quoted Content-Security-Policy hash values. +struct CspHashStrings { + script: Vec, + style: Vec, +} + +/// Sets the CSP value to the asset HTML if needed (on Linux). +/// Returns the CSP string for access on the response header (on Windows and macOS). +pub(crate) fn set_csp( + asset: &mut String, + assets: &impl std::borrow::Borrow>, + asset_path: &AssetKey, + manager: &AppManager, + csp: Csp, +) -> HashMap { + let mut csp = csp.into(); + let hash_strings = + assets + .borrow() + .csp_hashes(asset_path) + .fold(CspHashStrings::default(), |mut acc, hash| { + match hash { + CspHash::Script(hash) => { + acc.script.push(hash.into()); + } + CspHash::Style(hash) => { + acc.style.push(hash.into()); + } + _csp_hash => { + log::debug!("Unknown CspHash variant encountered: {:?}", _csp_hash); + } + } + + acc + }); + + let dangerous_disable_asset_csp_modification = &manager + .config() + .app + .security + .dangerous_disable_asset_csp_modification; + if dangerous_disable_asset_csp_modification.can_modify("script-src") { + replace_csp_nonce( + asset, + SCRIPT_NONCE_TOKEN, + &mut csp, + "script-src", + hash_strings.script, + ); + } + + if dangerous_disable_asset_csp_modification.can_modify("style-src") { + replace_csp_nonce( + asset, + STYLE_NONCE_TOKEN, + &mut csp, + "style-src", + hash_strings.style, + ); + } + + csp +} + +// inspired by +fn replace_with_callback String>( + original: &str, + pattern: &str, + mut replacement: F, +) -> String { + let mut result = String::new(); + let mut last_end = 0; + for (start, part) in original.match_indices(pattern) { + result.push_str(unsafe { original.get_unchecked(last_end..start) }); + result.push_str(&replacement()); + last_end = start + part.len(); + } + result.push_str(unsafe { original.get_unchecked(last_end..original.len()) }); + result +} + +fn replace_csp_nonce( + asset: &mut String, + token: &str, + csp: &mut HashMap, + directive: &str, + hashes: Vec, +) { + let mut nonces = Vec::new(); + *asset = replace_with_callback(asset, token, || { + let nonce = getrandom::u64().expect("failed to get random bytes"); + nonces.push(nonce); + nonce.to_string() + }); + + if !(nonces.is_empty() && hashes.is_empty()) { + let nonce_sources = nonces + .into_iter() + .map(|n| format!("'nonce-{n}'")) + .collect::>(); + let sources = csp.entry(directive.into()).or_default(); + let self_source = "'self'".to_string(); + if !sources.contains(&self_source) { + sources.push(self_source); + } + sources.extend(nonce_sources); + sources.extend(hashes); + } +} + +/// A resolved asset. +#[non_exhaustive] +pub struct Asset { + /// The asset bytes. + pub bytes: Vec, + /// The asset's mime type. + pub mime_type: String, + /// The `Content-Security-Policy` header value. + pub csp_header: Option, +} + +impl Asset { + /// The asset bytes. + pub fn bytes(&self) -> &[u8] { + &self.bytes + } + + /// The asset's mime type. + pub fn mime_type(&self) -> &str { + &self.mime_type + } + + /// The `Content-Security-Policy` header value. + pub fn csp_header(&self) -> Option<&str> { + self.csp_header.as_deref() + } +} + +#[default_runtime(crate::Wry, wry)] +pub struct AppManager { + pub runtime_authority: Mutex, + pub window: window::WindowManager, + pub webview: webview::WebviewManager, + #[cfg(all(desktop, feature = "tray-icon"))] + pub tray: tray::TrayManager, + #[cfg(desktop)] + pub menu: menu::MenuManager, + + pub(crate) plugins: Mutex>, + pub listeners: Listeners, + pub state: Arc, + pub config: Config, + #[cfg(dev)] + pub config_parent: Option, + pub assets: Box>, + + pub app_icon: Option>, + + pub package_info: PackageInfo, + + /// Application pattern. + pub pattern: Arc, + + /// Global API scripts collected from plugins. + pub plugin_global_api_scripts: Arc>, + + /// Application Resources Table + pub(crate) resources_table: Arc>, + + /// Runtime-generated invoke key. + pub(crate) invoke_key: String, + + pub(crate) channel_interceptor: Option>, + + /// Sets to true in [`AppHandle::request_restart`] and [`AppHandle::restart`] + /// and we will call `restart` on the next `RuntimeRunEvent::Exit` event + pub(crate) restart_on_exit: AtomicBool, +} + +impl fmt::Debug for AppManager { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("AppManager"); + + d.field("window", &self.window) + .field("plugins", &self.plugins) + .field("state", &self.state) + .field("config", &self.config) + .field("app_icon", &DebugAppIcon(&self.app_icon)) + .field("package_info", &self.package_info) + .field("pattern", &self.pattern); + + #[cfg(all(desktop, feature = "tray-icon"))] + { + d.field("tray", &self.tray); + } + + d.finish() + } +} + +pub(crate) enum EmitPayload<'a, S: Serialize> { + Serialize(&'a S), + Str(String), +} + +impl AppManager { + #[allow(clippy::too_many_arguments, clippy::type_complexity)] + pub(crate) fn with_handlers( + #[allow(unused_mut)] mut context: Context, + plugins: PluginStore, + invoke_handler: Box>, + on_page_load: Option>>, + on_permission_request: 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>>, + #[cfg(all(desktop, feature = "tray-icon"))] tray_icon_event_listeners: Vec< + crate::app::GlobalTrayIconEventListener>, + >, + window_event_listeners: Vec>, + webview_event_listeners: Vec>, + #[cfg(desktop)] window_menu_event_listeners: HashMap< + String, + crate::app::GlobalMenuEventListener>, + >, + invoke_initialization_script: String, + channel_interceptor: Option>, + invoke_key: String, + ) -> Self { + // generate a random isolation key at runtime + #[cfg(feature = "isolation")] + if let Pattern::Isolation { key, .. } = &mut context.pattern { + *key = uuid::Uuid::new_v4().to_string(); + } + + Self { + runtime_authority: Mutex::new(context.runtime_authority), + window: window::WindowManager { + windows: Mutex::default(), + default_icon: context.default_window_icon, + event_listeners: Arc::new(window_event_listeners), + }, + webview: webview::WebviewManager { + webviews: Mutex::default(), + invoke_handler, + on_page_load, + on_permission_request, + #[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, + invoke_key: invoke_key.clone(), + }, + #[cfg(all(desktop, feature = "tray-icon"))] + tray: tray::TrayManager { + icon: context.tray_icon, + icons: Default::default(), + global_event_listeners: Mutex::new(tray_icon_event_listeners), + event_listeners: Default::default(), + }, + #[cfg(desktop)] + menu: menu::MenuManager { + menus: Default::default(), + menu: Default::default(), + global_event_listeners: Mutex::new(menu_event_listener), + event_listeners: Mutex::new(window_menu_event_listeners), + }, + plugins: Mutex::new(plugins), + listeners: Listeners::default(), + state: Arc::new(state), + config: context.config, + #[cfg(dev)] + config_parent: context.config_parent, + assets: context.assets, + app_icon: context.app_icon, + package_info: context.package_info, + pattern: Arc::new(context.pattern), + plugin_global_api_scripts: Arc::new(context.plugin_global_api_scripts), + resources_table: Arc::default(), + invoke_key, + channel_interceptor, + restart_on_exit: AtomicBool::new(false), + } + } + + /// The `tauri` custom protocol URL we use to serve the embedded assets. + /// Returns `tauri://localhost` or its `wry` workaround URL `http://tauri.localhost`/`https://tauri.localhost` + pub(crate) fn tauri_protocol_url(&self, https: bool) -> Cow<'_, Url> { + Cow::Owned(Url::parse(&crate::protocol::origin("tauri", https)).unwrap()) + } + + /// Get the base app URL for [`WebviewUrl::App`](tauri_utils::config::WebviewUrl::App). + /// + /// * In dev mode, this is the [`devUrl`](tauri_utils::config::BuildConfig::dev_url) configuration value if it exists. + /// * In production mode, this is the [`frontendDist`](tauri_utils::config::BuildConfig::frontend_dist) configuration value if it's a [`FrontendDist::Url`](tauri_utils::config::FrontendDist::Url). + /// * Returns [`Self::tauri_protocol_url`] (e.g. `tauri://localhost`) otherwise. + pub(crate) fn get_app_url(&self, https: bool) -> Cow<'_, Url> { + #[cfg(dev)] + let url = self.config.build.dev_url.as_ref(); + #[cfg(not(dev))] + let url = match self.config.build.frontend_dist.as_ref() { + Some(crate::utils::config::FrontendDist::Url(url)) => Some(url), + _ => None, + }; + + if let Some(url) = url { + Cow::Borrowed(url) + } else { + self.tauri_protocol_url(https) + } + } + + fn csp(&self) -> Option { + if !crate::is_dev() { + self.config.app.security.csp.clone() + } else { + self + .config + .app + .security + .dev_csp + .clone() + .or_else(|| self.config.app.security.csp.clone()) + } + } + + pub fn get_asset(&self, mut path: String, _use_https_schema: bool) -> crate::Result { + let assets = &self.assets; + if path.ends_with('/') { + path.pop(); + } + path = percent_encoding::percent_decode(path.as_bytes()) + .decode_utf8_lossy() + .to_string(); + let path = if path.is_empty() { + // if the url is `tauri://localhost`, we should load `index.html` + "index.html".to_string() + } else { + // skip the leading `/`, if it starts with one. + path.strip_prefix('/').unwrap_or(path.as_str()).to_string() + }; + + let mut asset_path = AssetKey::from(path.as_str()); + + let asset_response = assets + .get(&asset_path) + .or_else(|| { + log::debug!("Asset `{path}` not found; fallback to {path}.html"); + let fallback = format!("{path}.html").into(); + let asset = assets.get(&fallback); + asset_path = fallback; + asset + }) + .or_else(|| { + log::debug!("Asset `{path}` not found; fallback to {path}/index.html",); + let fallback = format!("{path}/index.html").into(); + let asset = assets.get(&fallback); + asset_path = fallback; + asset + }) + .or_else(|| { + log::debug!("Asset `{path}` not found; fallback to index.html"); + let fallback = AssetKey::from("index.html"); + let asset = assets.get(&fallback); + asset_path = fallback; + asset + }) + .ok_or_else(|| { + let error = crate::Error::AssetNotFound(path.clone()); + log::error!("{error}"); + error + })?; + + let mut csp_header = None; + let is_html = asset_path.as_ref().ends_with(".html"); + + 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(crate::pattern::format_real_schema( + schema, + _use_https_schema, + )); + } + + 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 { + &self.listeners + } + + pub fn run_invoke_handler(&self, invoke: Invoke) -> bool { + (self.webview.invoke_handler)(invoke) + } + + /// Runs the plugin [`crate::plugin::Plugin::extend_api`] hook if it exists. Returns whether the invoke message was handled or not. + /// + /// The message is not handled when the plugin exists **and** the command does not. + pub fn run_plugin_invoke_handler(&self, plugin: &str, invoke: Invoke) -> bool { + self + .plugins + .lock() + .expect("poisoned plugin store") + .run_invoke_handler(plugin, invoke) + } + + pub fn initialize_plugins(&self, app: &AppHandle) -> crate::Result<()> { + self + .plugins + .lock() + .expect("poisoned plugin store") + .initialize_all(app, &self.config.plugins) + } + + pub fn config(&self) -> &Config { + &self.config + } + + #[cfg(dev)] + pub fn config_parent(&self) -> Option<&std::path::PathBuf> { + self.config_parent.as_ref() + } + + pub fn package_info(&self) -> &PackageInfo { + &self.package_info + } + + /// # Panics + /// Will panic if `event` contains characters other than alphanumeric, `-`, `/`, `:` and `_` + pub(crate) fn listen( + &self, + event: EventName, + target: EventTarget, + handler: F, + ) -> EventId { + self.listeners().listen(event, target, handler) + } + + /// # Panics + /// Will panic if `event` contains characters other than alphanumeric, `-`, `/`, `:` and `_` + pub(crate) fn once( + &self, + event: EventName, + target: EventTarget, + handler: F, + ) -> EventId { + self.listeners().once(event, target, handler) + } + + pub fn unlisten(&self, id: EventId) { + self.listeners().unlisten(id) + } + + #[cfg_attr( + feature = "tracing", + tracing::instrument("app::emit", skip(self, payload)) + )] + pub(crate) fn emit( + &self, + event: EventName<&str>, + payload: EmitPayload<'_, S>, + ) -> crate::Result<()> { + #[cfg(feature = "tracing")] + let _span = tracing::debug_span!("emit::run").entered(); + let emit_args = match payload { + EmitPayload::Serialize(payload) => EmitArgs::new(event, payload)?, + EmitPayload::Str(payload) => EmitArgs::new_str(event, payload)?, + }; + + let listeners = self.listeners(); + + listeners.emit_js(self.webview.webviews_lock().values(), &emit_args)?; + listeners.emit(emit_args)?; + + Ok(()) + } + + #[cfg_attr( + feature = "tracing", + tracing::instrument("app::emit::filter", skip(self, payload, filter)) + )] + pub(crate) fn emit_filter( + &self, + event: EventName<&str>, + payload: EmitPayload<'_, S>, + filter: F, + ) -> crate::Result<()> + where + S: Serialize, + F: Fn(&EventTarget) -> bool, + { + #[cfg(feature = "tracing")] + let _span = tracing::debug_span!("emit::run").entered(); + let emit_args = match payload { + EmitPayload::Serialize(payload) => EmitArgs::new(event, payload)?, + EmitPayload::Str(payload) => EmitArgs::new_str(event, payload)?, + }; + + let listeners = self.listeners(); + + listeners.emit_js_filter( + self.webview.webviews_lock().values(), + &emit_args, + Some(&filter), + )?; + + listeners.emit_filter(emit_args, Some(filter))?; + + Ok(()) + } + + #[cfg_attr( + feature = "tracing", + tracing::instrument("app::emit::to", skip(self, target, payload), fields(target)) + )] + pub(crate) fn emit_to( + &self, + target: EventTarget, + event: EventName<&str>, + payload: EmitPayload<'_, S>, + ) -> crate::Result<()> + where + S: Serialize, + { + #[cfg(feature = "tracing")] + tracing::Span::current().record("target", format!("{target:?}")); + + fn filter_target(target: &EventTarget, candidate: &EventTarget) -> bool { + match target { + // if targeting any label, filter matching labels + EventTarget::AnyLabel { label } => match candidate { + EventTarget::Window { label: l } + | EventTarget::Webview { label: l } + | EventTarget::WebviewWindow { label: l } + | EventTarget::AnyLabel { label: l } => l == label, + _ => false, + }, + EventTarget::Window { label } => match candidate { + EventTarget::AnyLabel { label: l } | EventTarget::Window { label: l } => l == label, + _ => false, + }, + EventTarget::Webview { label } => match candidate { + EventTarget::AnyLabel { label: l } | EventTarget::Webview { label: l } => l == label, + _ => false, + }, + EventTarget::WebviewWindow { label } => match candidate { + EventTarget::AnyLabel { label: l } | EventTarget::WebviewWindow { label: l } => { + l == label + } + _ => false, + }, + // otherwise match same target + _ => target == candidate, + } + } + + match target { + // if targeting all, emit to all using emit without filter + EventTarget::Any => self.emit(event, payload), + target => self.emit_filter(event, payload, |t| filter_target(&target, t)), + } + } + + pub fn get_window(&self, label: &str) -> Option> { + self.window.windows_lock().get(label).cloned() + } + + pub fn get_focused_window(&self) -> Option> { + self + .window + .windows_lock() + .values() + .find(|w| w.is_focused().unwrap_or(false)) + .cloned() + } + + pub(crate) fn on_window_close(&self, label: &str) { + let window = self.window.windows_lock().remove(label); + if let Some(window) = window { + for webview in window.webviews() { + self.webview.webviews_lock().remove(webview.label()); + self.listeners().remove_webview_listeners(webview.label()); + self + .state + .get::() + .remove_webview_entries(webview.label()); + } + } + self.listeners().remove_window_listeners(label); + } + + #[cfg(desktop)] + pub(crate) fn on_webview_close(&self, label: &str) { + self.webview.webviews_lock().remove(label); + self.listeners().remove_webview_listeners(label); + self + .state + .get::() + .remove_webview_entries(label); + } + + pub fn windows(&self) -> HashMap> { + self.window.windows_lock().clone() + } + + pub fn get_webview(&self, label: &str) -> Option> { + self.webview.webviews_lock().get(label).cloned() + } + + pub fn webviews(&self) -> HashMap> { + self.webview.webviews_lock().clone() + } + + pub(crate) fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + self + .resources_table + .lock() + .expect("poisoned window manager") + } + + pub(crate) fn invoke_key(&self) -> &str { + &self.invoke_key + } +} + +#[cfg(desktop)] +impl AppManager { + pub fn remove_menu_from_stash_by_id(&self, id: Option<&crate::menu::MenuId>) { + if let Some(id) = id { + let is_used_by_a_window = self + .window + .windows_lock() + .values() + .any(|w| w.is_menu_in_use(id)); + if !(self.menu.is_menu_in_use(id) || is_used_by_a_window) { + self.menu.menus_stash_lock().remove(id); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::replace_with_callback; + + #[test] + fn string_replace_with_callback() { + let mut tauri_index = 0; + #[allow(clippy::single_element_loop)] + for (src, pattern, replacement, result) in [( + "tauri is awesome, tauri is amazing", + "tauri", + || { + tauri_index += 1; + tauri_index.to_string() + }, + "1 is awesome, 2 is amazing", + )] { + assert_eq!(replace_with_callback(src, pattern, replacement), result); + } + } +} + +#[cfg(test)] +mod test { + use std::{ + sync::mpsc::{Receiver, Sender, channel}, + time::Duration, + }; + + use crate::{ + App, Emitter, Listener, Manager, StateManager, Webview, WebviewWindow, WebviewWindowBuilder, + Window, Wry, + event::EventTarget, + generate_context, + plugin::PluginStore, + test::{MockRuntime, mock_app}, + webview::WebviewBuilder, + window::WindowBuilder, + }; + + use super::AppManager; + + const APP_LISTEN_ID: &str = "App::listen"; + const APP_LISTEN_ANY_ID: &str = "App::listen_any"; + const WINDOW_LISTEN_ID: &str = "Window::listen"; + const WINDOW_LISTEN_ANY_ID: &str = "Window::listen_any"; + const WEBVIEW_LISTEN_ID: &str = "Webview::listen"; + const WEBVIEW_LISTEN_ANY_ID: &str = "Webview::listen_any"; + const WEBVIEW_WINDOW_LISTEN_ID: &str = "WebviewWindow::listen"; + const WEBVIEW_WINDOW_LISTEN_ANY_ID: &str = "WebviewWindow::listen_any"; + const TEST_EVENT_NAME: &str = "event"; + + #[test] + fn check_get_url() { + let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate, test = true); + let manager: AppManager = AppManager::with_handlers( + context, + PluginStore::default(), + Box::new(|_| false), + None, + #[cfg(any(target_os = "macos", target_os = "ios"))] + None, + Default::default(), + Default::default(), + StateManager::new(), + Default::default(), + #[cfg(all(desktop, feature = "tray-icon"))] + Default::default(), + Default::default(), + Default::default(), + Default::default(), + "".into(), + None, + crate::generate_invoke_key().unwrap(), + ); + + #[cfg(custom_protocol)] + { + assert_eq!( + manager.get_app_url(false).to_string(), + if cfg!(windows) || cfg!(target_os = "android") { + "http://tauri.localhost/" + } else { + "tauri://localhost" + } + ); + assert_eq!( + manager.get_app_url(true).to_string(), + if cfg!(windows) || cfg!(target_os = "android") { + "https://tauri.localhost/" + } else { + "tauri://localhost" + } + ); + } + + #[cfg(dev)] + assert_eq!( + manager.get_app_url(false).to_string(), + "http://localhost:4000/" + ); + } + + struct EventSetup { + app: App, + window: Window, + webview: Webview, + webview_window: WebviewWindow, + tx: Sender<(&'static str, String)>, + rx: Receiver<(&'static str, String)>, + } + + fn setup_events(setup_any: bool) -> EventSetup { + let app = mock_app(); + + let window = WindowBuilder::new(&app, "main-window").build().unwrap(); + + let webview = window + .add_child( + WebviewBuilder::new("main-webview", Default::default()), + crate::LogicalPosition::new(0, 0), + window.inner_size().unwrap(), + ) + .unwrap(); + + let webview_window = WebviewWindowBuilder::new(&app, "main-webview-window", Default::default()) + .build() + .unwrap(); + + let (tx, rx) = channel(); + + macro_rules! setup_listener { + ($type:ident, $id:ident, $any_id:ident) => { + let tx_ = tx.clone(); + $type.listen(TEST_EVENT_NAME, move |evt| { + tx_ + .send(($id, serde_json::from_str::(evt.payload()).unwrap())) + .unwrap(); + }); + + if setup_any { + let tx_ = tx.clone(); + $type.listen_any(TEST_EVENT_NAME, move |evt| { + tx_ + .send(( + $any_id, + serde_json::from_str::(evt.payload()).unwrap(), + )) + .unwrap(); + }); + } + }; + } + + setup_listener!(app, APP_LISTEN_ID, APP_LISTEN_ANY_ID); + setup_listener!(window, WINDOW_LISTEN_ID, WINDOW_LISTEN_ANY_ID); + setup_listener!(webview, WEBVIEW_LISTEN_ID, WEBVIEW_LISTEN_ANY_ID); + setup_listener!( + webview_window, + WEBVIEW_WINDOW_LISTEN_ID, + WEBVIEW_WINDOW_LISTEN_ANY_ID + ); + + EventSetup { + app, + window, + webview, + webview_window, + tx, + rx, + } + } + + fn assert_events(kind: &str, received: &[&str], expected: &[&str]) { + for e in expected { + assert!(received.contains(e), "{e} did not receive `{kind}` event"); + } + assert_eq!( + received.len(), + expected.len(), + "received {received:?} `{kind}` events but expected {expected:?}" + ); + } + + #[test] + fn emit() { + let EventSetup { + app, + window, + webview, + webview_window, + tx: _, + rx, + } = setup_events(true); + + run_emit_test("emit (app)", app, &rx); + run_emit_test("emit (window)", window, &rx); + run_emit_test("emit (webview)", webview, &rx); + run_emit_test("emit (webview_window)", webview_window, &rx); + } + + fn run_emit_test + Emitter>( + kind: &str, + m: M, + rx: &Receiver<(&str, String)>, + ) { + let mut received = Vec::new(); + let payload = "global-payload"; + m.emit(TEST_EVENT_NAME, payload).unwrap(); + while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) { + assert_eq!(p, payload); + received.push(source); + } + assert_events( + kind, + &received, + &[ + APP_LISTEN_ID, + APP_LISTEN_ANY_ID, + WINDOW_LISTEN_ID, + WINDOW_LISTEN_ANY_ID, + WEBVIEW_LISTEN_ID, + WEBVIEW_LISTEN_ANY_ID, + WEBVIEW_WINDOW_LISTEN_ID, + WEBVIEW_WINDOW_LISTEN_ANY_ID, + ], + ); + } + + #[test] + fn emit_to() { + let EventSetup { + app, + window, + webview, + webview_window, + tx, + rx, + } = setup_events(false); + + run_emit_to_test( + "emit_to (App)", + &app, + &window, + &webview, + &webview_window, + tx.clone(), + &rx, + ); + run_emit_to_test( + "emit_to (window)", + &window, + &window, + &webview, + &webview_window, + tx.clone(), + &rx, + ); + run_emit_to_test( + "emit_to (webview)", + &webview, + &window, + &webview, + &webview_window, + tx.clone(), + &rx, + ); + run_emit_to_test( + "emit_to (webview_window)", + &webview_window, + &window, + &webview, + &webview_window, + tx.clone(), + &rx, + ); + } + + fn run_emit_to_test + Emitter>( + kind: &str, + m: &M, + window: &Window, + webview: &Webview, + webview_window: &WebviewWindow, + tx: Sender<(&'static str, String)>, + rx: &Receiver<(&'static str, String)>, + ) { + let mut received = Vec::new(); + let payload = "global-payload"; + + macro_rules! test_target { + ($target:expr, $id:ident) => { + m.emit_to($target, TEST_EVENT_NAME, payload).unwrap(); + while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) { + assert_eq!(p, payload); + received.push(source); + } + assert_events(kind, &received, &[$id]); + + received.clear(); + }; + } + + test_target!(EventTarget::App, APP_LISTEN_ID); + test_target!(window.label(), WINDOW_LISTEN_ID); + test_target!(webview.label(), WEBVIEW_LISTEN_ID); + test_target!(webview_window.label(), WEBVIEW_WINDOW_LISTEN_ID); + + let other_webview_listen_id = "OtherWebview::listen"; + let other_webview = WebviewWindowBuilder::new( + window, + kind.replace(['(', ')', ' '], ""), + Default::default(), + ) + .build() + .unwrap(); + + other_webview.listen(TEST_EVENT_NAME, move |evt| { + tx.send(( + other_webview_listen_id, + serde_json::from_str::(evt.payload()).unwrap(), + )) + .unwrap(); + }); + m.emit_to(other_webview.label(), TEST_EVENT_NAME, payload) + .unwrap(); + while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) { + assert_eq!(p, payload); + received.push(source); + } + assert_events("emit_to", &received, &[other_webview_listen_id]); + } +} diff --git a/crates/tauri/src/manager/tray.rs b/crates/tauri/src/manager/tray.rs new file mode 100644 index 0000000..e233d69 --- /dev/null +++ b/crates/tauri/src/manager/tray.rs @@ -0,0 +1,92 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::HashMap, + fmt, + sync::{Arc, Mutex}, +}; + +use crate::{ + AppHandle, Manager, Resource, ResourceId, Runtime, + app::GlobalTrayIconEventListener, + image::Image, + tray::{TrayIcon, TrayIconEvent, TrayIconId}, +}; + +pub struct TrayManager { + /// icon passed in from [`tauri_utils::config::TrayIconConfig::icon_path`], + /// used when creating tray icons from the configs. + pub(crate) icon: Option>, + /// Tray icons + pub(crate) icons: Mutex>, + /// Global Tray icon event listeners. + pub(crate) global_event_listeners: Mutex>>>, + /// Tray icon event listeners. + pub(crate) event_listeners: Mutex>>>, +} + +impl fmt::Debug for TrayManager { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TrayManager") + .field("icon", &self.icon) + .finish() + } +} + +impl TrayManager { + pub fn on_tray_icon_event, TrayIconEvent) + Send + Sync + 'static>( + &self, + handler: F, + ) { + self + .global_event_listeners + .lock() + .unwrap() + .push(Box::new(handler)); + } + + pub fn tray_by_id<'a, I>(&self, app: &AppHandle, id: &'a I) -> Option> + where + I: ?Sized, + TrayIconId: PartialEq<&'a I>, + { + let icons = self.icons.lock().unwrap(); + icons.iter().find_map(|(tray_icon_id, rid)| { + if tray_icon_id == &id { + let icon = app.resources_table().get::>(*rid).ok()?; + Some(Arc::unwrap_or_clone(icon)) + } else { + None + } + }) + } + + pub fn tray_resource_by_id<'a, I>(&self, id: &'a I) -> Option + where + I: ?Sized, + TrayIconId: PartialEq<&'a I>, + { + let icons = self.icons.lock().unwrap(); + icons.iter().find_map(|(tray_icon_id, rid)| { + if tray_icon_id == &id { + Some(*rid) + } else { + None + } + }) + } + + pub fn remove_tray_by_id<'a, I>(&self, app: &AppHandle, id: &'a I) -> Option> + where + I: ?Sized, + TrayIconId: PartialEq<&'a I>, + { + let rid = self.tray_resource_by_id(id)?; + let icon = app.resources_table().take::>(rid).ok()?; + let icon_to_return = icon.clone(); + icon.close(); + Some(Arc::unwrap_or_clone(icon_to_return)) + } +} diff --git a/crates/tauri/src/manager/webview.rs b/crates/tauri/src/manager/webview.rs new file mode 100644 index 0000000..d2322ac --- /dev/null +++ b/crates/tauri/src/manager/webview.rs @@ -0,0 +1,793 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, + fmt, + fs::create_dir_all, + sync::{Arc, Mutex, MutexGuard}, +}; + +use serde::Serialize; +use serialize_to_javascript::{DefaultTemplate, Template, default_template}; +use tauri_runtime::{ + webview::{DetachedWebview, InitializationScript, PendingWebview}, + window::DragDropEvent, +}; +use tauri_utils::config::WebviewUrl; +use url::Url; + +use crate::{ + EventLoopMessage, EventTarget, Manager, Runtime, Scopes, UriSchemeContext, Webview, Window, + app::{GlobalWebviewEventListener, OnPageLoad, UriSchemeResponder, WebviewEvent}, + ipc::InvokeHandler, + pattern::PatternJavascript, + sealed::ManagerBase, + 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}, +}; + +// we need to proxy the dev server on mobile because we can't use `localhost`, so we use the local IP address +// 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, mobile)); + +pub(crate) const PROCESS_IPC_MESSAGE_FN: &str = + include_str!("../../scripts/process-ipc-message-fn.js"); + +#[cfg(feature = "isolation")] +#[derive(Template)] +#[default_template("../../scripts/isolation.js")] +pub(crate) struct IsolationJavascript<'a> { + pub(crate) isolation_src: &'a str, + pub(crate) style: &'a str, +} + +#[derive(Template)] +#[default_template("../../scripts/ipc.js")] +pub(crate) struct IpcJavascript<'a> { + pub(crate) isolation_origin: &'a str, +} + +/// Uses a custom URI scheme handler to resolve file requests +pub struct UriSchemeProtocol { + /// Handler for protocol + #[allow(clippy::type_complexity)] + pub handler: + Box, http::Request>, UriSchemeResponder) + Send + Sync>, +} + +pub struct WebviewManager { + pub webviews: Mutex>>, + /// The JS message handler. + pub invoke_handler: Box>, + /// The page load hook, invoked when the webview performs a navigation. + pub on_page_load: Option>>, + /// The permission request hook, invoked when the webview requests a permission. + pub on_permission_request: 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. + pub event_listeners: Arc>>, + + /// The script that initializes the invoke system. + pub invoke_initialization_script: String, + + /// A runtime generated invoke key. + pub(crate) invoke_key: String, +} + +impl fmt::Debug for WebviewManager { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WebviewManager") + .field( + "invoke_initialization_script", + &self.invoke_initialization_script, + ) + .field("invoke_key", &self.invoke_key) + .finish() + } +} + +impl WebviewManager { + pub(crate) fn register_uri_scheme_protocol>( + &self, + uri_scheme: N, + protocol: Arc>, + ) { + let uri_scheme = uri_scheme.into(); + self + .uri_scheme_protocols + .lock() + .unwrap() + .insert(uri_scheme, protocol); + } + + /// Get a locked handle to the webviews. + pub(crate) fn webviews_lock(&self) -> MutexGuard<'_, HashMap>> { + self.webviews.lock().expect("poisoned webview manager") + } + + fn prepare_pending_webview>( + &self, + mut pending: PendingWebview, + label: &str, + window_label: &str, + manager: &M, + ) -> crate::Result> { + let app_manager = manager.manager(); + + let plugin_init_scripts = app_manager + .plugins + .lock() + .expect("poisoned plugin store") + .initialization_script(); + + let pattern_init = PatternJavascript { + pattern: (&*app_manager.pattern).into(), + } + .render_default(&Default::default())?; + + let mut webview_attributes = pending.webview_attributes; + + let use_https_scheme = webview_attributes.use_https_scheme; + + let ipc_init = IpcJavascript { + isolation_origin: &match &*app_manager.pattern { + #[cfg(feature = "isolation")] + crate::Pattern::Isolation { schema, .. } => { + crate::pattern::format_real_schema(schema, use_https_scheme) + } + _ => "".to_owned(), + }, + } + .render_default(&Default::default())?; + + let mut all_initialization_scripts: Vec = vec![]; + + fn main_frame_script(script: String) -> InitializationScript { + InitializationScript { + script, + for_main_frame_only: true, + } + } + + all_initialization_scripts.push(main_frame_script( + r" + Object.defineProperty(window, 'isTauri', { + value: true, + }); + + if (!window.__TAURI_INTERNALS__) { + Object.defineProperty(window, '__TAURI_INTERNALS__', { + value: { + plugins: {} + } + }) + } + " + .to_owned(), + )); + all_initialization_scripts.push(main_frame_script(self.invoke_initialization_script.clone())); + all_initialization_scripts.push(main_frame_script(format!( + r#" + Object.defineProperty(window.__TAURI_INTERNALS__, 'metadata', {{ + value: {{ + currentWindow: {{ label: {current_window_label} }}, + currentWebview: {{ label: {current_webview_label} }} + }} + }}) + "#, + current_window_label = serde_json::to_string(window_label)?, + current_webview_label = serde_json::to_string(&label)?, + ))); + all_initialization_scripts.push(main_frame_script(self.initialization_script( + app_manager, + &ipc_init.into_string(), + &pattern_init.into_string(), + use_https_scheme, + )?)); + + all_initialization_scripts.extend(plugin_init_scripts); + + #[cfg(feature = "isolation")] + if let crate::Pattern::Isolation { schema, .. } = &*app_manager.pattern { + all_initialization_scripts.push(main_frame_script( + IsolationJavascript { + isolation_src: &crate::pattern::format_real_schema(schema, use_https_scheme), + style: tauri_utils::pattern::isolation::IFRAME_STYLE, + } + .render_default(&Default::default())? + .into_string(), + )); + } + + if let Some(plugin_global_api_scripts) = &*app_manager.plugin_global_api_scripts { + for &script in plugin_global_api_scripts.iter() { + all_initialization_scripts.push(main_frame_script(script.to_owned())); + } + } + + // Prepend `all_initialization_scripts` to `webview_attributes.initialization_scripts` + all_initialization_scripts.extend(webview_attributes.initialization_scripts); + webview_attributes.initialization_scripts = all_initialization_scripts; + + pending.webview_attributes = webview_attributes; + + let mut registered_scheme_protocols = Vec::new(); + + for (uri_scheme, protocol) in &*self.uri_scheme_protocols.lock().unwrap() { + registered_scheme_protocols.push(uri_scheme.clone()); + let protocol = protocol.clone(); + let app_handle = manager.app_handle().clone(); + + pending.register_uri_scheme_protocol(uri_scheme, move |webview_id, request, responder| { + let context = UriSchemeContext { + app_handle: &app_handle, + webview_label: webview_id, + }; + (protocol.handler)(context, request, UriSchemeResponder(responder)) + }); + } + + let window_url = Url::parse(&pending.url).unwrap(); + let window_origin = if window_url.scheme() == "data" { + "null".into() + } else if (cfg!(windows) || cfg!(target_os = "android")) + && window_url.scheme() != "http" + && window_url.scheme() != "https" + { + crate::protocol::origin(window_url.scheme(), use_https_scheme) + } else if let Some(host) = window_url.host() { + format!( + "{}://{}{}", + window_url.scheme(), + host, + window_url + .port() + .map(|p| format!(":{p}")) + .unwrap_or_default() + ) + } else { + "null".into() + }; + + if !registered_scheme_protocols.contains(&"tauri".into()) { + let web_resource_request_handler = pending.web_resource_request_handler.take(); + let protocol = crate::protocol::tauri::get( + manager.manager_owned(), + window_origin.clone(), + web_resource_request_handler, + ); + pending.register_uri_scheme_protocol("tauri", move |webview_id, request, responder| { + protocol(webview_id, request, UriSchemeResponder(responder)) + }); + registered_scheme_protocols.push("tauri".into()); + } + + if !registered_scheme_protocols.contains(&"ipc".into()) { + let protocol = crate::ipc::protocol::get(manager.manager_owned()); + pending.register_uri_scheme_protocol("ipc", move |webview_id, request, responder| { + protocol(webview_id, request, UriSchemeResponder(responder)) + }); + registered_scheme_protocols.push("ipc".into()); + } + + let label = pending.label.clone(); + let app_manager_ = manager.manager_owned(); + let on_page_load_handler = pending.on_page_load_handler.take(); + pending + .on_page_load_handler + .replace(Box::new(move |url, event| { + let payload = PageLoadPayload { url: &url, event }; + + if let Some(w) = app_manager_.get_webview(&label) { + if let Some(on_page_load) = &app_manager_.webview.on_page_load { + on_page_load(&w, &payload); + } + + app_manager_ + .plugins + .lock() + .unwrap() + .on_page_load(&w, &payload); + } + + if let Some(handler) = &on_page_load_handler { + handler(url, event); + } + })); + + let permission_request_handler = pending.permission_request_handler.take(); + if permission_request_handler.is_some() || self.on_permission_request.is_some() { + let label_ = pending.label.clone(); + let app_manager_ = manager.manager_owned(); + pending + .permission_request_handler + .replace(Box::new(move |kind| { + if let Some(handler) = &permission_request_handler { + let response = handler(kind); + if response != crate::webview::PermissionResponse::Default { + return response; + } + } + + if let Some(w) = app_manager_.get_webview(&label_) { + if let Some(on_permission_request) = &app_manager_.webview.on_permission_request { + return on_permission_request(w, kind); + } + } + + crate::webview::PermissionResponse::Default + })); + } + + #[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_) { + if 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 + .state + .get::() + .asset_protocol + .clone(); + let protocol = crate::protocol::asset::get(asset_scope, window_origin.clone()); + pending.register_uri_scheme_protocol("asset", move |webview_id, request, responder| { + protocol(webview_id, request, UriSchemeResponder(responder)) + }); + } + + #[cfg(feature = "isolation")] + if let crate::Pattern::Isolation { + assets, + schema, + key: _, + crypto_keys, + } = &*app_manager.pattern + { + let protocol = crate::protocol::isolation::get( + manager.manager_owned(), + schema, + assets.clone(), + *crypto_keys.aes_gcm().raw(), + window_origin, + use_https_scheme, + ); + pending.register_uri_scheme_protocol(schema, move |webview_id, request, responder| { + protocol(webview_id, request, UriSchemeResponder(responder)) + }); + } + + Ok(pending) + } + + fn initialization_script( + &self, + app_manager: &AppManager, + ipc_script: &str, + pattern_script: &str, + use_https_scheme: bool, + ) -> crate::Result { + #[derive(Template)] + #[default_template("../../scripts/init.js")] + struct InitJavascript<'a> { + #[raw] + pattern_script: &'a str, + #[raw] + ipc_script: &'a str, + #[raw] + core_script: &'a str, + #[raw] + event_initialization_script: &'a str, + #[raw] + freeze_prototype: &'a str, + } + + #[derive(Template)] + #[default_template("../../scripts/core.js")] + struct CoreJavascript<'a> { + os_name: &'a str, + protocol_scheme: &'a str, + invoke_key: &'a str, + } + + let freeze_prototype = if app_manager.config.app.security.freeze_prototype { + include_str!("../../scripts/freeze_prototype.js") + } else { + "" + }; + + InitJavascript { + pattern_script, + ipc_script, + core_script: &CoreJavascript { + os_name: std::env::consts::OS, + protocol_scheme: if use_https_scheme { "https" } else { "http" }, + invoke_key: self.invoke_key(), + } + .render_default(&Default::default())? + .into_string(), + event_initialization_script: &crate::event::event_initialization_script( + app_manager.listeners().function_name(), + app_manager.listeners().listeners_object_name(), + ), + freeze_prototype, + } + .render_default(&Default::default()) + .map(|s| s.into_string()) + .map_err(Into::into) + } + + pub fn prepare_webview>( + &self, + manager: &M, + mut pending: PendingWebview, + window_label: &str, + ) -> crate::Result> { + if self.webviews_lock().contains_key(&pending.label) { + return Err(crate::Error::WebviewLabelAlreadyExists(pending.label)); + } + + let app_manager = manager.manager(); + + #[allow(unused_mut)] // mut url only for the data-url parsing + let mut url = match &pending.webview_attributes.url { + WebviewUrl::App(path) => { + let app_url = app_manager.get_app_url(pending.webview_attributes.use_https_scheme); + let url = if PROXY_DEV_SERVER && is_local_network_url(&app_url) { + Cow::Owned(Url::parse("tauri://localhost").unwrap()) + } else { + app_url + }; + // ignore "index.html" just to simplify the url + if path.to_str() != Some("index.html") { + url + .join(&path.to_string_lossy()) + .map_err(crate::Error::InvalidUrl) + // this will never fail + .unwrap() + } else { + url.into_owned() + } + } + WebviewUrl::External(url) => { + let config_url = app_manager.get_app_url(pending.webview_attributes.use_https_scheme); + let is_app_url = config_url.make_relative(url).is_some(); + let mut url = url.clone(); + if is_app_url && PROXY_DEV_SERVER && is_local_network_url(&url) { + Url::parse("tauri://localhost").unwrap() + } else { + url + } + } + + WebviewUrl::CustomProtocol(url) => url.clone(), + _ => unimplemented!(), + }; + + #[cfg(not(feature = "webview-data-url"))] + if url.scheme() == "data" { + return Err(crate::Error::InvalidWebviewUrl( + "data URLs are not supported without the `webview-data-url` feature.", + )); + } + + #[cfg(feature = "webview-data-url")] + if let Some(csp) = app_manager.csp() { + if url.scheme() == "data" { + if let Ok(data_url) = data_url::DataUrl::process(url.as_str()) { + let (body, _) = data_url.decode_to_vec().unwrap(); + 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::html2::parse_doc(html); + tauri_utils::html2::inject_csp(&document, &csp.to_string()); + url.set_path(&format!("{},{}", mime::TEXT_HTML, document.html())); + } + } + } + } + + pending.url = url.to_string(); + + #[cfg(target_os = "android")] + { + pending = pending.on_webview_created(move |ctx| { + let plugin_manager = ctx + .env + .call_method( + ctx.activity, + "getPluginManager", + "()Lapp/tauri/plugin/PluginManager;", + &[], + )? + .l()?; + + // tell the manager the webview is ready + ctx.env.call_method( + plugin_manager, + "onWebViewCreated", + "(Landroid/webkit/WebView;)V", + &[ctx.webview.into()], + )?; + + Ok(()) + }); + } + + let label = pending.label.clone(); + pending = self.prepare_pending_webview(pending, &label, window_label, manager)?; + + pending.ipc_handler = Some(crate::ipc::protocol::message_handler( + manager.manager_owned(), + )); + + // in `windows`, we need to force a data_directory + // but we do respect user-specification + #[cfg(any(target_os = "linux", target_os = "windows"))] + if pending.webview_attributes.data_directory.is_none() { + let local_app_data = manager.path().resolve( + &app_manager.config.identifier, + crate::path::BaseDirectory::LocalData, + ); + if let Ok(user_data_dir) = local_app_data { + pending.webview_attributes.data_directory = Some(user_data_dir); + } + } + + // make sure the directory is created and available to prevent a panic + if let Some(user_data_dir) = &pending.webview_attributes.data_directory { + if !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)] + #[default_template("../webview/scripts/zoom-hotkey.js")] + struct HotkeyZoom<'a> { + os_name: &'a str, + } + + pending + .webview_attributes + .initialization_scripts + .push(InitializationScript { + script: HotkeyZoom { + os_name: std::env::consts::OS, + } + .render_default(&Default::default())? + .into_string(), + for_main_frame_only: true, + }) + } + + #[cfg(feature = "isolation")] + let pattern = app_manager.pattern.clone(); + let navigation_handler = pending.navigation_handler.take(); + let app_manager = manager.manager_owned(); + let label = pending.label.clone(); + pending.navigation_handler = Some(Box::new(move |url| { + // always allow navigation events for the isolation iframe and do not emit them for consumers + #[cfg(feature = "isolation")] + if let crate::Pattern::Isolation { schema, .. } = &*pattern { + if url.scheme() == schema + && url.domain() == Some(crate::pattern::ISOLATION_IFRAME_SRC_DOMAIN) + { + return true; + } + } + if let Some(handler) = &navigation_handler { + if !handler(url) { + return false; + } + } + let webview = app_manager.webview.webviews_lock().get(&label).cloned(); + if let Some(w) = webview { + app_manager + .plugins + .lock() + .expect("poisoned plugin store") + .on_navigation(&w, url) + } else { + true + } + })); + + Ok(pending) + } + + pub(crate) fn attach_webview( + &self, + window: Window, + webview: DetachedWebview, + use_https_scheme: bool, + ) -> Webview { + let webview = Webview::new(window, webview, use_https_scheme); + + let webview_event_listeners = self.event_listeners.clone(); + let webview_ = webview.clone(); + webview.on_webview_event(move |event| { + let _ = on_webview_event(&webview_, event); + for handler in webview_event_listeners.iter() { + handler(&webview_, event); + } + }); + + // insert the webview into our manager + { + self + .webviews_lock() + .insert(webview.label().to_string(), webview.clone()); + } + + // let plugins know that a new webview has been added to the manager + let manager = webview.manager_owned(); + let webview_ = webview.clone(); + // run on main thread so the plugin store doesn't dead lock with the event loop handler in App + let _ = webview.run_on_main_thread(move || { + manager + .plugins + .lock() + .expect("poisoned plugin store") + .webview_created(webview_); + }); + + #[cfg(all(target_os = "ios", feature = "wry"))] + { + webview + .with_webview(|w| { + unsafe { crate::ios::on_webview_created(w.inner() as _, w.view_controller() as _) }; + }) + .expect("failed to run on_webview_created hook"); + } + + let event = crate::EventName::from_str("tauri://webview-created"); + let payload = Some(crate::webview::CreatedEvent { + label: webview.label().into(), + }); + + let _ = webview + .manager + .emit(event, EmitPayload::Serialize(&payload)); + + webview + } + + pub fn eval_script_all>(&self, script: S) -> crate::Result<()> { + let script = script.into(); + let webviews = self.webviews_lock().values().cloned().collect::>(); + webviews + .iter() + .try_for_each(|webview| webview.eval(&script)) + } + + pub fn labels(&self) -> HashSet { + self.webviews_lock().keys().cloned().collect() + } + + pub(crate) fn invoke_key(&self) -> &str { + &self.invoke_key + } +} + +impl Webview { + /// Emits event to [`EventTarget::Window`] and [`EventTarget::WebviewWindow`] + fn emit_to_webview( + &self, + event: crate::EventName<&str>, + payload: &S, + ) -> crate::Result<()> { + let window_label = self.label(); + let payload = EmitPayload::Serialize(payload); + self + .manager() + .emit_filter(event, payload, |target| match target { + EventTarget::Webview { label } | EventTarget::WebviewWindow { label } => { + label == window_label + } + _ => false, + }) + } +} + +fn on_webview_event(webview: &Webview, event: &WebviewEvent) -> crate::Result<()> { + match event { + WebviewEvent::DragDrop(event) => match event { + DragDropEvent::Enter { paths, position } => { + let payload = DragDropPayload { + paths: Some(paths), + position, + }; + webview.emit_to_webview(DRAG_ENTER_EVENT, &payload)? + } + DragDropEvent::Over { position } => { + let payload = DragDropPayload { + position, + paths: None, + }; + webview.emit_to_webview(DRAG_OVER_EVENT, &payload)? + } + DragDropEvent::Drop { paths, position } => { + let scopes = webview.state::(); + for path in paths { + if path.is_file() { + let _ = scopes.allow_file(path); + } else { + let _ = scopes.allow_directory(path, false); + } + } + let payload = DragDropPayload { + paths: Some(paths), + position, + }; + webview.emit_to_webview(DRAG_DROP_EVENT, &payload)? + } + DragDropEvent::Leave => webview.emit_to_webview(DRAG_LEAVE_EVENT, &())?, + _ => unimplemented!(), + }, + } + + Ok(()) +} + +fn is_local_network_url(url: &url::Url) -> bool { + match url.host() { + Some(url::Host::Domain(s)) => s == "localhost", + Some(url::Host::Ipv4(_)) | Some(url::Host::Ipv6(_)) => true, + None => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn local_network_url() { + assert!(is_local_network_url(&"http://localhost".parse().unwrap())); + assert!(is_local_network_url( + &"http://127.0.0.1:8080".parse().unwrap() + )); + assert!(is_local_network_url( + &"https://192.168.3.17".parse().unwrap() + )); + + assert!(!is_local_network_url(&"https://tauri.app".parse().unwrap())); + } +} diff --git a/crates/tauri/src/manager/window.rs b/crates/tauri/src/manager/window.rs new file mode 100644 index 0000000..717c109 --- /dev/null +++ b/crates/tauri/src/manager/window.rs @@ -0,0 +1,285 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::{HashMap, HashSet}, + fmt, + path::PathBuf, + sync::{Arc, Mutex, MutexGuard}, +}; + +use serde::Serialize; +use tauri_runtime::{ + dpi::{PhysicalPosition, PhysicalSize}, + window::WindowBuilder, + window::{DetachedWindow, DragDropEvent, PendingWindow}, +}; + +use crate::{ + AppHandle, EventLoopMessage, EventTarget, Manager, Runtime, Scopes, Window, WindowEvent, + app::GlobalWindowEventListener, event::EventName, image::Image, sealed::ManagerBase, +}; + +use super::EmitPayload; + +const WINDOW_RESIZED_EVENT: EventName<&str> = EventName::from_str("tauri://resize"); +const WINDOW_MOVED_EVENT: EventName<&str> = EventName::from_str("tauri://move"); +const WINDOW_CLOSE_REQUESTED_EVENT: EventName<&str> = + EventName::from_str("tauri://close-requested"); +const WINDOW_DESTROYED_EVENT: EventName<&str> = EventName::from_str("tauri://destroyed"); +const WINDOW_FOCUS_EVENT: EventName<&str> = EventName::from_str("tauri://focus"); +const WINDOW_BLUR_EVENT: EventName<&str> = EventName::from_str("tauri://blur"); +const WINDOW_SCALE_FACTOR_CHANGED_EVENT: EventName<&str> = + EventName::from_str("tauri://scale-change"); +const WINDOW_THEME_CHANGED: EventName<&str> = EventName::from_str("tauri://theme-changed"); +pub(crate) const DRAG_ENTER_EVENT: EventName<&str> = EventName::from_str("tauri://drag-enter"); +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>>, + pub default_icon: Option>, + /// Window event listeners to all windows. + pub event_listeners: Arc>>, +} + +impl fmt::Debug for WindowManager { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WindowManager") + .field("default_window_icon", &self.default_icon) + .finish() + } +} + +impl WindowManager { + /// Get a locked handle to the windows. + pub(crate) fn windows_lock(&self) -> MutexGuard<'_, HashMap>> { + self.windows.lock().expect("poisoned window manager") + } + + pub fn prepare_window( + &self, + mut pending: PendingWindow, + ) -> crate::Result> { + if self.windows_lock().contains_key(&pending.label) { + return Err(crate::Error::WindowLabelAlreadyExists(pending.label)); + } + + if !pending.window_builder.has_icon() { + if let Some(default_window_icon) = self.default_icon.clone() { + pending.window_builder = pending.window_builder.icon(default_window_icon.into())?; + } + } + + Ok(pending) + } + + pub(crate) fn attach_window( + &self, + app_handle: AppHandle, + window: DetachedWindow, + #[cfg(desktop)] menu: Option>, + ) -> Window { + let window = Window::new( + app_handle.manager.clone(), + window, + app_handle, + #[cfg(desktop)] + menu, + ); + + let window_ = window.clone(); + let window_event_listeners = self.event_listeners.clone(); + window.on_window_event(move |event| { + let _ = on_window_event(&window_, event); + for handler in window_event_listeners.iter() { + handler(&window_, event); + } + }); + + // insert the window into our manager + { + self + .windows_lock() + .insert(window.label().to_string(), window.clone()); + } + + // let plugins know that a new window has been added to the manager + let manager = window.manager.clone(); + let window_ = window.clone(); + // run on main thread so the plugin store doesn't dead lock with the event loop handler in App + let _ = window.run_on_main_thread(move || { + manager + .plugins + .lock() + .expect("poisoned plugin store") + .window_created(window_); + }); + + window + } + + pub fn labels(&self) -> HashSet { + self.windows_lock().keys().cloned().collect() + } +} + +impl Window { + /// Emits event to [`EventTarget::Window`] and [`EventTarget::WebviewWindow`] + fn emit_to_window(&self, event: EventName<&str>, payload: &S) -> crate::Result<()> { + let window_label = self.label(); + let payload = EmitPayload::Serialize(payload); + self + .manager() + .emit_filter(event, payload, |target| match target { + EventTarget::Window { label } | EventTarget::WebviewWindow { label } => { + label == window_label + } + _ => false, + }) + } + + /// Checks whether has js listener for [`EventTarget::Window`] or [`EventTarget::WebviewWindow`] + fn has_js_listener(&self, event: EventName<&str>) -> bool { + let window_label = self.label(); + let listeners = self.manager().listeners(); + listeners.has_js_listener(event, |target| match target { + EventTarget::Window { label } | EventTarget::WebviewWindow { label } => label == window_label, + _ => false, + }) + } +} + +#[derive(Serialize, Clone)] +pub(crate) struct DragDropPayload<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + pub paths: Option<&'a Vec>, + pub position: &'a PhysicalPosition, +} + +fn on_window_event(window: &Window, event: &WindowEvent) -> crate::Result<()> { + match event { + WindowEvent::Resized(size) => window.emit_to_window(WINDOW_RESIZED_EVENT, size)?, + WindowEvent::Moved(position) => window.emit_to_window(WINDOW_MOVED_EVENT, position)?, + WindowEvent::CloseRequested { api } => { + if window.has_js_listener(WINDOW_CLOSE_REQUESTED_EVENT) { + api.prevent_close(); + } + window.emit_to_window(WINDOW_CLOSE_REQUESTED_EVENT, &())?; + } + WindowEvent::Destroyed => { + window.emit_to_window(WINDOW_DESTROYED_EVENT, &())?; + } + WindowEvent::Focused(focused) => window.emit_to_window( + if *focused { + WINDOW_FOCUS_EVENT + } else { + WINDOW_BLUR_EVENT + }, + &(), + )?, + WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + .. + } => window.emit_to_window( + WINDOW_SCALE_FACTOR_CHANGED_EVENT, + &ScaleFactorChanged { + scale_factor: *scale_factor, + size: *new_inner_size, + }, + )?, + WindowEvent::DragDrop(event) => match event { + DragDropEvent::Enter { paths, position } => { + let payload = DragDropPayload { + paths: Some(paths), + position, + }; + + if window.is_webview_window() { + // use underlying manager, otherwise have to recheck EventName + window.manager().emit_to( + EventTarget::labeled(window.label()), + DRAG_ENTER_EVENT, + EmitPayload::Serialize(&payload), + )? + } else { + window.emit_to_window(DRAG_ENTER_EVENT, &payload)? + } + } + DragDropEvent::Over { position } => { + let payload = DragDropPayload { + position, + paths: None, + }; + if window.is_webview_window() { + // use underlying manager, otherwise have to recheck EventName + window.manager().emit_to( + EventTarget::labeled(window.label()), + DRAG_OVER_EVENT, + EmitPayload::Serialize(&payload), + )? + } else { + window.emit_to_window(DRAG_OVER_EVENT, &payload)? + } + } + DragDropEvent::Drop { paths, position } => { + let scopes = window.state::(); + for path in paths { + if path.is_file() { + let _ = scopes.allow_file(path); + } else { + let _ = scopes.allow_directory(path, true); + } + } + let payload = DragDropPayload { + paths: Some(paths), + position, + }; + + if window.is_webview_window() { + // use underlying manager, otherwise have to recheck EventName + window.manager().emit_to( + EventTarget::labeled(window.label()), + DRAG_DROP_EVENT, + EmitPayload::Serialize(&payload), + )? + } else { + window.emit_to_window(DRAG_DROP_EVENT, &payload)? + } + } + DragDropEvent::Leave => { + if window.is_webview_window() { + // use underlying manager, otherwise have to recheck EventName + window.manager().emit_to( + EventTarget::labeled(window.label()), + DRAG_LEAVE_EVENT, + EmitPayload::Serialize(&()), + )? + } else { + window.emit_to_window(DRAG_LEAVE_EVENT, &())? + } + } + _ => 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(()) +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct ScaleFactorChanged { + scale_factor: f64, + size: PhysicalSize, +} diff --git a/crates/tauri/src/menu/builders/check.rs b/crates/tauri/src/menu/builders/check.rs new file mode 100644 index 0000000..15cf837 --- /dev/null +++ b/crates/tauri/src/menu/builders/check.rs @@ -0,0 +1,90 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{Manager, Runtime, menu::CheckMenuItem, menu::MenuId}; + +/// A builder type for [`CheckMenuItem`] +pub struct CheckMenuItemBuilder { + id: Option, + text: String, + enabled: bool, + checked: bool, + accelerator: Option, +} + +impl CheckMenuItemBuilder { + /// Create a new menu item builder. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new>(text: S) -> Self { + Self { + id: None, + text: text.as_ref().to_string(), + enabled: true, + checked: true, + accelerator: None, + } + } + + /// Create a new menu item builder with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, S: AsRef>(id: I, text: S) -> Self { + Self { + id: Some(id.into()), + text: text.as_ref().to_string(), + enabled: true, + checked: true, + accelerator: None, + } + } + + /// Set the id for this menu item. + pub fn id>(mut self, id: I) -> Self { + self.id.replace(id.into()); + self + } + + /// Set the enabled state for this menu item. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Set the checked state for this menu item. + pub fn checked(mut self, checked: bool) -> Self { + self.checked = checked; + self + } + + /// Set the accelerator for this menu item. + pub fn accelerator>(mut self, accelerator: S) -> Self { + self.accelerator.replace(accelerator.as_ref().to_string()); + self + } + + /// Build the menu item + pub fn build>(self, manager: &M) -> crate::Result> { + if let Some(id) = self.id { + CheckMenuItem::with_id( + manager, + id, + self.text, + self.enabled, + self.checked, + self.accelerator, + ) + } else { + CheckMenuItem::new( + manager, + self.text, + self.enabled, + self.checked, + self.accelerator, + ) + } + } +} diff --git a/crates/tauri/src/menu/builders/icon.rs b/crates/tauri/src/menu/builders/icon.rs new file mode 100644 index 0000000..fd37709 --- /dev/null +++ b/crates/tauri/src/menu/builders/icon.rs @@ -0,0 +1,130 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + Manager, Runtime, + image::Image, + menu::{IconMenuItem, MenuId, NativeIcon}, +}; + +/// A builder type for [`IconMenuItem`] +pub struct IconMenuItemBuilder<'a> { + id: Option, + text: String, + enabled: bool, + icon: Option>, + native_icon: Option, + accelerator: Option, +} + +impl<'a> IconMenuItemBuilder<'a> { + /// Create a new menu item builder. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new>(text: S) -> Self { + Self { + id: None, + text: text.as_ref().to_string(), + enabled: true, + icon: None, + native_icon: None, + accelerator: None, + } + } + + /// Create a new menu item builder with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, S: AsRef>(id: I, text: S) -> Self { + Self { + id: Some(id.into()), + text: text.as_ref().to_string(), + enabled: true, + icon: None, + native_icon: None, + accelerator: None, + } + } + + /// Set the id for this menu item. + pub fn id>(mut self, id: I) -> Self { + self.id.replace(id.into()); + self + } + + /// Set the enabled state for this menu item. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Set the accelerator for this menu item. + pub fn accelerator>(mut self, accelerator: S) -> Self { + self.accelerator.replace(accelerator.as_ref().to_string()); + self + } + + /// Set the icon for this menu item. + /// + /// **Note:** This method conflicts with [`Self::native_icon`] + /// so calling one of them, will reset the other. + pub fn icon(mut self, icon: Image<'a>) -> Self { + self.icon.replace(icon); + self.native_icon = None; + self + } + + /// Set the icon for this menu item. + /// + /// **Note:** This method conflicts with [`Self::icon`] + /// so calling one of them, will reset the other. + pub fn native_icon(mut self, icon: NativeIcon) -> Self { + self.native_icon.replace(icon); + self.icon = None; + self + } + + /// Build the menu item + pub fn build>(self, manager: &M) -> crate::Result> { + if self.icon.is_some() { + if let Some(id) = self.id { + IconMenuItem::with_id( + manager, + id, + self.text, + self.enabled, + self.icon, + self.accelerator, + ) + } else { + IconMenuItem::new( + manager, + self.text, + self.enabled, + self.icon, + self.accelerator, + ) + } + } else if let Some(id) = self.id { + IconMenuItem::with_id_and_native_icon( + manager, + id, + self.text, + self.enabled, + self.native_icon, + self.accelerator, + ) + } else { + IconMenuItem::with_native_icon( + manager, + self.text, + self.enabled, + self.native_icon, + self.accelerator, + ) + } + } +} diff --git a/crates/tauri/src/menu/builders/menu.rs b/crates/tauri/src/menu/builders/menu.rs new file mode 100644 index 0000000..069ca4d --- /dev/null +++ b/crates/tauri/src/menu/builders/menu.rs @@ -0,0 +1,675 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{Manager, Runtime, image::Image, menu::*}; + +/// A builder type for [`Menu`] +/// +/// ## Platform-specific: +/// +/// - **macOS**: if using [`MenuBuilder`] for the global menubar, it can only contain [`Submenu`]s +/// +/// # Example +/// +/// ```no_run +/// use tauri::menu::*; +/// tauri::Builder::default() +/// .setup(move |app| { +/// let handle = app.handle(); +/// # let icon1 = tauri::image::Image::new(&[], 0, 0); +/// let menu = MenuBuilder::new(handle) +/// .item(&MenuItem::new(handle, "MenuItem 1", true, None::<&str>)?) +/// .items(&[ +/// &CheckMenuItem::new(handle, "CheckMenuItem 1", true, true, None::<&str>)?, +/// &IconMenuItem::new(handle, "IconMenuItem 1", true, Some(icon1), None::<&str>)?, +/// ]) +/// .separator() +/// .cut() +/// .copy() +/// .paste() +/// .separator() +/// .text("item2", "MenuItem 2") +/// .check("checkitem2", "CheckMenuItem 2") +/// .icon("iconitem2", "IconMenuItem 2", app.default_window_icon().cloned().unwrap()) +/// .build()?; +/// app.set_menu(menu); +/// Ok(()) +/// }); +/// ``` +pub struct MenuBuilder<'m, R: Runtime, M: Manager> { + pub(crate) id: Option, + pub(crate) manager: &'m M, + pub(crate) items: Vec>>, +} + +impl<'m, R: Runtime, M: Manager> MenuBuilder<'m, R, M> { + /// Create a new menu builder. + pub fn new(manager: &'m M) -> Self { + Self { + id: None, + items: Vec::new(), + manager, + } + } + + /// Create a new menu builder with the specified id. + pub fn with_id>(manager: &'m M, id: I) -> Self { + Self { + id: Some(id.into()), + items: Vec::new(), + manager, + } + } + + /// Builds this menu + pub fn build(self) -> crate::Result> { + let menu = if let Some(id) = self.id { + Menu::with_id(self.manager, id)? + } else { + Menu::new(self.manager)? + }; + + for item in self.items { + let item = item?; + menu.append(&item)?; + } + + Ok(menu) + } +} + +/// A builder type for [`Submenu`] +/// +/// # Example +/// +/// ```no_run +/// use tauri::menu::*; +/// tauri::Builder::default() +/// .setup(move |app| { +/// let handle = app.handle(); +/// # let icon1 = tauri::image::Image::new(&[], 0, 0); +/// # let icon2 = icon1.clone(); +/// let menu = Menu::new(handle)?; +/// let submenu = SubmenuBuilder::new(handle, "File") +/// .item(&MenuItem::new(handle, "MenuItem 1", true, None::<&str>)?) +/// .items(&[ +/// &CheckMenuItem::new(handle, "CheckMenuItem 1", true, true, None::<&str>)?, +/// &IconMenuItem::new(handle, "IconMenuItem 1", true, Some(icon1), None::<&str>)?, +/// ]) +/// .separator() +/// .cut() +/// .copy() +/// .paste() +/// .separator() +/// .text("item2", "MenuItem 2") +/// .check("checkitem2", "CheckMenuItem 2") +/// .icon("iconitem2", "IconMenuItem 2", app.default_window_icon().cloned().unwrap()) +/// .build()?; +/// menu.append(&submenu)?; +/// app.set_menu(menu); +/// Ok(()) +/// }); +/// ``` +pub struct SubmenuBuilder<'m, R: Runtime, M: Manager> { + pub(crate) id: Option, + pub(crate) manager: &'m M, + pub(crate) text: String, + pub(crate) enabled: bool, + pub(crate) items: Vec>>, + pub(crate) icon: Option>, + pub(crate) native_icon: Option, +} + +impl<'m, R: Runtime, M: Manager> SubmenuBuilder<'m, R, M> { + /// Create a new submenu builder. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new>(manager: &'m M, text: S) -> Self { + Self { + id: None, + items: Vec::new(), + text: text.as_ref().to_string(), + enabled: true, + manager, + icon: None, + native_icon: None, + } + } + + /// Create a new submenu builder with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, S: AsRef>(manager: &'m M, id: I, text: S) -> Self { + Self { + id: Some(id.into()), + text: text.as_ref().to_string(), + enabled: true, + items: Vec::new(), + manager, + icon: None, + native_icon: None, + } + } + + /// Set an icon for the submenu. + /// Calling this method resets the native_icon. + pub fn submenu_icon(mut self, icon: crate::image::Image<'m>) -> Self { + self.icon = Some(icon); + self.native_icon = None; + self + } + + /// Set a native icon for the submenu. + /// Calling this method resets the icon. + pub fn submenu_native_icon(mut self, icon: NativeIcon) -> Self { + self.native_icon = Some(icon); + self.icon = None; + self + } + + /// Set the enabled state for the submenu. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Builds this submenu + pub fn build(self) -> crate::Result> { + let submenu = if let Some(id) = self.id { + if let Some(icon) = self.icon { + Submenu::with_id_and_icon(self.manager, id, self.text, self.enabled, Some(icon))? + } else if let Some(native_icon) = self.native_icon { + Submenu::with_id_and_native_icon( + self.manager, + id, + self.text, + self.enabled, + Some(native_icon), + )? + } else { + Submenu::with_id(self.manager, id, self.text, self.enabled)? + } + } else if let Some(icon) = self.icon { + Submenu::new_with_icon(self.manager, self.text, self.enabled, Some(icon))? + } else if let Some(native_icon) = self.native_icon { + Submenu::new_with_native_icon(self.manager, self.text, self.enabled, Some(native_icon))? + } else { + Submenu::new(self.manager, self.text, self.enabled)? + }; + + for item in self.items { + let item = item?; + submenu.append(&item)?; + } + + Ok(submenu) + } +} + +macro_rules! shared_menu_builder { + ($menu: ty) => { + impl<'m, R: Runtime, M: Manager> $menu { + /// Set the id for this menu. + pub fn id>(mut self, id: I) -> Self { + self.id.replace(id.into()); + self + } + + /// Add this item to the menu. + pub fn item(mut self, item: &dyn IsMenuItem) -> Self { + self.items.push(Ok(item.kind())); + self + } + + /// Add these items to the menu. + pub fn items(mut self, items: &[&dyn IsMenuItem]) -> Self { + for item in items { + self = self.item(*item); + } + self + } + + /// Add a [MenuItem] to the menu. + pub fn text, S: AsRef>(mut self, id: I, text: S) -> Self { + self + .items + .push(MenuItem::with_id(self.manager, id, text, true, None::<&str>).map(|i| i.kind())); + self + } + + /// Add a [CheckMenuItem] to the menu. + pub fn check, S: AsRef>(mut self, id: I, text: S) -> Self { + self.items.push( + CheckMenuItem::with_id(self.manager, id, text, true, true, None::<&str>) + .map(|i| i.kind()), + ); + self + } + + /// Add an [IconMenuItem] to the menu. + pub fn icon, S: AsRef>( + mut self, + id: I, + text: S, + icon: Image<'_>, + ) -> Self { + self.items.push( + IconMenuItem::with_id(self.manager, id, text, true, Some(icon), None::<&str>) + .map(|i| i.kind()), + ); + self + } + + /// Add an [IconMenuItem] with a native icon to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn native_icon, S: AsRef>( + mut self, + id: I, + text: S, + icon: NativeIcon, + ) -> Self { + self.items.push( + IconMenuItem::with_id_and_native_icon( + self.manager, + id, + text, + true, + Some(icon), + None::<&str>, + ) + .map(|i| i.kind()), + ); + self + } + + /// Add Separator menu item to the menu. + pub fn separator(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::separator(self.manager).map(|i| i.kind())); + self + } + + /// Add Copy menu item to the menu. + pub fn copy(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::copy(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Copy menu item with specified text to the menu. + pub fn copy_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::copy(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Cut menu item to the menu. + pub fn cut(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::cut(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Cut menu item with specified text to the menu. + pub fn cut_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::cut(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Paste menu item to the menu. + pub fn paste(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::paste(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Paste menu item with specified text to the menu. + pub fn paste_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::paste(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add SelectAll menu item to the menu. + pub fn select_all(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::select_all(self.manager, None).map(|i| i.kind())); + self + } + + /// Add SelectAll menu item with specified text to the menu. + pub fn select_all_with_text>(mut self, text: S) -> Self { + self.items.push( + PredefinedMenuItem::select_all(self.manager, Some(text.as_ref())).map(|i| i.kind()), + ); + self + } + + /// Add Undo menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn undo(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::undo(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Undo menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn undo_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::undo(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + /// Add Redo menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn redo(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::redo(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Redo menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn redo_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::redo(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Minimize window menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn minimize(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::minimize(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Minimize window menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn minimize_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::minimize(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Maximize window menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn maximize(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::maximize(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Maximize window menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn maximize_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::maximize(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Fullscreen menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn fullscreen(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::fullscreen(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Fullscreen menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn fullscreen_with_text>(mut self, text: S) -> Self { + self.items.push( + PredefinedMenuItem::fullscreen(self.manager, Some(text.as_ref())).map(|i| i.kind()), + ); + self + } + + /// Add Hide window menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::hide(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Hide window menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::hide(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Hide other windows menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide_others(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::hide_others(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Hide other windows menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide_others_with_text>(mut self, text: S) -> Self { + self.items.push( + PredefinedMenuItem::hide_others(self.manager, Some(text.as_ref())).map(|i| i.kind()), + ); + self + } + + /// Add Show all app windows menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn show_all(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::show_all(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Show all app windows menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn show_all_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::show_all(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add Close window menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn close_window(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::close_window(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Close window menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn close_window_with_text>(mut self, text: S) -> Self { + self.items.push( + PredefinedMenuItem::close_window(self.manager, Some(text.as_ref())).map(|i| i.kind()), + ); + self + } + + /// Add Quit app menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn quit(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::quit(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Quit app menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn quit_with_text>(mut self, text: S) -> Self { + self + .items + .push(PredefinedMenuItem::quit(self.manager, Some(text.as_ref())).map(|i| i.kind())); + self + } + + /// Add About app menu item to the menu. + pub fn about(mut self, metadata: Option>) -> Self { + self + .items + .push(PredefinedMenuItem::about(self.manager, None, metadata).map(|i| i.kind())); + self + } + + /// Add About app menu item with specified text to the menu. + pub fn about_with_text>( + mut self, + text: S, + metadata: Option>, + ) -> Self { + self.items.push( + PredefinedMenuItem::about(self.manager, Some(text.as_ref()), metadata).map(|i| i.kind()), + ); + self + } + + /// Add Services menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn services(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::services(self.manager, None).map(|i| i.kind())); + self + } + + /// Add Services menu item with specified text to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn services_with_text>(mut self, text: S) -> Self { + self + .items + .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 + } + } + }; +} + +shared_menu_builder!(MenuBuilder<'m, R, M>); +shared_menu_builder!(SubmenuBuilder<'m, R, M>); diff --git a/crates/tauri/src/menu/builders/mod.rs b/crates/tauri/src/menu/builders/mod.rs new file mode 100644 index 0000000..ce816fb --- /dev/null +++ b/crates/tauri/src/menu/builders/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg(desktop)] + +//! A module containing menu builder types + +mod menu; +pub use menu::MenuBuilder; +pub use menu::SubmenuBuilder; +mod normal; +pub use normal::MenuItemBuilder; +mod check; +pub use check::CheckMenuItemBuilder; +mod icon; +pub use icon::IconMenuItemBuilder; diff --git a/crates/tauri/src/menu/builders/normal.rs b/crates/tauri/src/menu/builders/normal.rs new file mode 100644 index 0000000..e5cb514 --- /dev/null +++ b/crates/tauri/src/menu/builders/normal.rs @@ -0,0 +1,68 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{Manager, Runtime, menu::MenuId, menu::MenuItem}; + +/// A builder type for [`MenuItem`] +pub struct MenuItemBuilder { + id: Option, + text: String, + enabled: bool, + accelerator: Option, +} + +impl MenuItemBuilder { + /// Create a new menu item builder. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new>(text: S) -> Self { + Self { + id: None, + text: text.as_ref().to_string(), + enabled: true, + accelerator: None, + } + } + + /// Create a new menu item builder with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, S: AsRef>(id: I, text: S) -> Self { + Self { + id: Some(id.into()), + text: text.as_ref().to_string(), + enabled: true, + accelerator: None, + } + } + + /// Set the id for this menu item. + pub fn id>(mut self, id: I) -> Self { + self.id.replace(id.into()); + self + } + + /// Set the enabled state for this menu item. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Set the accelerator for this menu item. + pub fn accelerator>(mut self, accelerator: S) -> Self { + self.accelerator.replace(accelerator.as_ref().to_string()); + self + } + + /// Build the menu item + pub fn build>(self, manager: &M) -> crate::Result> { + if let Some(id) = self.id { + MenuItem::with_id(manager, id, self.text, self.enabled, self.accelerator) + } else { + MenuItem::new(manager, self.text, self.enabled, self.accelerator) + } + } +} diff --git a/crates/tauri/src/menu/check.rs b/crates/tauri/src/menu/check.rs new file mode 100644 index 0000000..cf7d3c9 --- /dev/null +++ b/crates/tauri/src/menu/check.rs @@ -0,0 +1,129 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use super::run_item_main_thread; +use crate::menu::CheckMenuItemInner; +use crate::run_main_thread; +use crate::{AppHandle, Manager, Runtime, menu::MenuId}; + +use super::CheckMenuItem; + +impl CheckMenuItem { + /// Create a new menu item. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new( + manager: &M, + text: T, + enabled: bool, + checked: bool, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.as_ref().to_owned(); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + + let item = run_main_thread!(handle, || { + let item = muda::CheckMenuItem::new(text, enabled, checked, accelerator); + CheckMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Create a new menu item with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id( + manager: &M, + id: I, + text: T, + enabled: bool, + checked: bool, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + I: Into, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let id = id.into(); + let text = text.as_ref().to_owned(); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + + let item = run_main_thread!(handle, move || { + let item = muda::CheckMenuItem::with_id(id, text, enabled, checked, accelerator); + CheckMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.0.app_handle + } + + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + self.0.inner.id() + } + + /// Get the text for this menu item. + pub fn text(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text()) + } + + /// Set the text for this menu item. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text)) + } + + /// Get whether this menu item is enabled or not. + pub fn is_enabled(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled()) + } + + /// Enable or disable this menu item. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled)) + } + + /// Set this menu item accelerator. + pub fn set_accelerator>(&self, accelerator: Option) -> crate::Result<()> { + let accel = accelerator.and_then(|s| s.as_ref().parse().ok()); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().set_accelerator(accel) + })? + .map_err(Into::into) + } + + /// Get whether this check menu item is checked or not. + pub fn is_checked(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_checked()) + } + + /// Check or Uncheck this check menu item. + pub fn set_checked(&self, checked: bool) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_checked(checked)) + } +} diff --git a/crates/tauri/src/menu/icon.rs b/crates/tauri/src/menu/icon.rs new file mode 100644 index 0000000..d74cff6 --- /dev/null +++ b/crates/tauri/src/menu/icon.rs @@ -0,0 +1,220 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use super::run_item_main_thread; +use super::{IconMenuItem, NativeIcon}; +use crate::menu::IconMenuItemInner; +use crate::run_main_thread; +use crate::{AppHandle, Manager, Runtime, image::Image, menu::MenuId}; + +impl IconMenuItem { + /// Create a new menu item. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new( + manager: &M, + text: T, + enabled: bool, + icon: Option>, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.as_ref().to_owned(); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + let icon = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + + let item = run_main_thread!(handle, || { + let item = muda::IconMenuItem::new(text, enabled, icon, accelerator); + IconMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Create a new menu item with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id( + manager: &M, + id: I, + text: T, + enabled: bool, + icon: Option>, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + I: Into, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let id = id.into(); + let text = text.as_ref().to_owned(); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + let icon = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + + let item = run_main_thread!(handle, move || { + let item = muda::IconMenuItem::with_id(id, text, enabled, icon, accelerator); + IconMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Create a new icon menu item but with a native icon. + /// + /// See [`IconMenuItem::new`] for more info. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn with_native_icon( + manager: &M, + text: T, + enabled: bool, + native_icon: Option, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.as_ref().to_owned(); + let icon = native_icon.map(Into::into); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + + let item = run_main_thread!(handle, || { + let item = muda::IconMenuItem::with_native_icon(text, enabled, icon, accelerator); + IconMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Create a new icon menu item with the specified id but with a native icon. + /// + /// See [`IconMenuItem::new`] for more info. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn with_id_and_native_icon( + manager: &M, + id: I, + text: T, + enabled: bool, + native_icon: Option, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + I: Into, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let id = id.into(); + let text = text.as_ref().to_owned(); + let icon = native_icon.map(Into::into); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + + let item = run_main_thread!(handle, move || { + let item = muda::IconMenuItem::with_id_and_native_icon(id, text, enabled, icon, accelerator); + IconMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.0.app_handle + } + + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + self.0.inner.id() + } + + /// Get the text for this menu item. + pub fn text(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text()) + } + + /// Set the text for this menu item. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text)) + } + + /// Get whether this menu item is enabled or not. + pub fn is_enabled(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled()) + } + + /// Enable or disable this menu item. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled)) + } + + /// Set this menu item accelerator. + pub fn set_accelerator>(&self, accelerator: Option) -> crate::Result<()> { + let accel = accelerator.and_then(|s| s.as_ref().parse().ok()); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().set_accelerator(accel) + })? + .map_err(Into::into) + } + + /// Change this menu item icon or remove it. + pub fn set_icon(&self, icon: Option>) -> crate::Result<()> { + let icon = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_icon(icon)) + } + + /// Change this menu item icon to a native image or remove it. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn set_native_icon(&self, _icon: Option) -> crate::Result<()> { + #[cfg(target_os = "macos")] + return run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().set_native_icon(_icon.map(Into::into)) + }); + #[allow(unreachable_code)] + Ok(()) + } +} diff --git a/crates/tauri/src/menu/menu.rs b/crates/tauri/src/menu/menu.rs new file mode 100644 index 0000000..ba99e47 --- /dev/null +++ b/crates/tauri/src/menu/menu.rs @@ -0,0 +1,400 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use super::run_item_main_thread; +use super::sealed::ContextMenuBase; +use super::{ + AboutMetadata, IsMenuItem, Menu, MenuInner, MenuItemKind, PredefinedMenuItem, Submenu, +}; +use crate::Window; +use crate::run_main_thread; +use crate::{AppHandle, Manager, Position, Runtime}; +use muda::ContextMenu; +use muda::MenuId; + +/// Expected submenu id of the Window menu for macOS. +pub const WINDOW_SUBMENU_ID: &str = "__tauri_window_menu__"; +/// Expected submenu id of the Help menu for macOS. +pub const HELP_SUBMENU_ID: &str = "__tauri_help_menu__"; + +impl super::ContextMenu for Menu { + #[cfg(target_os = "windows")] + fn hpopupmenu(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().hpopupmenu()) + } + + fn popup(&self, window: Window) -> crate::Result<()> { + self.popup_inner(window, None::) + } + + fn popup_at>( + &self, + window: Window, + position: P, + ) -> crate::Result<()> { + self.popup_inner(window, Some(position)) + } +} + +impl ContextMenuBase for Menu { + fn popup_inner>( + &self, + window: crate::Window, + position: Option

, + ) -> crate::Result<()> { + let position = position.map(Into::into); + run_item_main_thread!(self, move |self_: Self| { + #[cfg(target_os = "macos")] + if let Ok(view) = window.ns_view() { + unsafe { + self_ + .inner() + .show_context_menu_for_nsview(view as _, position); + } + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(w) = window.gtk_window() { + self_ + .inner() + .show_context_menu_for_gtk_window(w.as_ref(), position); + } + + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + unsafe { + self_ + .inner() + .show_context_menu_for_hwnd(hwnd.0 as _, position); + } + } + }) + } + fn inner_context(&self) -> &dyn muda::ContextMenu { + (*self.0).as_ref() + } + + fn inner_context_owned(&self) -> Box { + Box::new((*self.0).as_ref().clone()) + } +} + +impl Menu { + /// Creates a new menu. + pub fn new>(manager: &M) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let menu = run_main_thread!(handle, || { + let menu = muda::Menu::new(); + MenuInner::new(app_handle, menu) + })?; + + Ok(Self(Arc::new(menu))) + } + + /// Creates a new menu with the specified id. + pub fn with_id, I: Into>(manager: &M, id: I) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let id = id.into(); + let menu = run_main_thread!(handle, move || { + let menu = muda::Menu::with_id(id); + MenuInner::new(app_handle, menu) + })?; + + Ok(Self(Arc::new(menu))) + } + + /// Creates a new menu with given `items`. It calls [`Menu::new`] and [`Menu::append_items`] internally. + pub fn with_items>( + manager: &M, + items: &[&dyn IsMenuItem], + ) -> crate::Result { + let menu = Self::new(manager)?; + menu.append_items(items)?; + Ok(menu) + } + + /// Creates a new menu with the specified id and given `items`. + /// It calls [`Menu::new`] and [`Menu::append_items`] internally. + pub fn with_id_and_items, I: Into>( + manager: &M, + id: I, + items: &[&dyn IsMenuItem], + ) -> crate::Result { + let menu = Self::with_id(manager, id)?; + menu.append_items(items)?; + Ok(menu) + } + + /// Creates a menu filled with default menu items and submenus. + pub fn default(app_handle: &AppHandle) -> crate::Result { + let pkg_info = app_handle.package_info(); + let config = app_handle.config(); + let about_metadata = AboutMetadata { + name: Some(pkg_info.name.clone()), + version: Some(pkg_info.version.to_string()), + copyright: config.bundle.copyright.clone(), + authors: config.bundle.publisher.clone().map(|p| vec![p]), + ..Default::default() + }; + + let window_menu = Submenu::with_id_and_items( + app_handle, + WINDOW_SUBMENU_ID, + "Window", + true, + &[ + &PredefinedMenuItem::minimize(app_handle, None)?, + &PredefinedMenuItem::maximize(app_handle, None)?, + #[cfg(target_os = "macos")] + &PredefinedMenuItem::separator(app_handle)?, + &PredefinedMenuItem::close_window(app_handle, None)?, + ], + )?; + + let help_menu = Submenu::with_id_and_items( + app_handle, + HELP_SUBMENU_ID, + "Help", + true, + &[ + #[cfg(not(target_os = "macos"))] + &PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?, + ], + )?; + + let menu = Menu::with_items( + app_handle, + &[ + #[cfg(target_os = "macos")] + &Submenu::with_items( + app_handle, + pkg_info.name.clone(), + true, + &[ + &PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?, + &PredefinedMenuItem::separator(app_handle)?, + &PredefinedMenuItem::services(app_handle, None)?, + &PredefinedMenuItem::separator(app_handle)?, + &PredefinedMenuItem::hide(app_handle, None)?, + &PredefinedMenuItem::hide_others(app_handle, None)?, + &PredefinedMenuItem::separator(app_handle)?, + &PredefinedMenuItem::quit(app_handle, None)?, + ], + )?, + #[cfg(not(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + )))] + &Submenu::with_items( + app_handle, + "File", + true, + &[ + &PredefinedMenuItem::close_window(app_handle, None)?, + #[cfg(not(target_os = "macos"))] + &PredefinedMenuItem::quit(app_handle, None)?, + ], + )?, + &Submenu::with_items( + app_handle, + "Edit", + true, + &[ + &PredefinedMenuItem::undo(app_handle, None)?, + &PredefinedMenuItem::redo(app_handle, None)?, + &PredefinedMenuItem::separator(app_handle)?, + &PredefinedMenuItem::cut(app_handle, None)?, + &PredefinedMenuItem::copy(app_handle, None)?, + &PredefinedMenuItem::paste(app_handle, None)?, + &PredefinedMenuItem::select_all(app_handle, None)?, + ], + )?, + #[cfg(target_os = "macos")] + &Submenu::with_items( + app_handle, + "View", + true, + &[&PredefinedMenuItem::fullscreen(app_handle, None)?], + )?, + &window_menu, + &help_menu, + ], + )?; + + Ok(menu) + } + + pub(crate) fn inner(&self) -> &muda::Menu { + (*self.0).as_ref() + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.0.app_handle + } + + /// Returns a unique identifier associated with this menu. + pub fn id(&self) -> &MenuId { + self.0.inner.id() + } + + /// Add a menu item to the end of this menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu. + /// + /// [`Submenu`]: super::Submenu + pub fn append(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().append(kind.inner().inner_muda()) + })? + .map_err(Into::into) + } + + /// Add menu items to the end of this menu. It calls [`Menu::append`] in a loop internally. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn append_items(&self, items: &[&dyn IsMenuItem]) -> crate::Result<()> { + for item in items { + self.append(*item)? + } + + Ok(()) + } + + /// Add a menu item to the beginning of this menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn prepend(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().prepend(kind.inner().inner_muda()) + })? + .map_err(Into::into) + } + + /// Add menu items to the beginning of this menu. It calls [`Menu::insert_items`] with position of `0` internally. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn prepend_items(&self, items: &[&dyn IsMenuItem]) -> crate::Result<()> { + self.insert_items(items, 0) + } + + /// Insert a menu item at the specified `position` in the menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn insert(&self, item: &dyn IsMenuItem, position: usize) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| (*self_.0) + .as_ref() + .insert(kind.inner().inner_muda(), position))? + .map_err(Into::into) + } + + /// Insert menu items at the specified `position` in the menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn insert_items(&self, items: &[&dyn IsMenuItem], position: usize) -> crate::Result<()> { + for (i, item) in items.iter().enumerate() { + self.insert(*item, position + i)? + } + + Ok(()) + } + + /// Remove a menu item from this menu. + pub fn remove(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().remove(kind.inner().inner_muda()) + })? + .map_err(Into::into) + } + + /// Remove the menu item at the specified position from this menu and returns it. + pub fn remove_at(&self, position: usize) -> crate::Result>> { + run_item_main_thread!(self, |self_: Self| { + (*self_.0) + .as_ref() + .remove_at(position) + .map(|i| MenuItemKind::from_muda(self_.0.app_handle.clone(), i)) + }) + } + + /// Retrieves the menu item matching the given identifier. + pub fn get<'a, I>(&self, id: &'a I) -> Option> + where + I: ?Sized, + MenuId: PartialEq<&'a I>, + { + self + .items() + .unwrap_or_default() + .into_iter() + .find(|i| i.id() == &id) + } + + /// Returns a list of menu items that has been added to this menu. + pub fn items(&self) -> crate::Result>> { + run_item_main_thread!(self, |self_: Self| { + (*self_.0) + .as_ref() + .items() + .into_iter() + .map(|i| MenuItemKind::from_muda(self_.0.app_handle.clone(), i)) + .collect::>() + }) + } + + /// Set this menu as the application menu. + /// + /// This is an alias for [`AppHandle::set_menu`]. + pub fn set_as_app_menu(&self) -> crate::Result>> { + self.0.app_handle.set_menu(self.clone()) + } + + /// Set this menu as the window menu. + /// + /// This is an alias for [`Window::set_menu`]. + pub fn set_as_window_menu(&self, window: &Window) -> crate::Result>> { + window.set_menu(self.clone()) + } +} diff --git a/crates/tauri/src/menu/mod.rs b/crates/tauri/src/menu/mod.rs new file mode 100644 index 0000000..ab43592 --- /dev/null +++ b/crates/tauri/src/menu/mod.rs @@ -0,0 +1,771 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Menu types and utilities. + +mod builders; +mod check; +mod icon; +#[allow(clippy::module_inception)] +mod menu; +mod normal; +pub(crate) mod plugin; +mod predefined; +mod submenu; +use std::{mem::ManuallyDrop, sync::Arc}; + +pub use builders::*; +pub use menu::{HELP_SUBMENU_ID, WINDOW_SUBMENU_ID}; +use serde::{Deserialize, Serialize}; + +use crate::menu::plugin::remove_menu_channel; +use crate::{AppHandle, Runtime, image::Image}; +pub use muda::MenuId; + +macro_rules! run_item_main_thread { + ($self:ident, $ex:expr) => {{ + use std::sync::mpsc::channel; + let (tx, rx) = channel(); + let self_ = $self.clone(); + let task = move || { + let f = $ex; + let _ = tx.send(f(self_)); + }; + $self + .app_handle() + .run_on_main_thread(task) + .and_then(|_| rx.recv().map_err(|_| crate::Error::FailedToReceiveMessage)) + }}; +} + +pub(crate) use run_item_main_thread; + +/// Describes a menu event emitted when a menu item is activated +#[derive(Debug, Clone, Serialize)] +pub struct MenuEvent { + /// Id of the menu item which triggered this event + pub id: MenuId, +} + +impl MenuEvent { + /// Returns the id of the menu item which triggered this event + pub fn id(&self) -> &MenuId { + &self.id + } +} + +impl From for MenuEvent { + fn from(value: muda::MenuEvent) -> Self { + Self { id: value.id } + } +} + +macro_rules! gen_wrappers { + ( + $( + $(#[$attr:meta])* + $type:ident($inner:ident$(, $kind:ident)?) + ),* + ) => { + $( + #[tauri_macros::default_runtime(crate::Wry, wry)] + pub(crate) struct $inner { + // This [`ManuallyDrop`] is used to [`ManuallyDrop::take`] in [`Self::drop`] to drop it on main thread + inner: ManuallyDrop<::muda::$type>, + app_handle: $crate::AppHandle, + } + + impl $inner { + fn new(app_handle: $crate::AppHandle, menu: ::muda::$type) -> Self { + Self { + inner: ManuallyDrop::new(menu), + app_handle, + } + } + } + + /// # Safety + /// + /// We make sure it always runs on the main thread. + unsafe impl Sync for $inner {} + unsafe impl Send for $inner {} + + impl $crate::Resource for $type {} + + impl Drop for $inner { + fn drop(&mut self) { + remove_menu_channel(&self.app_handle, self.inner.id()); + // SAFETY: we will not access `self.inner` after this + let inner = unsafe { ManuallyDrop::take(&mut self.inner) }; + // SAFETY: inner was created on main thread and is being dropped on main thread + let inner = $crate::UnsafeSend(inner); + let _ = self.app_handle.run_on_main_thread(move || { + drop(inner.take()); + }); + } + } + + impl AsRef<::muda::$type> for $inner { + fn as_ref(&self) -> &::muda::$type { + &self.inner + } + } + + $(#[$attr])* + pub struct $type(::std::sync::Arc<$inner>); + + impl Clone for $type { + fn clone(&self) -> Self { + Self(self.0.clone()) + } + } + + $( + impl $crate::menu::sealed::IsMenuItemBase for $type { + fn inner_muda(&self) -> &dyn muda::IsMenuItem { + (*self.0).as_ref() + } + } + + impl $crate::menu::IsMenuItem for $type { + fn kind(&self) -> MenuItemKind { + MenuItemKind::$kind(self.clone()) + } + + fn id(&self) -> &MenuId { + self.0.inner.id() + } + } + )* + )* + }; +} + +gen_wrappers!( + /// A type that is either a menu bar on the window + /// on Windows and Linux or as a global menu in the menubar on macOS. + /// + /// ## Platform-specific: + /// + /// - **macOS**: if using [`Menu`] for the global menubar, it can only contain [`Submenu`]s + Menu(MenuInner), + /// A menu item inside a [`Menu`] or [`Submenu`] and contains only text. + MenuItem(MenuItemInner, MenuItem), + /// A type that is a submenu inside a [`Menu`] or [`Submenu`] + Submenu(SubmenuInner, Submenu), + /// A predefined (native) menu item which has a predefined behavior by the OS or by this crate. + PredefinedMenuItem(PredefinedMenuItemInner, Predefined), + /// A menu item inside a [`Menu`] or [`Submenu`] + /// and usually contains a text and a check mark or a similar toggle + /// that corresponds to a checked and unchecked states. + CheckMenuItem(CheckMenuItemInner, Check), + /// A menu item inside a [`Menu`] or [`Submenu`] + /// and usually contains an icon and a text. + IconMenuItem(IconMenuItemInner, Icon) +); + +/// Application metadata for the [`PredefinedMenuItem::about`]. +#[derive(Debug, Clone, Default)] +pub struct AboutMetadata<'a> { + /// Sets the application name. + pub name: Option, + /// The application version. + pub version: Option, + /// The short version, e.g. "1.0". + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Appended to the end of `version` in parentheses. + pub short_version: Option, + /// The authors of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub authors: Option>, + /// Application comments. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub comments: Option, + /// The copyright of the application. + pub copyright: Option, + /// The license of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub license: Option, + /// The application website. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub website: Option, + /// The website label. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub website_label: Option, + /// The credits. + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Unsupported. + pub credits: Option, + /// The application icon. + /// + /// ## Platform-specific + /// + /// - **Windows:** Unsupported. + pub icon: Option>, +} + +/// A builder type for [`AboutMetadata`]. +#[derive(Clone, Debug, Default)] +pub struct AboutMetadataBuilder<'a>(AboutMetadata<'a>); + +impl<'a> AboutMetadataBuilder<'a> { + /// Create a new about metadata builder. + pub fn new() -> Self { + Default::default() + } + + /// Sets the application name. + pub fn name>(mut self, name: Option) -> Self { + self.0.name = name.map(|s| s.into()); + self + } + /// Sets the application version. + pub fn version>(mut self, version: Option) -> Self { + self.0.version = version.map(|s| s.into()); + self + } + /// Sets the short version, e.g. "1.0". + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Appended to the end of `version` in parentheses. + pub fn short_version>(mut self, short_version: Option) -> Self { + self.0.short_version = short_version.map(|s| s.into()); + self + } + /// Sets the authors of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn authors(mut self, authors: Option>) -> Self { + self.0.authors = authors; + self + } + /// Application comments. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn comments>(mut self, comments: Option) -> Self { + self.0.comments = comments.map(|s| s.into()); + self + } + /// Sets the copyright of the application. + pub fn copyright>(mut self, copyright: Option) -> Self { + self.0.copyright = copyright.map(|s| s.into()); + self + } + /// Sets the license of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn license>(mut self, license: Option) -> Self { + self.0.license = license.map(|s| s.into()); + self + } + /// Sets the application website. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn website>(mut self, website: Option) -> Self { + self.0.website = website.map(|s| s.into()); + self + } + /// Sets the website label. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn website_label>(mut self, website_label: Option) -> Self { + self.0.website_label = website_label.map(|s| s.into()); + self + } + /// Sets the credits. + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Unsupported. + pub fn credits>(mut self, credits: Option) -> Self { + self.0.credits = credits.map(|s| s.into()); + self + } + /// Sets the application icon. + /// + /// ## Platform-specific + /// + /// - **Windows:** Unsupported. + pub fn icon(mut self, icon: Option>) -> Self { + self.0.icon = icon; + self + } + + /// Construct the final [`AboutMetadata`] + pub fn build(self) -> AboutMetadata<'a> { + self.0 + } +} + +impl TryFrom> for muda::AboutMetadata { + type Error = crate::Error; + + fn try_from(value: AboutMetadata<'_>) -> Result { + let icon = match value.icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + + Ok(Self { + authors: value.authors, + name: value.name, + version: value.version, + short_version: value.short_version, + comments: value.comments, + copyright: value.copyright, + license: value.license, + website: value.website, + website_label: value.website_label, + credits: value.credits, + icon, + }) + } +} + +/// A native Icon to be used for the menu item +/// +/// ## Platform-specific: +/// +/// - **Windows / Linux**: Unsupported. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +pub enum NativeIcon { + /// An add item template image. + Add, + /// Advanced preferences toolbar icon for the preferences window. + Advanced, + /// A Bluetooth template image. + Bluetooth, + /// Bookmarks image suitable for a template. + Bookmarks, + /// A caution image. + Caution, + /// A color panel toolbar icon. + ColorPanel, + /// A column view mode template image. + ColumnView, + /// A computer icon. + Computer, + /// An enter full-screen mode template image. + EnterFullScreen, + /// Permissions for all users. + Everyone, + /// An exit full-screen mode template image. + ExitFullScreen, + /// A cover flow view mode template image. + FlowView, + /// A folder image. + Folder, + /// A burnable folder icon. + FolderBurnable, + /// A smart folder icon. + FolderSmart, + /// A link template image. + FollowLinkFreestanding, + /// A font panel toolbar icon. + FontPanel, + /// A `go back` template image. + GoLeft, + /// A `go forward` template image. + GoRight, + /// Home image suitable for a template. + Home, + /// An iChat Theater template image. + IChatTheater, + /// An icon view mode template image. + IconView, + /// An information toolbar icon. + Info, + /// A template image used to denote invalid data. + InvalidDataFreestanding, + /// A generic left-facing triangle template image. + LeftFacingTriangle, + /// A list view mode template image. + ListView, + /// A locked padlock template image. + LockLocked, + /// An unlocked padlock template image. + LockUnlocked, + /// A horizontal dash, for use in menus. + MenuMixedState, + /// A check mark template image, for use in menus. + MenuOnState, + /// A MobileMe icon. + MobileMe, + /// A drag image for multiple items. + MultipleDocuments, + /// A network icon. + Network, + /// A path button template image. + Path, + /// General preferences toolbar icon for the preferences window. + PreferencesGeneral, + /// A Quick Look template image. + QuickLook, + /// A refresh template image. + RefreshFreestanding, + /// A refresh template image. + Refresh, + /// A remove item template image. + Remove, + /// A reveal contents template image. + RevealFreestanding, + /// A generic right-facing triangle template image. + RightFacingTriangle, + /// A share view template image. + Share, + /// A slideshow template image. + Slideshow, + /// A badge for a `smart` item. + SmartBadge, + /// Small green indicator, similar to iChat's available image. + StatusAvailable, + /// Small clear indicator. + StatusNone, + /// Small yellow indicator, similar to iChat's idle image. + StatusPartiallyAvailable, + /// Small red indicator, similar to iChat's unavailable image. + StatusUnavailable, + /// A stop progress template image. + StopProgressFreestanding, + /// A stop progress button template image. + StopProgress, + /// An image of the empty trash can. + TrashEmpty, + /// An image of the full trash can. + TrashFull, + /// Permissions for a single user. + User, + /// User account toolbar icon for the preferences window. + UserAccounts, + /// Permissions for a group of users. + UserGroup, + /// Permissions for guests. + UserGuest, +} + +impl From for muda::NativeIcon { + fn from(value: NativeIcon) -> Self { + match value { + NativeIcon::Add => muda::NativeIcon::Add, + NativeIcon::Advanced => muda::NativeIcon::Advanced, + NativeIcon::Bluetooth => muda::NativeIcon::Bluetooth, + NativeIcon::Bookmarks => muda::NativeIcon::Bookmarks, + NativeIcon::Caution => muda::NativeIcon::Caution, + NativeIcon::ColorPanel => muda::NativeIcon::ColorPanel, + NativeIcon::ColumnView => muda::NativeIcon::ColumnView, + NativeIcon::Computer => muda::NativeIcon::Computer, + NativeIcon::EnterFullScreen => muda::NativeIcon::EnterFullScreen, + NativeIcon::Everyone => muda::NativeIcon::Everyone, + NativeIcon::ExitFullScreen => muda::NativeIcon::ExitFullScreen, + NativeIcon::FlowView => muda::NativeIcon::FlowView, + NativeIcon::Folder => muda::NativeIcon::Folder, + NativeIcon::FolderBurnable => muda::NativeIcon::FolderBurnable, + NativeIcon::FolderSmart => muda::NativeIcon::FolderSmart, + NativeIcon::FollowLinkFreestanding => muda::NativeIcon::FollowLinkFreestanding, + NativeIcon::FontPanel => muda::NativeIcon::FontPanel, + NativeIcon::GoLeft => muda::NativeIcon::GoLeft, + NativeIcon::GoRight => muda::NativeIcon::GoRight, + NativeIcon::Home => muda::NativeIcon::Home, + NativeIcon::IChatTheater => muda::NativeIcon::IChatTheater, + NativeIcon::IconView => muda::NativeIcon::IconView, + NativeIcon::Info => muda::NativeIcon::Info, + NativeIcon::InvalidDataFreestanding => muda::NativeIcon::InvalidDataFreestanding, + NativeIcon::LeftFacingTriangle => muda::NativeIcon::LeftFacingTriangle, + NativeIcon::ListView => muda::NativeIcon::ListView, + NativeIcon::LockLocked => muda::NativeIcon::LockLocked, + NativeIcon::LockUnlocked => muda::NativeIcon::LockUnlocked, + NativeIcon::MenuMixedState => muda::NativeIcon::MenuMixedState, + NativeIcon::MenuOnState => muda::NativeIcon::MenuOnState, + NativeIcon::MobileMe => muda::NativeIcon::MobileMe, + NativeIcon::MultipleDocuments => muda::NativeIcon::MultipleDocuments, + NativeIcon::Network => muda::NativeIcon::Network, + NativeIcon::Path => muda::NativeIcon::Path, + NativeIcon::PreferencesGeneral => muda::NativeIcon::PreferencesGeneral, + NativeIcon::QuickLook => muda::NativeIcon::QuickLook, + NativeIcon::RefreshFreestanding => muda::NativeIcon::RefreshFreestanding, + NativeIcon::Refresh => muda::NativeIcon::Refresh, + NativeIcon::Remove => muda::NativeIcon::Remove, + NativeIcon::RevealFreestanding => muda::NativeIcon::RevealFreestanding, + NativeIcon::RightFacingTriangle => muda::NativeIcon::RightFacingTriangle, + NativeIcon::Share => muda::NativeIcon::Share, + NativeIcon::Slideshow => muda::NativeIcon::Slideshow, + NativeIcon::SmartBadge => muda::NativeIcon::SmartBadge, + NativeIcon::StatusAvailable => muda::NativeIcon::StatusAvailable, + NativeIcon::StatusNone => muda::NativeIcon::StatusNone, + NativeIcon::StatusPartiallyAvailable => muda::NativeIcon::StatusPartiallyAvailable, + NativeIcon::StatusUnavailable => muda::NativeIcon::StatusUnavailable, + NativeIcon::StopProgressFreestanding => muda::NativeIcon::StopProgressFreestanding, + NativeIcon::StopProgress => muda::NativeIcon::StopProgress, + NativeIcon::TrashEmpty => muda::NativeIcon::TrashEmpty, + NativeIcon::TrashFull => muda::NativeIcon::TrashFull, + NativeIcon::User => muda::NativeIcon::User, + NativeIcon::UserAccounts => muda::NativeIcon::UserAccounts, + NativeIcon::UserGroup => muda::NativeIcon::UserGroup, + NativeIcon::UserGuest => muda::NativeIcon::UserGuest, + } + } +} + +/// An enumeration of all menu item kinds that could be added to +/// a [`Menu`] or [`Submenu`] +pub enum MenuItemKind { + /// Normal menu item + MenuItem(MenuItem), + /// Submenu menu item + Submenu(Submenu), + /// Predefined menu item + Predefined(PredefinedMenuItem), + /// Check menu item + Check(CheckMenuItem), + /// Icon menu item + Icon(IconMenuItem), +} + +impl MenuItemKind { + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + match self { + MenuItemKind::MenuItem(i) => i.id(), + MenuItemKind::Submenu(i) => i.id(), + MenuItemKind::Predefined(i) => i.id(), + MenuItemKind::Check(i) => i.id(), + MenuItemKind::Icon(i) => i.id(), + } + } + + pub(crate) fn inner(&self) -> &dyn IsMenuItem { + match self { + MenuItemKind::MenuItem(i) => i, + MenuItemKind::Submenu(i) => i, + MenuItemKind::Predefined(i) => i, + MenuItemKind::Check(i) => i, + MenuItemKind::Icon(i) => i, + } + } + + pub(crate) fn from_muda(app_handle: AppHandle, i: muda::MenuItemKind) -> Self { + match i { + muda::MenuItemKind::MenuItem(i) => { + Self::MenuItem(MenuItem(Arc::new(MenuItemInner::new(app_handle, i)))) + } + muda::MenuItemKind::Submenu(i) => { + Self::Submenu(Submenu(Arc::new(SubmenuInner::new(app_handle, i)))) + } + muda::MenuItemKind::Predefined(i) => Self::Predefined(PredefinedMenuItem(Arc::new( + PredefinedMenuItemInner::new(app_handle, i), + ))), + muda::MenuItemKind::Check(i) => Self::Check(CheckMenuItem(Arc::new( + CheckMenuItemInner::new(app_handle, i), + ))), + muda::MenuItemKind::Icon(i) => Self::Icon(IconMenuItem(Arc::new(IconMenuItemInner::new( + app_handle, i, + )))), + } + } + + /// Casts this item to a [`MenuItem`], and returns `None` if it wasn't. + pub fn as_menuitem(&self) -> Option<&MenuItem> { + match self { + MenuItemKind::MenuItem(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`MenuItem`], and panics if it wasn't. + pub fn as_menuitem_unchecked(&self) -> &MenuItem { + match self { + MenuItemKind::MenuItem(i) => i, + _ => panic!("Not a MenuItem"), + } + } + + /// Casts this item to a [`Submenu`], and returns `None` if it wasn't. + pub fn as_submenu(&self) -> Option<&Submenu> { + match self { + MenuItemKind::Submenu(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`Submenu`], and panics if it wasn't. + pub fn as_submenu_unchecked(&self) -> &Submenu { + match self { + MenuItemKind::Submenu(i) => i, + _ => panic!("Not a Submenu"), + } + } + + /// Casts this item to a [`PredefinedMenuItem`], and returns `None` if it wasn't. + pub fn as_predefined_menuitem(&self) -> Option<&PredefinedMenuItem> { + match self { + MenuItemKind::Predefined(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`PredefinedMenuItem`], and panics if it wasn't. + pub fn as_predefined_menuitem_unchecked(&self) -> &PredefinedMenuItem { + match self { + MenuItemKind::Predefined(i) => i, + _ => panic!("Not a PredefinedMenuItem"), + } + } + + /// Casts this item to a [`CheckMenuItem`], and returns `None` if it wasn't. + pub fn as_check_menuitem(&self) -> Option<&CheckMenuItem> { + match self { + MenuItemKind::Check(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`CheckMenuItem`], and panics if it wasn't. + pub fn as_check_menuitem_unchecked(&self) -> &CheckMenuItem { + match self { + MenuItemKind::Check(i) => i, + _ => panic!("Not a CheckMenuItem"), + } + } + + /// Casts this item to a [`IconMenuItem`], and returns `None` if it wasn't. + pub fn as_icon_menuitem(&self) -> Option<&IconMenuItem> { + match self { + MenuItemKind::Icon(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`IconMenuItem`], and panics if it wasn't. + pub fn as_icon_menuitem_unchecked(&self) -> &IconMenuItem { + match self { + MenuItemKind::Icon(i) => i, + _ => panic!("Not an IconMenuItem"), + } + } +} + +impl Clone for MenuItemKind { + fn clone(&self) -> Self { + match self { + Self::MenuItem(i) => Self::MenuItem(i.clone()), + Self::Submenu(i) => Self::Submenu(i.clone()), + Self::Predefined(i) => Self::Predefined(i.clone()), + Self::Check(i) => Self::Check(i.clone()), + Self::Icon(i) => Self::Icon(i.clone()), + } + } +} + +impl sealed::IsMenuItemBase for MenuItemKind { + fn inner_muda(&self) -> &dyn muda::IsMenuItem { + self.inner().inner_muda() + } +} + +impl IsMenuItem for MenuItemKind { + fn kind(&self) -> MenuItemKind { + self.clone() + } + + fn id(&self) -> &MenuId { + self.id() + } +} + +/// A trait that defines a generic item in a menu, which may be one of [`MenuItemKind`] +/// +/// # Safety +/// +/// This trait is ONLY meant to be implemented internally by the crate. +pub trait IsMenuItem: sealed::IsMenuItemBase { + /// Returns the kind of this menu item. + fn kind(&self) -> MenuItemKind; + + /// Returns a unique identifier associated with this menu. + fn id(&self) -> &MenuId; +} + +/// A helper trait with methods to help creating a context menu. +/// +/// # Safety +/// +/// This trait is ONLY meant to be implemented internally by the crate. +pub trait ContextMenu: sealed::ContextMenuBase + Send + Sync { + /// Get the popup [`HMENU`] for this menu. + /// + /// The returned [`HMENU`] is valid as long as the [`ContextMenu`] is. + /// + /// [`HMENU`]: https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types#HMENU + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + fn hpopupmenu(&self) -> crate::Result; + + /// Popup this menu as a context menu on the specified window at the cursor position. + fn popup(&self, window: crate::Window) -> crate::Result<()>; + + /// Popup this menu as a context menu on the specified window at the specified position. + /// + /// The position is relative to the window's top-left corner. + fn popup_at>( + &self, + window: crate::Window, + position: P, + ) -> crate::Result<()>; +} + +pub(crate) mod sealed { + + pub trait IsMenuItemBase { + fn inner_muda(&self) -> &dyn muda::IsMenuItem; + } + + pub trait ContextMenuBase { + fn inner_context(&self) -> &dyn muda::ContextMenu; + fn inner_context_owned(&self) -> Box; + fn popup_inner>( + &self, + window: crate::Window, + position: Option

, + ) -> crate::Result<()>; + } +} + +#[cfg(windows)] +pub(crate) fn map_to_menu_theme(theme: tauri_utils::Theme) -> muda::MenuTheme { + match theme { + tauri_utils::Theme::Light => muda::MenuTheme::Light, + tauri_utils::Theme::Dark => muda::MenuTheme::Dark, + _ => muda::MenuTheme::Auto, + } +} diff --git a/crates/tauri/src/menu/normal.rs b/crates/tauri/src/menu/normal.rs new file mode 100644 index 0000000..c6aa015 --- /dev/null +++ b/crates/tauri/src/menu/normal.rs @@ -0,0 +1,117 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use super::run_item_main_thread; +use crate::menu::MenuItemInner; +use crate::run_main_thread; +use crate::{AppHandle, Manager, Runtime, menu::MenuId}; + +use super::MenuItem; + +impl MenuItem { + /// Create a new menu item. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new( + manager: &M, + text: T, + enabled: bool, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.as_ref().to_owned(); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + + let item = run_main_thread!(handle, || { + let item = muda::MenuItem::new(text, enabled, accelerator); + MenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Create a new menu item with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id( + manager: &M, + id: I, + text: T, + enabled: bool, + accelerator: Option, + ) -> crate::Result + where + M: Manager, + I: Into, + T: AsRef, + A: AsRef, + { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let id = id.into(); + let accelerator = accelerator.and_then(|s| s.as_ref().parse().ok()); + let text = text.as_ref().to_owned(); + + let item = run_main_thread!(handle, move || { + let item = muda::MenuItem::with_id(id, text, enabled, accelerator); + MenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.0.app_handle + } + + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + self.0.inner.id() + } + + /// Get the text for this menu item. + pub fn text(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text()) + } + + /// Set the text for this menu item. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text)) + } + + /// Get whether this menu item is enabled or not. + pub fn is_enabled(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled()) + } + + /// Enable or disable this menu item. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled)) + } + + /// Set this menu item accelerator. + pub fn set_accelerator>(&self, accelerator: Option) -> crate::Result<()> { + let accel = accelerator.and_then(|s| s.as_ref().parse().ok()); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().set_accelerator(accel) + })? + .map_err(Into::into) + } +} diff --git a/crates/tauri/src/menu/plugin.rs b/crates/tauri/src/menu/plugin.rs new file mode 100644 index 0000000..0e89b6a --- /dev/null +++ b/crates/tauri/src/menu/plugin.rs @@ -0,0 +1,941 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{collections::HashMap, sync::Mutex}; + +use serde::{Deserialize, Serialize}; +use tauri_runtime::dpi::Position; + +use super::{sealed::ContextMenuBase, *}; +use crate::{ + Manager, ResourceTable, RunEvent, Runtime, State, Webview, Window, command, + image::JsImage, + ipc::{Channel, channel::JavaScriptChannelId}, + plugin::{Builder, TauriPlugin}, + resources::ResourceId, + sealed::ManagerBase, +}; +use tauri_macros::do_menu_item; + +#[derive(Deserialize, Serialize)] +pub(crate) enum ItemKind { + Menu, + MenuItem, + Predefined, + Submenu, + Check, + Icon, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AboutMetadata { + pub name: Option, + pub version: Option, + pub short_version: Option, + pub authors: Option>, + pub comments: Option, + pub copyright: Option, + pub license: Option, + pub website: Option, + pub website_label: Option, + pub credits: Option, + pub icon: Option, +} + +impl AboutMetadata { + pub fn into_metadata( + self, + resources_table: &ResourceTable, + ) -> crate::Result> { + let icon = match self.icon { + Some(i) => Some(i.into_img(resources_table)?.as_ref().clone()), + None => None, + }; + + Ok(super::AboutMetadata { + name: self.name, + version: self.version, + short_version: self.short_version, + authors: self.authors, + comments: self.comments, + copyright: self.copyright, + license: self.license, + website: self.website, + website_label: self.website_label, + credits: self.credits, + icon, + }) + } +} + +#[allow(clippy::large_enum_variant)] +#[derive(Deserialize)] +enum Predefined { + Separator, + Copy, + Cut, + Paste, + SelectAll, + Undo, + Redo, + Minimize, + Maximize, + Fullscreen, + Hide, + HideOthers, + ShowAll, + CloseWindow, + Quit, + About(Option), + Services, + BringAllToFront, +} + +#[derive(Deserialize)] +struct SubmenuPayload { + id: Option, + text: String, + enabled: Option, + items: Vec, + icon: Option, +} + +impl SubmenuPayload { + pub fn create_item( + self, + webview: &Webview, + resources_table: &ResourceTable, + ) -> crate::Result> { + let mut builder = if let Some(id) = self.id { + SubmenuBuilder::with_id(webview, id, self.text) + } else { + SubmenuBuilder::new(webview, self.text) + }; + if let Some(enabled) = self.enabled { + builder = builder.enabled(enabled); + } + if let Some(icon) = self.icon { + builder = match icon { + Icon::Native(native_icon) => builder.submenu_native_icon(native_icon), + Icon::Icon(js_icon) => { + builder.submenu_icon(js_icon.into_img(resources_table)?.as_ref().clone()) + } + }; + } + for item in self.items { + builder = item.with_item(webview, resources_table, |i| Ok(builder.item(i)))?; + } + + builder.build() + } +} + +#[derive(Deserialize)] +struct CheckMenuItemPayload { + handler: Option, + id: Option, + text: String, + checked: bool, + enabled: Option, + accelerator: Option, +} + +impl CheckMenuItemPayload { + pub fn create_item(self, webview: &Webview) -> crate::Result> { + let mut builder = if let Some(id) = self.id { + CheckMenuItemBuilder::with_id(id, self.text) + } else { + CheckMenuItemBuilder::new(self.text) + }; + if let Some(accelerator) = self.accelerator { + builder = builder.accelerator(accelerator); + } + if let Some(enabled) = self.enabled { + builder = builder.enabled(enabled); + } + + let item = builder.checked(self.checked).build(webview)?; + + if let Some(handler) = self.handler { + let handler = handler.channel_on(webview.clone()); + webview + .state::() + .0 + .lock() + .unwrap() + .insert(item.id().clone(), handler); + } + + Ok(item) + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum Icon { + Native(NativeIcon), + Icon(JsImage), +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct IconMenuItemPayload { + handler: Option, + id: Option, + text: String, + icon: Icon, + enabled: Option, + accelerator: Option, +} + +impl IconMenuItemPayload { + pub fn create_item( + self, + webview: &Webview, + resources_table: &ResourceTable, + ) -> crate::Result> { + let mut builder = if let Some(id) = self.id { + IconMenuItemBuilder::with_id(id, self.text) + } else { + IconMenuItemBuilder::new(self.text) + }; + if let Some(accelerator) = self.accelerator { + builder = builder.accelerator(accelerator); + } + if let Some(enabled) = self.enabled { + builder = builder.enabled(enabled); + } + builder = match self.icon { + Icon::Native(native_icon) => builder.native_icon(native_icon), + Icon::Icon(icon) => builder.icon(icon.into_img(resources_table)?.as_ref().clone()), + }; + + let item = builder.build(webview)?; + + if let Some(handler) = self.handler { + let handler = handler.channel_on(webview.clone()); + webview + .state::() + .0 + .lock() + .unwrap() + .insert(item.id().clone(), handler); + } + + Ok(item) + } +} + +#[derive(Deserialize)] +struct MenuItemPayload { + handler: Option, + id: Option, + text: String, + enabled: Option, + accelerator: Option, +} + +impl MenuItemPayload { + pub fn create_item(self, webview: &Webview) -> crate::Result> { + let mut builder = if let Some(id) = self.id { + MenuItemBuilder::with_id(id, self.text) + } else { + MenuItemBuilder::new(self.text) + }; + if let Some(accelerator) = self.accelerator { + builder = builder.accelerator(accelerator); + } + if let Some(enabled) = self.enabled { + builder = builder.enabled(enabled); + } + + let item = builder.build(webview)?; + + if let Some(handler) = self.handler { + let handler = handler.channel_on(webview.clone()); + webview + .state::() + .0 + .lock() + .unwrap() + .insert(item.id().clone(), handler); + } + + Ok(item) + } +} + +#[derive(Deserialize)] +struct PredefinedMenuItemPayload { + item: Predefined, + text: Option, +} + +impl PredefinedMenuItemPayload { + pub fn create_item( + self, + webview: &Webview, + resources_table: &ResourceTable, + ) -> crate::Result> { + match self.item { + Predefined::Separator => PredefinedMenuItem::separator(webview), + Predefined::Copy => PredefinedMenuItem::copy(webview, self.text.as_deref()), + Predefined::Cut => PredefinedMenuItem::cut(webview, self.text.as_deref()), + Predefined::Paste => PredefinedMenuItem::paste(webview, self.text.as_deref()), + Predefined::SelectAll => PredefinedMenuItem::select_all(webview, self.text.as_deref()), + Predefined::Undo => PredefinedMenuItem::undo(webview, self.text.as_deref()), + Predefined::Redo => PredefinedMenuItem::redo(webview, self.text.as_deref()), + Predefined::Minimize => PredefinedMenuItem::minimize(webview, self.text.as_deref()), + Predefined::Maximize => PredefinedMenuItem::maximize(webview, self.text.as_deref()), + Predefined::Fullscreen => PredefinedMenuItem::fullscreen(webview, self.text.as_deref()), + Predefined::Hide => PredefinedMenuItem::hide(webview, self.text.as_deref()), + Predefined::HideOthers => PredefinedMenuItem::hide_others(webview, self.text.as_deref()), + Predefined::ShowAll => PredefinedMenuItem::show_all(webview, self.text.as_deref()), + Predefined::CloseWindow => PredefinedMenuItem::close_window(webview, self.text.as_deref()), + Predefined::Quit => PredefinedMenuItem::quit(webview, self.text.as_deref()), + Predefined::About(metadata) => { + let metadata = match metadata { + Some(m) => Some(m.into_metadata(resources_table)?), + None => None, + }; + 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()) + } + } + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +// Note, order matters for untagged enum deserialization +enum MenuItemPayloadKind { + ExistingItem((ResourceId, ItemKind)), + Predefined(PredefinedMenuItemPayload), + Check(CheckMenuItemPayload), + Icon(IconMenuItemPayload), + Submenu(SubmenuPayload), + MenuItem(MenuItemPayload), +} + +impl MenuItemPayloadKind { + pub fn with_item) -> crate::Result>( + self, + webview: &Webview, + resources_table: &ResourceTable, + f: F, + ) -> crate::Result { + match self { + Self::ExistingItem((rid, kind)) => { + do_menu_item!(resources_table, rid, kind, |i| f(&*i)) + } + Self::Submenu(i) => f(&i.create_item(webview, resources_table)?), + Self::Predefined(i) => f(&i.create_item(webview, resources_table)?), + Self::Check(i) => f(&i.create_item(webview)?), + Self::Icon(i) => f(&i.create_item(webview, resources_table)?), + Self::MenuItem(i) => f(&i.create_item(webview)?), + } + } +} + +#[derive(Deserialize, Default)] +#[serde(rename_all = "camelCase")] +struct NewOptions { + id: Option, + text: Option, + enabled: Option, + checked: Option, + accelerator: Option, + #[serde(rename = "item")] + predefined_item: Option, + icon: Option, + items: Option>, +} + +#[command(root = "crate")] +fn new( + webview: Webview, + kind: ItemKind, + options: Option, + channels: State<'_, MenuChannels>, + handler: Channel, +) -> crate::Result<(ResourceId, MenuId)> { + let options = options.unwrap_or_default(); + let mut resources_table = webview.resources_table(); + + let (rid, id) = match kind { + ItemKind::Menu => { + let mut builder = MenuBuilder::new(&webview); + if let Some(id) = options.id { + builder = builder.id(id); + } + if let Some(items) = options.items { + for item in items { + builder = item.with_item(&webview, &resources_table, |i| Ok(builder.item(i)))?; + } + } + let menu = builder.build()?; + let id = menu.id().clone(); + let rid = resources_table.add(menu); + + (rid, id) + } + + ItemKind::Submenu => { + let submenu = SubmenuPayload { + id: options.id, + text: options.text.unwrap_or_default(), + enabled: options.enabled, + items: options.items.unwrap_or_default(), + icon: options.icon, + } + .create_item(&webview, &resources_table)?; + let id = submenu.id().clone(); + let rid = resources_table.add(submenu); + + (rid, id) + } + + ItemKind::MenuItem => { + let item = MenuItemPayload { + // handler managed in this function instead + handler: None, + id: options.id, + text: options.text.unwrap_or_default(), + enabled: options.enabled, + accelerator: options.accelerator, + } + .create_item(&webview)?; + let id = item.id().clone(); + let rid = resources_table.add(item); + (rid, id) + } + + ItemKind::Predefined => { + let item = PredefinedMenuItemPayload { + item: options.predefined_item.unwrap(), + text: options.text, + } + .create_item(&webview, &resources_table)?; + let id = item.id().clone(); + let rid = resources_table.add(item); + (rid, id) + } + + ItemKind::Check => { + let item = CheckMenuItemPayload { + // handler managed in this function instead + handler: None, + id: options.id, + text: options.text.unwrap_or_default(), + checked: options.checked.unwrap_or_default(), + enabled: options.enabled, + accelerator: options.accelerator, + } + .create_item(&webview)?; + let id = item.id().clone(); + let rid = resources_table.add(item); + (rid, id) + } + + ItemKind::Icon => { + let item = IconMenuItemPayload { + // handler managed in this function instead + handler: None, + id: options.id, + text: options.text.unwrap_or_default(), + icon: options.icon.unwrap_or(Icon::Native(NativeIcon::User)), + enabled: options.enabled, + accelerator: options.accelerator, + } + .create_item(&webview, &resources_table)?; + let id = item.id().clone(); + let rid = resources_table.add(item); + (rid, id) + } + }; + + channels.0.lock().unwrap().insert(id.clone(), handler); + + Ok((rid, id)) +} + +#[command(root = "crate")] +fn append( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + items: Vec, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + for item in items { + item.with_item(&webview, &resources_table, |i| menu.append(i))?; + } + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + for item in items { + item.with_item(&webview, &resources_table, |i| submenu.append(i))?; + } + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + + Ok(()) +} + +#[command(root = "crate")] +fn prepend( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + items: Vec, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + for item in items { + item.with_item(&webview, &resources_table, |i| menu.prepend(i))?; + } + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + for item in items { + item.with_item(&webview, &resources_table, |i| submenu.prepend(i))?; + } + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + + Ok(()) +} + +#[command(root = "crate")] +fn insert( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + items: Vec, + mut position: usize, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + for item in items { + item.with_item(&webview, &resources_table, |i| menu.insert(i, position))?; + position += 1 + } + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + for item in items { + item.with_item(&webview, &resources_table, |i| submenu.insert(i, position))?; + position += 1 + } + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + + Ok(()) +} + +#[command(root = "crate")] +fn remove( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + item: (ResourceId, ItemKind), +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + let (item_rid, item_kind) = item; + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + do_menu_item!(resources_table, item_rid, item_kind, |i| menu.remove(&*i))?; + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + do_menu_item!(resources_table, item_rid, item_kind, |i| submenu + .remove(&*i))?; + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + + Ok(()) +} + +macro_rules! make_item_resource { + ($resources_table:ident, $item:ident) => {{ + let id = $item.id().clone(); + let (rid, kind) = match $item { + MenuItemKind::MenuItem(i) => ($resources_table.add(i), ItemKind::MenuItem), + MenuItemKind::Submenu(i) => ($resources_table.add(i), ItemKind::Submenu), + MenuItemKind::Predefined(i) => ($resources_table.add(i), ItemKind::Predefined), + MenuItemKind::Check(i) => ($resources_table.add(i), ItemKind::Check), + MenuItemKind::Icon(i) => ($resources_table.add(i), ItemKind::Icon), + }; + (rid, id, kind) + }}; +} + +#[command(root = "crate")] +fn remove_at( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + position: usize, +) -> crate::Result> { + let mut resources_table = webview.resources_table(); + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + if let Some(item) = menu.remove_at(position)? { + return Ok(Some(make_item_resource!(resources_table, item))); + } + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + if let Some(item) = submenu.remove_at(position)? { + return Ok(Some(make_item_resource!(resources_table, item))); + } + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + + Ok(None) +} + +#[command(root = "crate")] +fn items( + webview: Webview, + rid: ResourceId, + kind: ItemKind, +) -> crate::Result> { + let mut resources_table = webview.resources_table(); + let items = match kind { + ItemKind::Menu => resources_table.get::>(rid)?.items()?, + ItemKind::Submenu => resources_table.get::>(rid)?.items()?, + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + + Ok( + items + .into_iter() + .map(|i| make_item_resource!(resources_table, i)) + .collect::>(), + ) +} + +#[command(root = "crate")] +fn get( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + id: MenuId, +) -> crate::Result> { + let mut resources_table = webview.resources_table(); + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + if let Some(item) = menu.get(&id) { + return Ok(Some(make_item_resource!(resources_table, item))); + } + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + if let Some(item) = submenu.get(&id) { + return Ok(Some(make_item_resource!(resources_table, item))); + } + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + + Ok(None) +} + +#[command(root = "crate")] +async fn popup( + webview: Webview, + current_window: Window, + rid: ResourceId, + kind: ItemKind, + window: Option, + at: Option, +) -> crate::Result<()> { + let window = window + .map(|w| webview.manager().get_window(&w)) + .unwrap_or(Some(current_window)); + + if let Some(window) = window { + let resources_table = webview.resources_table(); + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + menu.popup_inner(window, at)?; + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + submenu.popup_inner(window, at)?; + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + } + + Ok(()) +} + +#[command(root = "crate")] +fn create_default( + app: AppHandle, + webview: Webview, +) -> crate::Result<(ResourceId, MenuId)> { + let mut resources_table = webview.resources_table(); + let menu = Menu::default(&app)?; + let id = menu.id().clone(); + let rid = resources_table.add(menu); + Ok((rid, id)) +} + +#[command(root = "crate")] +async fn set_as_app_menu( + webview: Webview, + rid: ResourceId, +) -> crate::Result> { + let mut resources_table = webview.resources_table(); + let menu = resources_table.get::>(rid)?; + if let Some(menu) = menu.set_as_app_menu()? { + let id = menu.id().clone(); + let rid = resources_table.add(menu); + return Ok(Some((rid, id))); + } + Ok(None) +} + +#[command(root = "crate")] +async fn set_as_window_menu( + webview: Webview, + current_window: Window, + rid: ResourceId, + window: Option, +) -> crate::Result> { + let window = window + .map(|w| webview.manager().get_window(&w)) + .unwrap_or(Some(current_window)); + + if let Some(window) = window { + let mut resources_table = webview.resources_table(); + let menu = resources_table.get::>(rid)?; + if let Some(menu) = menu.set_as_window_menu(&window)? { + let id = menu.id().clone(); + let rid = resources_table.add(menu); + return Ok(Some((rid, id))); + } + } + Ok(None) +} + +#[command(root = "crate")] +fn text(webview: Webview, rid: ResourceId, kind: ItemKind) -> crate::Result { + let resources_table = webview.resources_table(); + do_menu_item!(resources_table, rid, kind, |i| i.text()) +} + +#[command(root = "crate")] +fn set_text( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + text: String, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + do_menu_item!(resources_table, rid, kind, |i| i.set_text(text)) +} + +#[command(root = "crate")] +fn is_enabled( + webview: Webview, + rid: ResourceId, + kind: ItemKind, +) -> crate::Result { + let resources_table = webview.resources_table(); + do_menu_item!(resources_table, rid, kind, |i| i.is_enabled(), !Predefined) +} + +#[command(root = "crate")] +fn set_enabled( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + enabled: bool, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + do_menu_item!( + resources_table, + rid, + kind, + |i| i.set_enabled(enabled), + !Predefined + ) +} + +#[command(root = "crate")] +fn set_accelerator( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + accelerator: Option, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + do_menu_item!( + resources_table, + rid, + kind, + |i| i.set_accelerator(accelerator), + !Predefined | !Submenu + ) +} + +#[command(root = "crate")] +fn set_as_windows_menu_for_nsapp( + webview: Webview, + rid: ResourceId, +) -> crate::Result<()> { + #[cfg(target_os = "macos")] + { + let resources_table = webview.resources_table(); + let submenu = resources_table.get::>(rid)?; + submenu.set_as_windows_menu_for_nsapp()?; + } + + let _ = rid; + let _ = webview; + Ok(()) +} + +#[command(root = "crate")] +fn set_as_help_menu_for_nsapp( + webview: Webview, + rid: ResourceId, +) -> crate::Result<()> { + #[cfg(target_os = "macos")] + { + let resources_table = webview.resources_table(); + let submenu = resources_table.get::>(rid)?; + submenu.set_as_help_menu_for_nsapp()?; + } + + let _ = rid; + let _ = webview; + + Ok(()) +} + +#[command(root = "crate")] +fn is_checked(webview: Webview, rid: ResourceId) -> crate::Result { + let resources_table = webview.resources_table(); + let check_item = resources_table.get::>(rid)?; + check_item.is_checked() +} + +#[command(root = "crate")] +fn set_checked( + webview: Webview, + rid: ResourceId, + checked: bool, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + let check_item = resources_table.get::>(rid)?; + check_item.set_checked(checked) +} + +#[command(root = "crate")] +fn set_icon( + webview: Webview, + rid: ResourceId, + kind: ItemKind, + icon: Option, +) -> crate::Result<()> { + let resources_table = webview.resources_table(); + do_menu_item!( + resources_table, + rid, + kind, + |icon_item| match icon { + Some(Icon::Native(icon)) => icon_item.set_native_icon(Some(icon)), + Some(Icon::Icon(icon)) => { + icon_item.set_icon(Some(icon.into_img(&resources_table)?.as_ref().clone())) + } + None => { + icon_item.set_icon(None)?; + icon_item.set_native_icon(None)?; + Ok(()) + } + }, + Icon | Submenu + ) +} + +struct MenuChannels(Mutex>>); + +// Called in `Menu`'s `Drop` to clean up the event handlers +pub(crate) fn remove_menu_channel(app: &AppHandle, id: &MenuId) { + app.state::().0.lock().unwrap().remove(id); +} + +pub(crate) fn init() -> TauriPlugin { + Builder::new("menu") + .setup(|app, _api| { + app.manage(MenuChannels(Mutex::default())); + Ok(()) + }) + .on_event(|app, e| { + if let RunEvent::MenuEvent(e) = e { + // Cloning the channel out in case the menu gets dropped during the channel send through `channel_interceptor` + let channel = app + .state::() + .0 + .lock() + .unwrap() + .get(&e.id) + .cloned(); + if let Some(channel) = channel { + let _ = channel.send(e.id.clone()); + } + } + }) + .invoke_handler(crate::generate_handler![ + #![plugin(menu)] + new, + append, + prepend, + insert, + remove, + remove_at, + items, + get, + popup, + create_default, + set_as_app_menu, + set_as_window_menu, + text, + set_text, + is_enabled, + set_enabled, + set_accelerator, + set_as_windows_menu_for_nsapp, + set_as_help_menu_for_nsapp, + is_checked, + set_checked, + set_icon, + ]) + .build() +} diff --git a/crates/tauri/src/menu/predefined.rs b/crates/tauri/src/menu/predefined.rs new file mode 100644 index 0000000..059183b --- /dev/null +++ b/crates/tauri/src/menu/predefined.rs @@ -0,0 +1,360 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use super::run_item_main_thread; +use super::{AboutMetadata, PredefinedMenuItem}; +use crate::menu::PredefinedMenuItemInner; +use crate::run_main_thread; +use crate::{AppHandle, Manager, Runtime, menu::MenuId}; + +impl PredefinedMenuItem { + /// Separator menu item + pub fn separator>(manager: &M) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::separator(); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Copy menu item + pub fn copy>(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::copy(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Cut menu item + pub fn cut>(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::cut(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Paste menu item + pub fn paste>(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::paste(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// SelectAll menu item + pub fn select_all>(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::select_all(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Undo menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn undo>(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::undo(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + /// Redo menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn redo>(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::redo(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Minimize window menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn minimize>(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::minimize(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Maximize window menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn maximize>(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::maximize(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Fullscreen menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn fullscreen>(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::fullscreen(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Hide window menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide>(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::hide(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Hide other windows menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide_others>(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::hide_others(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Show all app windows menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn show_all>(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::show_all(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Close window menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn close_window>(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::close_window(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Quit app menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn quit>(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::quit(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// About app menu item + pub fn about>( + manager: &M, + text: Option<&str>, + metadata: Option>, + ) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.map(|t| t.to_owned()); + + let metadata = match metadata { + Some(m) => Some(m.try_into()?), + None => None, + }; + + let item = run_main_thread!(handle, || { + let item = muda::PredefinedMenuItem::about(text.as_deref(), metadata); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Services menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn services>(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::services(text.as_deref()); + PredefinedMenuItemInner::new(app_handle, item) + })?; + + 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::new(app_handle, item) + })?; + + Ok(Self(Arc::new(item))) + } + + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + self.0.inner.id() + } + + /// Get the text for this menu item. + pub fn text(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text()) + } + + /// Set the text for this menu item. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text)) + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.0.app_handle + } +} diff --git a/crates/tauri/src/menu/submenu.rs b/crates/tauri/src/menu/submenu.rs new file mode 100644 index 0000000..e36f255 --- /dev/null +++ b/crates/tauri/src/menu/submenu.rs @@ -0,0 +1,419 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use super::Submenu; +use super::run_item_main_thread; +use super::{IsMenuItem, MenuItemKind, sealed::ContextMenuBase}; +use crate::menu::NativeIcon; +use crate::menu::SubmenuInner; +use crate::run_main_thread; +use crate::{AppHandle, Manager, Position, Runtime, Window}; +use muda::{ContextMenu, Icon as MudaIcon, MenuId}; + +impl super::ContextMenu for Submenu { + #[cfg(target_os = "windows")] + fn hpopupmenu(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().hpopupmenu()) + } + + fn popup(&self, window: Window) -> crate::Result<()> { + self.popup_inner(window, None::) + } + + fn popup_at>( + &self, + window: Window, + position: P, + ) -> crate::Result<()> { + self.popup_inner(window, Some(position)) + } +} + +impl ContextMenuBase for Submenu { + fn popup_inner>( + &self, + window: crate::Window, + position: Option

, + ) -> crate::Result<()> { + let position = position.map(Into::into); + run_item_main_thread!(self, move |self_: Self| { + #[cfg(target_os = "macos")] + if let Ok(view) = window.ns_view() { + unsafe { + self_ + .inner() + .show_context_menu_for_nsview(view as _, position); + } + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(w) = window.gtk_window() { + self_ + .inner() + .show_context_menu_for_gtk_window(w.as_ref(), position); + } + + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + unsafe { + self_ + .inner() + .show_context_menu_for_hwnd(hwnd.0 as _, position); + } + } + }) + } + + fn inner_context(&self) -> &dyn muda::ContextMenu { + (*self.0).as_ref() + } + + fn inner_context_owned(&self) -> Box { + Box::new((*self.0).as_ref().clone()) + } +} + +impl Submenu { + /// Creates a new submenu. + pub fn new, S: AsRef>( + manager: &M, + text: S, + enabled: bool, + ) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let text = text.as_ref().to_owned(); + + let submenu = run_main_thread!(handle, || { + let submenu = muda::Submenu::new(text, enabled); + SubmenuInner::new(app_handle, submenu) + })?; + + Ok(Self(Arc::new(submenu))) + } + + /// Create a new submenu with an icon. + pub fn new_with_icon, S: AsRef>( + manager: &M, + text: S, + enabled: bool, + icon: Option>, + ) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + let text = text.as_ref().to_owned(); + let icon_data = icon.map(|i| (i.rgba().to_vec(), i.width(), i.height())); + let submenu = run_main_thread!(handle, move || { + let submenu = muda::Submenu::new(text, enabled); + if let Some((rgba, width, height)) = icon_data { + submenu.set_icon(Some(MudaIcon::from_rgba(rgba, width, height).unwrap())); + } + SubmenuInner::new(app_handle, submenu) + })?; + Ok(Self(Arc::new(submenu))) + } + + /// Create a new submenu with a native icon. + pub fn new_with_native_icon, S: AsRef>( + manager: &M, + text: S, + enabled: bool, + icon: Option, + ) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + let text = text.as_ref().to_owned(); + let submenu = run_main_thread!(handle, || { + let submenu = muda::Submenu::new(text, enabled); + if let Some(icon) = icon { + submenu.set_native_icon(Some(icon.into())); + } + SubmenuInner::new(app_handle, submenu) + })?; + Ok(Self(Arc::new(submenu))) + } + + /// Creates a new submenu with the specified id. + pub fn with_id, I: Into, S: AsRef>( + manager: &M, + id: I, + text: S, + enabled: bool, + ) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + + let id = id.into(); + let text = text.as_ref().to_owned(); + + let submenu = run_main_thread!(handle, move || { + let submenu = muda::Submenu::with_id(id, text, enabled); + SubmenuInner::new(app_handle, submenu) + })?; + + Ok(Self(Arc::new(submenu))) + } + + /// Create a new submenu with an id and an icon. + pub fn with_id_and_icon, I: Into, S: AsRef>( + manager: &M, + id: I, + text: S, + enabled: bool, + icon: Option>, + ) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + let id = id.into(); + let text = text.as_ref().to_owned(); + let icon_data = icon.map(|i| (i.rgba().to_vec(), i.width(), i.height())); + let submenu = run_main_thread!(handle, move || { + let submenu = muda::Submenu::with_id(id, text, enabled); + if let Some((rgba, width, height)) = icon_data { + submenu.set_icon(Some(MudaIcon::from_rgba(rgba, width, height).unwrap())); + } + SubmenuInner::new(app_handle, submenu) + })?; + Ok(Self(Arc::new(submenu))) + } + + /// Create a new submenu with an id and a native icon. + pub fn with_id_and_native_icon, I: Into, S: AsRef>( + manager: &M, + id: I, + text: S, + enabled: bool, + icon: Option, + ) -> crate::Result { + let handle = manager.app_handle(); + let app_handle = handle.clone(); + let id = id.into(); + let text = text.as_ref().to_owned(); + let submenu = run_main_thread!(handle, move || { + let submenu = muda::Submenu::with_id(id, text, enabled); + if let Some(icon) = icon { + submenu.set_native_icon(Some(icon.into())); + } + SubmenuInner::new(app_handle, submenu) + })?; + Ok(Self(Arc::new(submenu))) + } + + /// Creates a new menu with given `items`. It calls [`Submenu::new`] and [`Submenu::append_items`] internally. + pub fn with_items, S: AsRef>( + manager: &M, + text: S, + enabled: bool, + items: &[&dyn IsMenuItem], + ) -> crate::Result { + let menu = Self::new(manager, text, enabled)?; + menu.append_items(items)?; + Ok(menu) + } + + /// Creates a new menu with the specified id and given `items`. + /// It calls [`Submenu::new`] and [`Submenu::append_items`] internally. + pub fn with_id_and_items, I: Into, S: AsRef>( + manager: &M, + id: I, + text: S, + enabled: bool, + items: &[&dyn IsMenuItem], + ) -> crate::Result { + let menu = Self::with_id(manager, id, text, enabled)?; + menu.append_items(items)?; + Ok(menu) + } + + pub(crate) fn inner(&self) -> &muda::Submenu { + (*self.0).as_ref() + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.0.app_handle + } + + /// Returns a unique identifier associated with this submenu. + pub fn id(&self) -> &MenuId { + self.0.inner.id() + } + + /// Add a menu item to the end of this submenu. + pub fn append(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().append(kind.inner().inner_muda()) + })? + .map_err(Into::into) + } + + /// Add menu items to the end of this submenu. It calls [`Submenu::append`] in a loop internally. + pub fn append_items(&self, items: &[&dyn IsMenuItem]) -> crate::Result<()> { + for item in items { + self.append(*item)? + } + + Ok(()) + } + + /// Add a menu item to the beginning of this submenu. + pub fn prepend(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().prepend(kind.inner().inner_muda()) + })? + .map_err(Into::into) + } + + /// Add menu items to the beginning of this submenu. It calls [`Submenu::insert_items`] with position of `0` internally. + pub fn prepend_items(&self, items: &[&dyn IsMenuItem]) -> crate::Result<()> { + self.insert_items(items, 0) + } + + /// Insert a menu item at the specified `position` in this submenu. + pub fn insert(&self, item: &dyn IsMenuItem, position: usize) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| { + (*self_.0) + .as_ref() + .insert(kind.inner().inner_muda(), position) + })? + .map_err(Into::into) + } + + /// Insert menu items at the specified `position` in this submenu. + pub fn insert_items(&self, items: &[&dyn IsMenuItem], position: usize) -> crate::Result<()> { + for (i, item) in items.iter().enumerate() { + self.insert(*item, position + i)? + } + + Ok(()) + } + + /// Remove a menu item from this submenu. + pub fn remove(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().remove(kind.inner().inner_muda()) + })? + .map_err(Into::into) + } + + /// Remove the menu item at the specified position from this submenu and returns it. + pub fn remove_at(&self, position: usize) -> crate::Result>> { + run_item_main_thread!(self, |self_: Self| { + (*self_.0) + .as_ref() + .remove_at(position) + .map(|i| MenuItemKind::from_muda(self_.0.app_handle.clone(), i)) + }) + } + + /// Retrieves the menu item matching the given identifier. + pub fn get<'a, I>(&self, id: &'a I) -> Option> + where + I: ?Sized, + MenuId: PartialEq<&'a I>, + { + self + .items() + .unwrap_or_default() + .into_iter() + .find(|i| i.id() == &id) + } + + /// Returns a list of menu items that has been added to this submenu. + pub fn items(&self) -> crate::Result>> { + run_item_main_thread!(self, |self_: Self| { + (*self_.0) + .as_ref() + .items() + .into_iter() + .map(|i| MenuItemKind::from_muda(self_.0.app_handle.clone(), i)) + .collect::>() + }) + } + + /// Get the text for this submenu. + pub fn text(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().text()) + } + + /// Set the text for this submenu. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this submenu. To display a `&` without assigning a mnemonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_text(text)) + } + + /// Get whether this submenu is enabled or not. + pub fn is_enabled(&self) -> crate::Result { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().is_enabled()) + } + + /// Enable or disable this submenu. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_enabled(enabled)) + } + + /// Set this submenu as the Window menu for the application on macOS. + /// + /// This will cause macOS to automatically add window-switching items and + /// certain other items to the menu. + #[cfg(target_os = "macos")] + pub fn set_as_windows_menu_for_nsapp(&self) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().set_as_windows_menu_for_nsapp() + })?; + Ok(()) + } + + /// Set this submenu as the Help menu for the application on macOS. + /// + /// This will cause macOS to automatically add a search box to the menu. + /// + /// If no menu is set as the Help menu, macOS will automatically use any menu + /// which has a title matching the localized word "Help". + #[cfg(target_os = "macos")] + pub fn set_as_help_menu_for_nsapp(&self) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().set_as_help_menu_for_nsapp() + })?; + Ok(()) + } + + /// Change this submenu icon or remove it. + pub fn set_icon(&self, icon: Option>) -> crate::Result<()> { + let icon = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + run_item_main_thread!(self, |self_: Self| (*self_.0).as_ref().set_icon(icon)) + } + + /// Change this submenu icon to a native image or remove it. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn set_native_icon(&self, _icon: Option) -> crate::Result<()> { + #[cfg(target_os = "macos")] + return run_item_main_thread!(self, |self_: Self| { + (*self_.0).as_ref().set_native_icon(_icon.map(Into::into)) + }); + #[allow(unreachable_code)] + Ok(()) + } +} diff --git a/crates/tauri/src/path/android.rs b/crates/tauri/src/path/android.rs new file mode 100644 index 0000000..f8d1746 --- /dev/null +++ b/crates/tauri/src/path/android.rs @@ -0,0 +1,187 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::Result; +use crate::{Runtime, plugin::PluginHandle}; +use std::path::{Path, PathBuf}; + +/// A helper class to access the mobile path APIs. +pub struct PathResolver(pub(crate) PluginHandle); + +impl Clone for PathResolver { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +#[derive(serde::Deserialize)] +struct PathResponse { + path: PathBuf, +} + +#[derive(serde::Serialize)] +struct GetFileNameFromUriRequest<'a> { + uri: &'a str, +} + +#[derive(serde::Deserialize)] +struct GetFileNameFromUriResponse { + name: Option, +} + +impl PathResolver { + /// Returns the final component of the `Path`, if there is one. + /// + /// If the path is a normal file, this is the file name. If it's the path of a directory, this + /// is the directory name. + /// + /// Returns [`None`] if the path terminates in `..`. + /// + /// On Android this also supports checking the file name of content URIs, such as the values returned by the dialog plugin. + /// + /// If you are dealing with plain file system paths or not worried about Android content URIs, prefer [`Path::file_name`]. + pub fn file_name(&self, path: &str) -> Option { + if path.starts_with("content://") || path.starts_with("file://") { + self + .0 + .run_mobile_plugin::( + "getFileNameFromUri", + GetFileNameFromUriRequest { uri: path }, + ) + .map(|r| r.name) + .unwrap_or_else(|e| { + log::error!("failed to get file name from URI: {e}"); + None + }) + } else { + Path::new(path) + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + } + } + + fn call_resolve(&self, dir: &str) -> Result { + self + .0 + .run_mobile_plugin::(dir, ()) + .map(|r| r.path) + .map_err(Into::into) + } + + /// Returns the path to the user's audio directory. + pub fn audio_dir(&self) -> Result { + self.call_resolve("getAudioDir") + } + + /// Returns the path to the user's cache directory. + pub fn cache_dir(&self) -> Result { + self.call_resolve("getExternalCacheDir") + } + + /// Returns the path to the user's config directory. + pub fn config_dir(&self) -> Result { + self.call_resolve("getConfigDir") + } + + /// Returns the path to the user's data directory. + pub fn data_dir(&self) -> Result { + self.call_resolve("getDataDir") + } + + /// Returns the path to the user's local data directory. + pub fn local_data_dir(&self) -> Result { + self.call_resolve("getDataDir") + } + + /// Returns the path to the user's document directory. + pub fn document_dir(&self) -> Result { + self.call_resolve("getDocumentDir") + } + + /// Returns the path to the user's download directory. + pub fn download_dir(&self) -> Result { + self.call_resolve("getDownloadDir") + } + + /// Returns the path to the user's picture directory. + pub fn picture_dir(&self) -> Result { + self.call_resolve("getPictureDir") + } + + /// Returns the path to the user's public directory. + pub fn public_dir(&self) -> Result { + self.call_resolve("getPublicDir") + } + + /// Returns the path to the user's video directory. + /// + /// Resolves to the app-specific Movies directory (`getExternalFilesDir(DIRECTORY_MOVIES)`), + /// typically `.../files/Movies`. + /// + /// ## Migration + /// + /// Previously this resolved to external cache storage ([`cache_dir`], typically `.../cache`). + /// Files written to the old location will not be discovered at the new path and must be migrated + /// or paths updated accordingly. + pub fn video_dir(&self) -> Result { + self.call_resolve("getVideoDir") + } + + /// Returns the path to the resource directory of this app. + pub fn resource_dir(&self) -> Result { + self.call_resolve("getResourcesDir") + } + + /// Returns the path to the suggested directory for your app's config files. + /// + /// Resolves to [`config_dir`]`/${bundle_identifier}`. + pub fn app_config_dir(&self) -> Result { + self.call_resolve("getConfigDir") + } + + /// Returns the path to the suggested directory for your app's data files. + /// + /// Resolves to [`data_dir`]`/${bundle_identifier}`. + pub fn app_data_dir(&self) -> Result { + self.call_resolve("getDataDir") + } + + /// Returns the path to the suggested directory for your app's local data files. + /// + /// Resolves to [`local_data_dir`]`/${bundle_identifier}`. + pub fn app_local_data_dir(&self) -> Result { + self.call_resolve("getDataDir") + } + + /// Returns the path to the suggested directory for your app's cache files. + /// + /// Resolves to [`cache_dir`]`/${bundle_identifier}`. + pub fn app_cache_dir(&self) -> Result { + self.call_resolve("getCacheDir") + } + + /// Returns the path to the suggested directory for your app's log files. + pub fn app_log_dir(&self) -> Result { + self + .call_resolve("getConfigDir") + .map(|dir| dir.join("logs")) + } + + /// A temporary directory. Resolves to [`std::env::temp_dir`]. + pub fn temp_dir(&self) -> Result { + Ok(std::env::temp_dir()) + } + + /// Returns the path to the user's home directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$HOME`. + /// - **macOS:** Resolves to `$HOME`. + /// - **Windows:** Resolves to `{FOLDERID_Profile}`. + /// - **iOS**: Cannot be written to directly, use one of the app paths instead. + pub fn home_dir(&self) -> Result { + self.call_resolve("getHomeDir") + } +} diff --git a/crates/tauri/src/path/desktop.rs b/crates/tauri/src/path/desktop.rs new file mode 100644 index 0000000..96ea0bd --- /dev/null +++ b/crates/tauri/src/path/desktop.rs @@ -0,0 +1,296 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{Error, Result}; +use crate::{AppHandle, Manager, Runtime}; +use std::path::{Path, PathBuf}; + +/// The path resolver is a helper class for general and application-specific path APIs. +pub struct PathResolver(pub(crate) AppHandle); + +impl Clone for PathResolver { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl PathResolver { + /// Returns the final component of the `Path`, if there is one. + /// + /// If the path is a normal file, this is the file name. If it's the path of a directory, this + /// is the directory name. + /// + /// Returns [`None`] if the path terminates in `..`. + /// + /// On Android this also supports checking the file name of content URIs, such as the values returned by the dialog plugin. + /// + /// If you are dealing with plain file system paths or not worried about Android content URIs, prefer [`Path::file_name`]. + pub fn file_name(&self, path: &str) -> Option { + Path::new(path) + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + } + + /// Returns the path to the user's audio directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_MUSIC_DIR`. + /// - **macOS:** Resolves to `$HOME/Music`. + /// - **Windows:** Resolves to `{FOLDERID_Music}`. + pub fn audio_dir(&self) -> Result { + dirs::audio_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's cache directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_CACHE_HOME` or `$HOME/.cache`. + /// - **macOS:** Resolves to `$HOME/Library/Caches`. + /// - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. + pub fn cache_dir(&self) -> Result { + dirs::cache_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's config directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_CONFIG_HOME` or `$HOME/.config`. + /// - **macOS:** Resolves to `$HOME/Library/Application Support`. + /// - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. + pub fn config_dir(&self) -> Result { + dirs::config_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's data directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. + /// - **macOS:** Resolves to `$HOME/Library/Application Support`. + /// - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. + pub fn data_dir(&self) -> Result { + dirs::data_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's local data directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. + /// - **macOS:** Resolves to `$HOME/Library/Application Support`. + /// - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. + pub fn local_data_dir(&self) -> Result { + dirs::data_local_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's desktop directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DESKTOP_DIR`. + /// - **macOS:** Resolves to `$HOME/Desktop`. + /// - **Windows:** Resolves to `{FOLDERID_Desktop}`. + pub fn desktop_dir(&self) -> Result { + dirs::desktop_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's document directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOCUMENTS_DIR`. + /// - **macOS:** Resolves to `$HOME/Documents`. + /// - **Windows:** Resolves to `{FOLDERID_Documents}`. + pub fn document_dir(&self) -> Result { + dirs::document_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's download directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOWNLOAD_DIR`. + /// - **macOS:** Resolves to `$HOME/Downloads`. + /// - **Windows:** Resolves to `{FOLDERID_Downloads}`. + pub fn download_dir(&self) -> Result { + dirs::download_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's executable directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_BIN_HOME/../bin` or `$XDG_DATA_HOME/../bin` or `$HOME/.local/bin`. + /// - **macOS:** Not supported. + /// - **Windows:** Not supported. + pub fn executable_dir(&self) -> Result { + dirs::executable_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's font directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_DATA_HOME/fonts` or `$HOME/.local/share/fonts`. + /// - **macOS:** Resolves to `$HOME/Library/Fonts`. + /// - **Windows:** Not supported. + pub fn font_dir(&self) -> Result { + dirs::font_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's home directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$HOME`. + /// - **macOS:** Resolves to `$HOME`. + /// - **Windows:** Resolves to `{FOLDERID_Profile}`. + /// - **iOS**: Cannot be written to directly, use one of the app paths instead. + pub fn home_dir(&self) -> Result { + dirs::home_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's picture directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PICTURES_DIR`. + /// - **macOS:** Resolves to `$HOME/Pictures`. + /// - **Windows:** Resolves to `{FOLDERID_Pictures}`. + pub fn picture_dir(&self) -> Result { + dirs::picture_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's public directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PUBLICSHARE_DIR`. + /// - **macOS:** Resolves to `$HOME/Public`. + /// - **Windows:** Resolves to `{FOLDERID_Public}`. + pub fn public_dir(&self) -> Result { + dirs::public_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's runtime directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_RUNTIME_DIR`. + /// - **macOS:** Not supported. + /// - **Windows:** Not supported. + pub fn runtime_dir(&self) -> Result { + dirs::runtime_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's template directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_TEMPLATES_DIR`. + /// - **macOS:** Not supported. + /// - **Windows:** Resolves to `{FOLDERID_Templates}`. + pub fn template_dir(&self) -> Result { + dirs::template_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's video dir + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_VIDEOS_DIR`. + /// - **macOS:** Resolves to `$HOME/Movies`. + /// - **Windows:** Resolves to `{FOLDERID_Videos}`. + pub fn video_dir(&self) -> Result { + dirs::video_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the resource directory of this app. + /// + /// ## Platform-specific + /// + /// Although we provide the exact path where this function resolves to, + /// this is not a contract and things might change in the future + /// + /// - **Windows:** Resolves to the directory that contains the main executable. + /// - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to + /// the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`. + /// If not running in an AppImage, the path is `/usr/lib/${exe_name}`. + /// When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`. + /// - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app). + /// - **iOS:** Resolves to `${exe_dir}/assets`. + /// - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path, + /// we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/), + /// with that, you can read the files through [`FsExt::fs`](https://docs.rs/tauri-plugin-fs/latest/tauri_plugin_fs/trait.FsExt.html#tymethod.fs) + /// like this: `app.fs().read_to_string(app.path().resource_dir().unwrap().join("resource"));` + pub fn resource_dir(&self) -> Result { + crate::utils::platform::resource_dir(self.0.package_info(), &self.0.env()) + .map_err(|_| Error::UnknownPath) + } + + /// Returns the path to the suggested directory for your app's config files. + /// + /// Resolves to [`config_dir`](Self::config_dir)`/${bundle_identifier}`. + pub fn app_config_dir(&self) -> Result { + dirs::config_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join(&self.0.config().identifier)) + } + + /// Returns the path to the suggested directory for your app's data files. + /// + /// Resolves to [`data_dir`](Self::data_dir)`/${bundle_identifier}`. + pub fn app_data_dir(&self) -> Result { + dirs::data_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join(&self.0.config().identifier)) + } + + /// Returns the path to the suggested directory for your app's local data files. + /// + /// Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}`. + pub fn app_local_data_dir(&self) -> Result { + dirs::data_local_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join(&self.0.config().identifier)) + } + + /// Returns the path to the suggested directory for your app's cache files. + /// + /// Resolves to [`cache_dir`](Self::cache_dir)`/${bundle_identifier}`. + pub fn app_cache_dir(&self) -> Result { + dirs::cache_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join(&self.0.config().identifier)) + } + + /// Returns the path to the suggested directory for your app's log files. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}/logs`. + /// - **macOS:** Resolves to [`home_dir`](Self::home_dir)`/Library/Logs/${bundle_identifier}` + /// - **Windows:** Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}/logs`. + pub fn app_log_dir(&self) -> Result { + #[cfg(target_os = "macos")] + let path = dirs::home_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join("Library/Logs").join(&self.0.config().identifier)); + + #[cfg(not(target_os = "macos"))] + let path = dirs::data_local_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join(&self.0.config().identifier).join("logs")); + + path + } + + /// A temporary directory. Resolves to [`std::env::temp_dir`]. + pub fn temp_dir(&self) -> Result { + Ok(std::env::temp_dir()) + } +} diff --git a/crates/tauri/src/path/init.js b/crates/tauri/src/path/init.js new file mode 100644 index 0000000..a402f61 --- /dev/null +++ b/crates/tauri/src/path/init.js @@ -0,0 +1,10 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +Object.defineProperty(window.__TAURI_INTERNALS__.plugins, 'path', { + value: { + sep: __TEMPLATE_sep__, + delimiter: __TEMPLATE_delimiter__ + } +}) diff --git a/crates/tauri/src/path/mod.rs b/crates/tauri/src/path/mod.rs new file mode 100644 index 0000000..2826c0e --- /dev/null +++ b/crates/tauri/src/path/mod.rs @@ -0,0 +1,386 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + path::{Component, Display, Path, PathBuf}, + str::FromStr, +}; + +use crate::Runtime; + +use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +pub(crate) mod plugin; + +use crate::error::*; + +#[cfg(target_os = "android")] +mod android; +#[cfg(not(target_os = "android"))] +mod desktop; + +#[cfg(target_os = "android")] +pub use android::PathResolver; +#[cfg(not(target_os = "android"))] +pub use desktop::PathResolver; + +/// A wrapper for [`PathBuf`] that prevents path traversal. +/// +/// # Examples +/// +/// ``` +/// # use tauri::path::SafePathBuf; +/// assert!(SafePathBuf::new("../secret.txt".into()).is_err()); +/// assert!(SafePathBuf::new("/home/user/stuff/../secret.txt".into()).is_err()); +/// +/// assert!(SafePathBuf::new("./file.txt".into()).is_ok()); +/// assert!(SafePathBuf::new("/home/user/secret.txt".into()).is_ok()); +/// ``` +#[derive(Clone, Debug, Serialize)] +pub struct SafePathBuf(PathBuf); + +impl SafePathBuf { + /// Validates the path for directory traversal vulnerabilities and returns a new [`SafePathBuf`] instance if it is safe. + pub fn new(path: PathBuf) -> std::result::Result { + if path.components().any(|x| matches!(x, Component::ParentDir)) { + return Err("cannot traverse directory, rewrite the path without the use of `../`"); + } + Ok(Self(path)) + } + + /// Returns an object that implements [`std::fmt::Display`] for safely printing paths. + /// + /// See [`PathBuf#method.display`] for more information. + pub fn display(&self) -> Display<'_> { + self.0.display() + } +} + +impl AsRef for SafePathBuf { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl FromStr for SafePathBuf { + type Err = &'static str; + + fn from_str(s: &str) -> std::result::Result { + Self::new(s.into()) + } +} + +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 + D: Deserializer<'de>, + { + let path = PathBuf::deserialize(deserializer)?; + SafePathBuf::new(path).map_err(DeError::custom) + } +} + +/// A base directory for a path. +/// +/// The base directory is the optional root of a file system operation. +/// If informed by the API call, all paths will be relative to the path of the given directory. +/// +/// For more information, check the [`dirs` documentation](https://docs.rs/dirs/). +#[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)] +#[repr(u16)] +#[non_exhaustive] +pub enum BaseDirectory { + /// The Audio directory. + /// Resolves to [`crate::path::PathResolver::audio_dir`]. + Audio = 1, + /// The Cache directory. + /// Resolves to [`crate::path::PathResolver::cache_dir`]. + Cache = 2, + /// The Config directory. + /// Resolves to [`crate::path::PathResolver::config_dir`]. + Config = 3, + /// The Data directory. + /// Resolves to [`crate::path::PathResolver::data_dir`]. + Data = 4, + /// The LocalData directory. + /// Resolves to [`crate::path::PathResolver::local_data_dir`]. + LocalData = 5, + /// The Document directory. + /// Resolves to [`crate::path::PathResolver::document_dir`]. + Document = 6, + /// The Download directory. + /// Resolves to [`crate::path::PathResolver::download_dir`]. + Download = 7, + /// The Picture directory. + /// Resolves to [`crate::path::PathResolver::picture_dir`]. + Picture = 8, + /// The Public directory. + /// Resolves to [`crate::path::PathResolver::public_dir`]. + Public = 9, + /// The Video directory. + /// Resolves to [`crate::path::PathResolver::video_dir`]. + Video = 10, + /// The Resource directory. + /// Resolves to [`crate::path::PathResolver::resource_dir`]. + Resource = 11, + /// A temporary directory. + /// Resolves to [`std::env::temp_dir`]. + Temp = 12, + /// The default app config directory. + /// Resolves to [`BaseDirectory::Config`]`/{bundle_identifier}`. + AppConfig = 13, + /// The default app data directory. + /// Resolves to [`BaseDirectory::Data`]`/{bundle_identifier}`. + AppData = 14, + /// The default app local data directory. + /// Resolves to [`BaseDirectory::LocalData`]`/{bundle_identifier}`. + AppLocalData = 15, + /// The default app cache directory. + /// Resolves to [`BaseDirectory::Cache`]`/{bundle_identifier}`. + AppCache = 16, + /// The default app log directory. + /// Resolves to [`BaseDirectory::Home`]`/Library/Logs/{bundle_identifier}` on macOS + /// and [`BaseDirectory::Config`]`/{bundle_identifier}/logs` on linux and Windows. + AppLog = 17, + /// The Desktop directory. + /// Resolves to [`crate::path::PathResolver::desktop_dir`]. + #[cfg(not(target_os = "android"))] + Desktop = 18, + /// The Executable directory. + /// Resolves to [`crate::path::PathResolver::executable_dir`]. + #[cfg(not(target_os = "android"))] + Executable = 19, + /// The Font directory. + /// Resolves to [`crate::path::PathResolver::font_dir`]. + #[cfg(not(target_os = "android"))] + Font = 20, + /// The Home directory. + /// Resolves to [`crate::path::PathResolver::home_dir`]. + Home = 21, + /// The Runtime directory. + /// Resolves to [`crate::path::PathResolver::runtime_dir`]. + #[cfg(not(target_os = "android"))] + Runtime = 22, + /// The Template directory. + /// Resolves to [`crate::path::PathResolver::template_dir`]. + #[cfg(not(target_os = "android"))] + Template = 23, +} + +impl BaseDirectory { + /// Gets the variable that represents this [`BaseDirectory`] for string paths. + pub fn variable(self) -> &'static str { + match self { + Self::Audio => "$AUDIO", + Self::Cache => "$CACHE", + Self::Config => "$CONFIG", + Self::Data => "$DATA", + Self::LocalData => "$LOCALDATA", + Self::Document => "$DOCUMENT", + Self::Download => "$DOWNLOAD", + Self::Picture => "$PICTURE", + Self::Public => "$PUBLIC", + Self::Video => "$VIDEO", + Self::Resource => "$RESOURCE", + Self::Temp => "$TEMP", + Self::AppConfig => "$APPCONFIG", + Self::AppData => "$APPDATA", + Self::AppLocalData => "$APPLOCALDATA", + Self::AppCache => "$APPCACHE", + Self::AppLog => "$APPLOG", + Self::Home => "$HOME", + + #[cfg(not(target_os = "android"))] + Self::Desktop => "$DESKTOP", + #[cfg(not(target_os = "android"))] + Self::Executable => "$EXE", + #[cfg(not(target_os = "android"))] + Self::Font => "$FONT", + #[cfg(not(target_os = "android"))] + Self::Runtime => "$RUNTIME", + #[cfg(not(target_os = "android"))] + Self::Template => "$TEMPLATE", + } + } + + /// Gets the [`BaseDirectory`] associated with the given variable, or [`None`] if the variable doesn't match any. + pub fn from_variable(variable: &str) -> Option { + let res = match variable { + "$AUDIO" => Self::Audio, + "$CACHE" => Self::Cache, + "$CONFIG" => Self::Config, + "$DATA" => Self::Data, + "$LOCALDATA" => Self::LocalData, + "$DOCUMENT" => Self::Document, + "$DOWNLOAD" => Self::Download, + + "$PICTURE" => Self::Picture, + "$PUBLIC" => Self::Public, + "$VIDEO" => Self::Video, + "$RESOURCE" => Self::Resource, + "$TEMP" => Self::Temp, + "$APPCONFIG" => Self::AppConfig, + "$APPDATA" => Self::AppData, + "$APPLOCALDATA" => Self::AppLocalData, + "$APPCACHE" => Self::AppCache, + "$APPLOG" => Self::AppLog, + "$HOME" => Self::Home, + + #[cfg(not(target_os = "android"))] + "$DESKTOP" => Self::Desktop, + #[cfg(not(target_os = "android"))] + "$EXE" => Self::Executable, + #[cfg(not(target_os = "android"))] + "$FONT" => Self::Font, + #[cfg(not(target_os = "android"))] + "$RUNTIME" => Self::Runtime, + #[cfg(not(target_os = "android"))] + "$TEMPLATE" => Self::Template, + + _ => return None, + }; + Some(res) + } +} + +impl PathResolver { + /// Resolves the path with the base directory. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::{path::BaseDirectory, Manager}; + /// tauri::Builder::default() + /// .setup(|app| { + /// let path = app.path().resolve("path/to/something", BaseDirectory::Config)?; + /// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.config/path/to/something"); + /// Ok(()) + /// }); + /// ``` + pub fn resolve>(&self, path: P, base_directory: BaseDirectory) -> Result { + resolve_path::(self, base_directory, Some(path.as_ref().to_path_buf())) + } + + /// Parse the given path, resolving a [`BaseDirectory`] variable if the path starts with one. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// let path = app.path().parse("$HOME/.bashrc")?; + /// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.bashrc"); + /// Ok(()) + /// }); + /// ``` + pub fn parse>(&self, path: P) -> Result { + let mut p = PathBuf::new(); + let mut components = path.as_ref().components(); + match components.next() { + Some(Component::Normal(str)) => { + if let Some(base_directory) = BaseDirectory::from_variable(&str.to_string_lossy()) { + p.push(resolve_path::(self, base_directory, None)?); + } else { + p.push(str); + } + } + Some(component) => p.push(component), + None => (), + } + + for component in components { + if let Component::ParentDir = component { + continue; + } + p.push(component); + } + + Ok(p) + } +} + +fn resolve_path( + resolver: &PathResolver, + directory: BaseDirectory, + path: Option, +) -> Result { + let resolve_resource = matches!(directory, BaseDirectory::Resource); + let mut base_dir_path = match directory { + BaseDirectory::Audio => resolver.audio_dir(), + BaseDirectory::Cache => resolver.cache_dir(), + BaseDirectory::Config => resolver.config_dir(), + BaseDirectory::Data => resolver.data_dir(), + BaseDirectory::LocalData => resolver.local_data_dir(), + BaseDirectory::Document => resolver.document_dir(), + BaseDirectory::Download => resolver.download_dir(), + BaseDirectory::Picture => resolver.picture_dir(), + BaseDirectory::Public => resolver.public_dir(), + BaseDirectory::Video => resolver.video_dir(), + BaseDirectory::Resource => resolver.resource_dir(), + BaseDirectory::Temp => resolver.temp_dir(), + BaseDirectory::AppConfig => resolver.app_config_dir(), + BaseDirectory::AppData => resolver.app_data_dir(), + BaseDirectory::AppLocalData => resolver.app_local_data_dir(), + BaseDirectory::AppCache => resolver.app_cache_dir(), + BaseDirectory::AppLog => resolver.app_log_dir(), + BaseDirectory::Home => resolver.home_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Desktop => resolver.desktop_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Executable => resolver.executable_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Font => resolver.font_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Runtime => resolver.runtime_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Template => resolver.template_dir(), + }?; + + if let Some(path) = path { + // use the same path resolution mechanism as the bundler's resource injection algorithm + if resolve_resource { + let mut resource_path = PathBuf::new(); + for component in path.components() { + match component { + Component::Prefix(_) => {} + Component::RootDir => resource_path.push("_root_"), + Component::CurDir => {} + Component::ParentDir => resource_path.push("_up_"), + Component::Normal(p) => resource_path.push(p), + } + } + base_dir_path.push(resource_path); + } else { + base_dir_path.push(path); + } + } + + Ok(base_dir_path) +} + +#[cfg(test)] +mod test { + use super::SafePathBuf; + use quickcheck::{Arbitrary, Gen}; + + use std::path::PathBuf; + + impl Arbitrary for SafePathBuf { + fn arbitrary(g: &mut Gen) -> Self { + Self(PathBuf::arbitrary(g)) + } + + fn shrink(&self) -> Box> { + Box::new(self.0.shrink().map(SafePathBuf)) + } + } +} diff --git a/crates/tauri/src/path/plugin.rs b/crates/tauri/src/path/plugin.rs new file mode 100644 index 0000000..c00ff68 --- /dev/null +++ b/crates/tauri/src/path/plugin.rs @@ -0,0 +1,315 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::{Component, MAIN_SEPARATOR, Path, PathBuf}; + +use serialize_to_javascript::{DefaultTemplate, Template, default_template}; + +use super::{BaseDirectory, Error, PathResolver, Result}; +use crate::{ + AppHandle, Manager, Runtime, State, command, + plugin::{Builder, TauriPlugin}, +}; + +/// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util. +/// +fn normalize_path(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + } + } + ret +} + +/// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util but +/// slightly modified to not resolve absolute paths. +/// +fn normalize_path_no_absolute(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + // Using PathBuf::push here will replace the whole path if an absolute path is encountered + // which is not the intended behavior, so instead of that, convert the current resolved path + // to a string and do simple string concatenation with the current component then convert it + // back to a PathBuf + let mut p = ret.to_string_lossy().to_string(); + // Only add a separator if it doesn't have one already or if current normalized path is empty, + // this ensures it won't have an unwanted leading separator + if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') { + p.push(MAIN_SEPARATOR); + } + if let Some(c) = c.to_str() { + p.push_str(c); + } + ret = PathBuf::from(p); + } + } + } + ret +} + +#[command(root = "crate")] +pub fn resolve_directory( + _app: AppHandle, + resolver: State<'_, PathResolver>, + directory: BaseDirectory, + path: Option, +) -> Result { + super::resolve_path(&resolver, directory, path).map(|p| dunce::simplified(&p).to_path_buf()) +} + +#[command(root = "crate")] +pub fn resolve(paths: Vec) -> Result { + // Start with current directory then start adding paths from the vector one by one using `PathBuf.push()` which + // will ensure that if an absolute path is encountered in the iteration, it will be used as the current full path. + // + // examples: + // 1. `vec!["."]` or `vec![]` will be equal to `std::env::current_dir()` + // 2. `vec!["/foo/bar", "/tmp/file", "baz"]` will be equal to `PathBuf::from("/tmp/file/baz")` + let mut path = std::env::current_dir().map_err(Error::CurrentDir)?; + for p in paths { + path.push(p); + } + Ok(dunce::simplified(&normalize_path(&path)).to_path_buf()) +} + +#[command(root = "crate")] +pub fn normalize(path: String) -> String { + let mut p = dunce::simplified(&normalize_path_no_absolute(Path::new(&path))) + .to_string_lossy() + .to_string(); + + // Node.js behavior is to return `".."` for `normalize("..")` + // and `"."` for `normalize("")` or `normalize(".")` + if p.is_empty() && path == ".." { + "..".into() + } else if p.is_empty() && (path.is_empty() || path == ".") { + ".".into() + } else { + // Add a trailing separator if the path passed to this functions had a trailing separator. That's how Node.js behaves. + if (path.ends_with('/') || path.ends_with('\\')) && (!p.ends_with('/') || !p.ends_with('\\')) { + p.push(MAIN_SEPARATOR); + } + p + } +} + +#[command(root = "crate")] +pub fn join(paths: Vec) -> String { + let path = PathBuf::from( + paths + .into_iter() + .map(|mut p| { + // Add a `MAIN_SEPARATOR` if it doesn't already have one and is not an empty string. + // Doing this to ensure that the vector elements are separated in + // the resulting string so path.components() can work correctly when called + // in `normalize_path_no_absolute()` later on. + if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') { + p.push(MAIN_SEPARATOR); + } + p + }) + .collect::(), + ); + + let p = dunce::simplified(&normalize_path_no_absolute(&path)) + .to_string_lossy() + .to_string(); + + if p.is_empty() { ".".into() } else { p } +} + +#[command(root = "crate")] +pub fn dirname(path: String) -> Result { + match Path::new(&path).parent() { + Some(p) => Ok(dunce::simplified(p).to_path_buf()), + None => Err(Error::NoParent), + } +} + +#[command(root = "crate")] +pub fn extname(app: AppHandle, path: String) -> Result { + let file_name = app.path().file_name(&path).ok_or(Error::NoExtension)?; + match Path::new(&file_name) + .extension() + .and_then(std::ffi::OsStr::to_str) + { + Some(p) => Ok(p.to_string()), + None => Err(Error::NoExtension), + } +} + +#[command(root = "crate")] +pub fn basename(app: AppHandle, path: &str, ext: Option<&str>) -> Result { + let file_name = app.path().file_name(path); + match file_name { + Some(p) => { + let maybe_stripped = if let Some(ext) = ext { + p.strip_suffix(ext).unwrap_or(&p).to_string() + } else { + p + }; + Ok(maybe_stripped) + } + None => Err(Error::NoBasename), + } +} + +#[command(root = "crate")] +pub fn is_absolute(path: String) -> bool { + Path::new(&path).is_absolute() +} + +#[derive(Template)] +#[default_template("./init.js")] +struct InitJavascript { + sep: &'static str, + delimiter: &'static str, +} + +/// Initializes the plugin. +pub(crate) fn init() -> TauriPlugin { + #[cfg(windows)] + let (sep, delimiter) = ("\\", ";"); + #[cfg(not(windows))] + let (sep, delimiter) = ("/", ":"); + + let init_js = InitJavascript { sep, delimiter } + .render_default(&Default::default()) + // this will never fail with the above sep and delimiter values + .unwrap(); + + Builder::new("path") + .invoke_handler(crate::generate_handler![ + #![plugin(path)] + resolve_directory, + resolve, + normalize, + join, + dirname, + extname, + basename, + is_absolute + ]) + .js_init_script(init_js.to_string()) + .setup(|app, _api| { + #[cfg(target_os = "android")] + { + let handle = _api.register_android_plugin("app.tauri", "PathPlugin")?; + app.manage(PathResolver(handle)); + } + + #[cfg(not(target_os = "android"))] + { + app.manage(PathResolver(app.clone())); + } + + Ok(()) + }) + .build() +} + +#[cfg(test)] +mod tests { + use crate::test::mock_app; + + #[test] + fn basename() { + let app = mock_app(); + let path = "/path/to/some-json-file.json"; + assert_eq!( + super::basename(app.handle().clone(), path, Some(".json")).unwrap(), + "some-json-file" + ); + + let path = "/path/to/some-json-file.json"; + assert_eq!( + super::basename(app.handle().clone(), path, Some("json")).unwrap(), + "some-json-file." + ); + + let path = "/path/to/some-json-file.html.json"; + assert_eq!( + super::basename(app.handle().clone(), path, Some(".json")).unwrap(), + "some-json-file.html" + ); + + let path = "/path/to/some-json-file.json.json"; + assert_eq!( + super::basename(app.handle().clone(), path, Some(".json")).unwrap(), + "some-json-file.json" + ); + + let path = "/path/to/some-json-file.json.html"; + assert_eq!( + super::basename(app.handle().clone(), path, Some(".json")).unwrap(), + "some-json-file.json.html" + ); + } + + #[test] + fn join() { + fn check(paths: Vec<&str>, expected_unix: &str, expected_windows: &str) { + let expected = if cfg!(windows) { + expected_windows + } else { + expected_unix + }; + let paths = paths.into_iter().map(String::from).collect(); + assert_eq!(super::join(paths), expected); + } + + check(vec![""], ".", "."); + check(vec!["", ""], ".", "."); + check(vec!["a"], "a", "a"); + check(vec!["", "a"], "a", "a"); + check(vec!["a", "b"], "a/b", r"a\b"); + check(vec!["a", "", "b"], "a/b", r"a\b"); + check(vec!["a", "/b", "c"], "a/b/c", r"a\b\c"); + check(vec!["a", "b/c", "d"], "a/b/c/d", r"a\b\c\d"); + check(vec!["a/", "b"], "a/b", r"a\b"); + } + + #[test] + fn normalize() { + assert_eq!(super::normalize("".into()), "."); + assert_eq!(super::normalize(".".into()), "."); + assert_eq!(super::normalize("..".into()), ".."); + } +} diff --git a/crates/tauri/src/pattern.rs b/crates/tauri/src/pattern.rs new file mode 100644 index 0000000..c4f73f1 --- /dev/null +++ b/crates/tauri/src/pattern.rs @@ -0,0 +1,87 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(feature = "isolation")] +use std::sync::Arc; + +use serde::Serialize; +use serialize_to_javascript::{Template, default_template}; + +/// The domain of the isolation iframe source. +#[cfg(feature = "isolation")] +pub const ISOLATION_IFRAME_SRC_DOMAIN: &str = "localhost"; + +/// An application pattern. +#[derive(Debug)] +pub enum Pattern { + /// The brownfield pattern. + Brownfield, + /// Isolation pattern. Recommended for security purposes. + #[cfg(feature = "isolation")] + Isolation { + /// The HTML served on `isolation://index.html`. + assets: Arc, + + /// The schema used for the isolation frames. + schema: String, + + /// A random string used to ensure that the message went through the isolation frame. + /// + /// This should be regenerated at runtime. + key: String, + + /// Cryptographically secure keys + crypto_keys: Box, + }, +} + +/// The shape of the JavaScript Pattern config +#[derive(Debug, Serialize)] +#[serde(rename_all = "lowercase", tag = "pattern")] +pub(crate) enum PatternObject { + /// Brownfield pattern. + Brownfield, + /// Isolation pattern. Recommended for security purposes. + #[cfg(feature = "isolation")] + Isolation { + /// Which `IsolationSide` this `PatternObject` is getting injected into + side: IsolationSide, + }, +} + +impl From<&Pattern> for PatternObject { + fn from(pattern: &Pattern) -> Self { + match pattern { + Pattern::Brownfield => Self::Brownfield, + #[cfg(feature = "isolation")] + Pattern::Isolation { .. } => Self::Isolation { + side: IsolationSide::default(), + }, + } + } +} + +/// Where the JavaScript is injected to +#[cfg(feature = "isolation")] +#[derive(Default, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub(crate) enum IsolationSide { + /// Original frame, the Brownfield application + #[default] + Original, + /// Secure frame, the isolation security application + #[allow(dead_code)] + Secure, +} + +#[derive(Template)] +#[default_template("../scripts/pattern.js")] +pub(crate) struct PatternJavascript { + pub(crate) pattern: PatternObject, +} + +#[cfg(feature = "isolation")] +pub(crate) fn format_real_schema(schema: &str, https: bool) -> String { + format!("{}/", crate::protocol::origin(schema, https)) +} diff --git a/crates/tauri/src/plugin.rs b/crates/tauri/src/plugin.rs new file mode 100644 index 0000000..634aa63 --- /dev/null +++ b/crates/tauri/src/plugin.rs @@ -0,0 +1,1151 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The Tauri plugin extension to expand Tauri functionality. + +use crate::{ + AppHandle, Error, RunEvent, Runtime, UriSchemeContext, Webview, Window, + app::UriSchemeResponder, + ipc::{Invoke, InvokeHandler, ScopeObject, ScopeValue}, + manager::webview::UriSchemeProtocol, + utils::config::PluginConfig, + webview::PageLoadPayload, +}; +use serde::{ + Serialize, Serializer, + de::{Deserialize, DeserializeOwned, Deserializer, Error as DeError}, +}; +use serde_json::Value as JsonValue; +use tauri_macros::default_runtime; +use tauri_runtime::webview::InitializationScript; +use thiserror::Error; +use url::Url; + +use std::{ + borrow::Cow, + collections::HashMap, + fmt::{self, Debug}, + sync::Arc, +}; + +/// Mobile APIs. +#[cfg(mobile)] +pub mod mobile; + +/// The plugin interface. +pub trait Plugin: Send { + /// The plugin name. Used as key on the plugin config object. + fn name(&self) -> &'static str; + + /// Initializes the plugin. + #[allow(unused_variables)] + fn initialize( + &mut self, + app: &AppHandle, + config: JsonValue, + ) -> Result<(), Box> { + Ok(()) + } + + /// Add the provided JavaScript to a list of scripts that should be run after the global object has been created, + /// but before the HTML document has been parsed and before any other script included by the HTML document is run. + /// + /// The script is wrapped into its own context with `(function () { /* your script here */ })();`, + /// so global variables must be assigned to `window` instead of implicitly declared. + /// + /// This is executed only on the main frame. + /// If you only want to run it in all frames, use [`Plugin::initialization_script_2`] to set that to false. + /// + /// ## Platform-specific + /// + /// - **Windows:** scripts are always added to subframes. + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// [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) + fn initialization_script(&self) -> Option { + None + } + + // TODO: Change `initialization_script` to this in v3 + /// Same as [`Plugin::initialization_script`] but returns an [`InitializationScript`] instead + /// We plan to replace [`Plugin::initialization_script`] with this signature in v3 + fn initialization_script_2(&self) -> Option { + self + .initialization_script() + .map(|script| InitializationScript { + script, + for_main_frame_only: true, + }) + } + + /// Callback invoked when the window is created. + #[allow(unused_variables)] + fn window_created(&mut self, window: Window) {} + + /// Callback invoked when the webview is created. + #[allow(unused_variables)] + fn webview_created(&mut self, webview: Webview) {} + + /// Callback invoked when webview tries to navigate to the given Url. Returning false cancels navigation. + #[allow(unused_variables)] + fn on_navigation(&mut self, webview: &Webview, url: &Url) -> bool { + true + } + + /// Callback invoked when the webview performs a navigation to a page. + #[allow(unused_variables)] + fn on_page_load(&mut self, webview: &Webview, payload: &PageLoadPayload<'_>) {} + + /// Callback invoked when the event loop receives a new event. + #[allow(unused_variables)] + fn on_event(&mut self, app: &AppHandle, event: &RunEvent) {} + + /// Callback invoked by [`App::cleanup_before_exit`](crate::App::cleanup_before_exit) right before the process exits. + /// + /// Use it to release resources the OS does not reclaim on its own, such as child processes (sidecars). + /// + /// Unlike [`RunEvent::Exit`], this hook also runs on exit paths that bypass the event loop, + /// e.g. [`AppHandle::restart`] when called on the main thread. On a regular exit, + /// [`Plugin::on_event`] receives [`RunEvent::Exit`] first and then this hook is called. + /// It does **not** run when the process is killed (e.g. by `tauri dev` on rebuild) + /// or when `std::process::exit` is called directly. + /// + /// The plugin store is locked while this hook runs, so it must not call APIs that access + /// other plugins such as [`AppHandle::remove_plugin`] or [`AppHandle::plugin`]. + /// No Tauri API should be used after it returns. + #[allow(unused_variables)] + fn cleanup_before_exit(&mut self, app: &AppHandle) {} + + // TODO: Change this to `run_invoke_handler` in v3 + /// Extend commands to [`crate::Builder::invoke_handler`]. + #[allow(unused_variables)] + fn extend_api(&mut self, invoke: Invoke) -> bool { + false + } +} + +type SetupHook = + dyn FnOnce(&AppHandle, PluginApi) -> Result<(), Box> + Send; +type OnWindowReady = dyn FnMut(Window) + Send; +type OnWebviewReady = dyn FnMut(Webview) + Send; +type OnEvent = dyn FnMut(&AppHandle, &RunEvent) + Send; +type OnCleanupBeforeExit = dyn FnMut(&AppHandle) + Send; +type OnNavigation = dyn Fn(&Webview, &Url) -> bool + Send; +type OnPageLoad = dyn FnMut(&Webview, &PageLoadPayload<'_>) + Send; +type OnDrop = dyn FnOnce(AppHandle) + Send; + +/// A handle to a plugin. +#[derive(Debug)] +#[allow(dead_code)] +pub struct PluginHandle { + name: &'static str, + handle: AppHandle, +} + +impl Clone for PluginHandle { + fn clone(&self) -> Self { + Self { + name: self.name, + handle: self.handle.clone(), + } + } +} + +impl PluginHandle { + /// Returns the application handle. + pub fn app(&self) -> &AppHandle { + &self.handle + } +} + +/// Api exposed to the plugin setup hook. +#[derive(Clone)] +#[allow(dead_code)] +pub struct PluginApi { + handle: AppHandle, + name: &'static str, + raw_config: Arc, + config: C, +} + +impl PluginApi { + /// Returns the plugin configuration. + pub fn config(&self) -> &C { + &self.config + } + + /// Returns the application handle. + pub fn app(&self) -> &AppHandle { + &self.handle + } + + /// Gets the global scope defined on the permissions that are part of the app ACL. + pub fn scope(&self) -> crate::Result> { + self + .handle + .manager + .runtime_authority + .lock() + .unwrap() + .scope_manager + .get_global_scope_typed(&self.handle, self.name) + } +} + +/// Errors that can happen during [`Builder`]. +#[derive(Debug, Clone, Hash, PartialEq, Error)] +#[non_exhaustive] +pub enum BuilderError { + /// Plugin attempted to use a reserved name. + #[error("plugin uses reserved name: {0}")] + ReservedName(String), +} + +const RESERVED_PLUGIN_NAMES: &[&str] = &["core", "tauri"]; + +/// Builds a [`TauriPlugin`]. +/// +/// This Builder offers a more concise way to construct Tauri plugins than implementing the Plugin trait directly. +/// +/// # Conventions +/// +/// When using the Builder Pattern it is encouraged to export a function called `init` that constructs and returns the plugin. +/// While plugin authors can provide every possible way to construct a plugin, +/// sticking to the `init` function convention helps users to quickly identify the correct function to call. +/// +/// ```rust +/// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; +/// +/// pub fn init() -> TauriPlugin { +/// Builder::new("example") +/// .build() +/// } +/// ``` +/// +/// When plugins expose more complex configuration options, it can be helpful to provide a Builder instead: +/// +/// ```rust +/// use tauri::{plugin::{Builder as PluginBuilder, TauriPlugin}, Runtime}; +/// +/// pub struct Builder { +/// option_a: String, +/// option_b: String, +/// option_c: bool +/// } +/// +/// impl Default for Builder { +/// fn default() -> Self { +/// Self { +/// option_a: "foo".to_string(), +/// option_b: "bar".to_string(), +/// option_c: false +/// } +/// } +/// } +/// +/// impl Builder { +/// pub fn new() -> Self { +/// Default::default() +/// } +/// +/// pub fn option_a(mut self, option_a: String) -> Self { +/// self.option_a = option_a; +/// self +/// } +/// +/// pub fn option_b(mut self, option_b: String) -> Self { +/// self.option_b = option_b; +/// self +/// } +/// +/// pub fn option_c(mut self, option_c: bool) -> Self { +/// self.option_c = option_c; +/// self +/// } +/// +/// pub fn build(self) -> TauriPlugin { +/// PluginBuilder::new("example") +/// .setup(move |app_handle, api| { +/// // use the options here to do stuff +/// println!("a: {}, b: {}, c: {}", self.option_a, self.option_b, self.option_c); +/// +/// Ok(()) +/// }) +/// .build() +/// } +/// } +/// ``` +pub struct Builder { + name: &'static str, + invoke_handler: Box>, + setup: Option>>, + js_init_script: Option, + on_navigation: Box>, + on_page_load: Box>, + on_window_ready: Box>, + on_webview_ready: Box>, + on_event: Box>, + on_cleanup_before_exit: Box>, + on_drop: Option>>, + uri_scheme_protocols: HashMap>>, +} + +impl Builder { + /// Creates a new Plugin builder. + pub fn new(name: &'static str) -> Self { + Self { + name, + setup: None, + js_init_script: None, + invoke_handler: Box::new(|_| false), + on_navigation: Box::new(|_, _| true), + on_page_load: Box::new(|_, _| ()), + on_window_ready: Box::new(|_| ()), + on_webview_ready: Box::new(|_| ()), + on_event: Box::new(|_, _| ()), + on_cleanup_before_exit: Box::new(|_| ()), + on_drop: None, + uri_scheme_protocols: Default::default(), + } + } + + /// Defines the JS message handler callback. + /// It is recommended you use the [tauri::generate_handler] to generate the input to this method, as the input type is not considered stable yet. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// #[tauri::command] + /// async fn foobar(app: tauri::AppHandle, window: tauri::Window) -> Result<(), String> { + /// println!("foobar"); + /// + /// Ok(()) + /// } + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .invoke_handler(tauri::generate_handler![foobar]) + /// .build() + /// } + /// + /// ``` + /// [tauri::generate_handler]: ../macro.generate_handler.html + #[must_use] + pub fn invoke_handler(mut self, invoke_handler: F) -> Self + where + F: Fn(Invoke) -> bool + Send + Sync + 'static, + { + self.invoke_handler = Box::new(invoke_handler); + self + } + + /// Sets the provided JavaScript to be run after the global object has been created, + /// but before the HTML document has been parsed and before any other script included by the HTML document is run. + /// + /// The script is wrapped into its own context with `(function () { /* your script here */ })();`, + /// so global variables must be assigned to `window` instead of implicitly declared. + /// + /// Note that calling this function multiple times overrides previous values. + /// + /// This is executed only on the main frame. + /// If you only want to run it in all frames, use [`Self::js_init_script_on_all_frames`] instead. + /// + /// ## Platform-specific + /// + /// - **Windows:** scripts are always added to subframes. + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// [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) + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// const INIT_SCRIPT: &str = r#" + /// if (window.location.origin === 'https://tauri.app') { + /// console.log("hello world from js init script"); + /// + /// window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' }; + /// } + /// "#; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .js_init_script(INIT_SCRIPT) + /// .build() + /// } + /// ``` + #[must_use] + // TODO: Rename to `initialization_script` in v3 + pub fn js_init_script(mut self, js_init_script: impl Into) -> Self { + self.js_init_script = Some(InitializationScript { + script: js_init_script.into(), + for_main_frame_only: true, + }); + self + } + + /// Sets the provided JavaScript to be run after the global object has been created, + /// but before the HTML document has been parsed and before any other script included by the HTML document is run. + /// + /// Since it runs on all top-level document and child frame page navigations, + /// it's recommended to check the `window.location` to guard your script from running on unexpected origins. + /// + /// Note that calling this function multiple times overrides previous values. + /// + /// This is executed on all frames, main frame and also sub frames. + /// If you only want to run it in the main frame, use [`Self::js_init_script`] instead. + /// + /// ## Platform-specific + /// + /// - **Windows:** scripts are always added to subframes. + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// [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 js_init_script_on_all_frames(mut self, js_init_script: impl Into) -> Self { + self.js_init_script = Some(InitializationScript { + script: js_init_script.into(), + for_main_frame_only: false, + }); + self + } + + /// Define a closure that runs when the plugin is registered. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime, Manager}; + /// use std::path::PathBuf; + /// + /// #[derive(Debug, Default)] + /// struct PluginState { + /// dir: Option + /// } + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .setup(|app, api| { + /// app.manage(PluginState::default()); + /// + /// Ok(()) + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn setup(mut self, setup: F) -> Self + where + F: FnOnce(&AppHandle, PluginApi) -> Result<(), Box> + + Send + + 'static, + { + self.setup.replace(Box::new(setup)); + self + } + + /// Callback invoked when the webview tries to navigate to a URL. Returning false cancels the navigation. + /// + /// #Example + /// + /// ``` + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_navigation(|webview, url| { + /// // allow the production URL or localhost on dev + /// url.scheme() == "tauri" || (cfg!(dev) && url.host_str() == Some("localhost")) + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn on_navigation(mut self, on_navigation: F) -> Self + where + F: Fn(&Webview, &Url) -> bool + Send + 'static, + { + self.on_navigation = Box::new(on_navigation); + self + } + + /// Callback invoked when the webview performs a navigation to a page. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_page_load(|webview, payload| { + /// println!("{:?} URL {} in webview {}", payload.event(), payload.url(), webview.label()); + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn on_page_load(mut self, on_page_load: F) -> Self + where + F: FnMut(&Webview, &PageLoadPayload<'_>) + Send + 'static, + { + self.on_page_load = Box::new(on_page_load); + self + } + + /// Callback invoked when the window is created. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_window_ready(|window| { + /// println!("created window {}", window.label()); + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn on_window_ready(mut self, on_window_ready: F) -> Self + where + F: FnMut(Window) + Send + 'static, + { + self.on_window_ready = Box::new(on_window_ready); + self + } + + /// Callback invoked when the webview is created. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_webview_ready(|webview| { + /// println!("created webview {}", webview.label()); + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn on_webview_ready(mut self, on_webview_ready: F) -> Self + where + F: FnMut(Webview) + Send + 'static, + { + self.on_webview_ready = Box::new(on_webview_ready); + self + } + + /// Callback invoked when the event loop receives a new event. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, RunEvent, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_event(|app_handle, event| { + /// match event { + /// RunEvent::ExitRequested { api, .. } => { + /// // Prevents the app from exiting. + /// // This will cause the core thread to continue running in the background even without any open windows. + /// api.prevent_exit(); + /// } + /// // Ignore all other cases. + /// _ => {} + /// } + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn on_event(mut self, on_event: F) -> Self + where + F: FnMut(&AppHandle, &RunEvent) + Send + 'static, + { + self.on_event = Box::new(on_event); + self + } + + /// Callback invoked when the application is performing cleanup before exit. + /// + /// See [`Plugin::cleanup_before_exit`] for details. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_cleanup_before_exit(|app| { + /// // release OS resources such as child processes here + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn on_cleanup_before_exit(mut self, on_cleanup_before_exit: F) -> Self + where + F: FnMut(&AppHandle) + Send + 'static, + { + self.on_cleanup_before_exit = Box::new(on_cleanup_before_exit); + self + } + + /// Callback invoked when the plugin is dropped. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_drop(|app| { + /// println!("plugin has been dropped and is no longer running"); + /// // you can run cleanup logic here + /// }) + /// .build() + /// } + /// ``` + #[must_use] + pub fn on_drop(mut self, on_drop: F) -> Self + where + F: FnOnce(AppHandle) + Send + 'static, + { + self.on_drop.replace(Box::new(on_drop)); + self + } + + /// Registers a URI scheme protocol available to all webviews. + /// + /// Leverages [setURLSchemeHandler](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler) on macOS, + /// [AddWebResourceRequestedFilter](https://docs.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.addwebresourcerequestedfilter?view=webview2-dotnet-1.0.774.44) on Windows + /// and [webkit-web-context-register-uri-scheme](https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebContext.html#webkit-web-context-register-uri-scheme) on Linux. + /// + /// # Known limitations + /// + /// URI scheme protocols are registered when the webview is created. Due to this limitation, if the plugin is registered after a webview has been created, this protocol won't be available. + /// + /// # Arguments + /// + /// * `uri_scheme` The URI scheme to register, such as `example`. + /// * `protocol` the protocol associated with the given URI scheme. It's a function that takes an URL such as `example://localhost/asset.css`. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("myplugin") + /// .register_uri_scheme_protocol("myscheme", |_ctx, req| { + /// http::Response::builder().body(Vec::new()).unwrap() + /// }) + /// .build() + /// } + /// ``` + /// + /// # Warning + /// + /// Pages loaded from a custom protocol will have a different Origin on different platforms. + /// Servers which enforce CORS will need to add the exact same Origin header (or `*`) in `Access-Control-Allow-Origin` + /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the + /// different Origin headers across platforms: + /// + /// - macOS, iOS and Linux: `://localhost/` (so it will be `my-scheme://localhost/path/to/page). + /// - Windows and Android: `http://.localhost/` by default (so it will be `http://my-scheme.localhost/path/to/page`). + /// To use `https` instead of `http`, use [`super::webview::WebviewBuilder::use_https_scheme`]. + #[must_use] + pub fn register_uri_scheme_protocol< + N: Into, + T: Into>, + H: Fn(UriSchemeContext<'_, R>, http::Request>) -> http::Response + + Send + + Sync + + 'static, + >( + mut self, + uri_scheme: N, + protocol_handler: H, + ) -> Self { + self.uri_scheme_protocols.insert( + uri_scheme.into(), + Arc::new(UriSchemeProtocol { + handler: Box::new(move |ctx, request, responder| { + responder.respond(protocol_handler(ctx, request)) + }), + }), + ); + self + } + + /// Similar to [`Self::register_uri_scheme_protocol`] but with an asynchronous responder that allows you + /// to process the request in a separate thread and respond asynchronously. + /// + /// # Arguments + /// + /// * `uri_scheme` The URI scheme to register, such as `example`. + /// * `protocol` the protocol associated with the given URI scheme. It's a function that takes an URL such as `example://localhost/asset.css`. + /// + /// # Examples + /// + /// ```rust + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; + /// + /// fn init() -> TauriPlugin { + /// Builder::new("myplugin") + /// .register_asynchronous_uri_scheme_protocol("app-files", |_ctx, request, responder| { + /// // skip leading `/` + /// let path = request.uri().path()[1..].to_string(); + /// std::thread::spawn(move || { + /// if let Ok(data) = std::fs::read(path) { + /// responder.respond( + /// http::Response::builder() + /// .body(data) + /// .unwrap() + /// ); + /// } else { + /// responder.respond( + /// http::Response::builder() + /// .status(http::StatusCode::BAD_REQUEST) + /// .header(http::header::CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + /// .body("failed to read file".as_bytes().to_vec()) + /// .unwrap() + /// ); + /// } + /// }); + /// }) + /// .build() + /// } + /// ``` + /// + /// # Warning + /// + /// Pages loaded from a custom protocol will have a different Origin on different platforms. + /// Servers which enforce CORS will need to add the exact same Origin header (or `*`) in `Access-Control-Allow-Origin` + /// if you wish to send requests with native `fetch` and `XmlHttpRequest` APIs. Here are the + /// different Origin headers across platforms: + /// + /// - macOS, iOS and Linux: `://localhost/` (so it will be `my-scheme://localhost/path/to/page). + /// - Windows and Android: `http://.localhost/` by default (so it will be `http://my-scheme.localhost/path/to/page`). + /// To use `https` instead of `http`, use [`super::webview::WebviewBuilder::use_https_scheme`]. + #[must_use] + pub fn register_asynchronous_uri_scheme_protocol< + N: Into, + H: Fn(UriSchemeContext<'_, R>, http::Request>, UriSchemeResponder) + Send + Sync + 'static, + >( + mut self, + uri_scheme: N, + protocol_handler: H, + ) -> Self { + self.uri_scheme_protocols.insert( + uri_scheme.into(), + Arc::new(UriSchemeProtocol { + handler: Box::new(protocol_handler), + }), + ); + self + } + + /// Builds the [`TauriPlugin`]. + pub fn try_build(self) -> Result, BuilderError> { + if let Some(&reserved) = RESERVED_PLUGIN_NAMES.iter().find(|&r| r == &self.name) { + return Err(BuilderError::ReservedName(reserved.into())); + } + + Ok(TauriPlugin { + name: self.name, + app: None, + invoke_handler: self.invoke_handler, + setup: self.setup, + js_init_script: self.js_init_script, + on_navigation: self.on_navigation, + on_page_load: self.on_page_load, + on_window_ready: self.on_window_ready, + on_webview_ready: self.on_webview_ready, + on_event: self.on_event, + on_cleanup_before_exit: self.on_cleanup_before_exit, + on_drop: self.on_drop, + uri_scheme_protocols: self.uri_scheme_protocols, + }) + } + + /// Builds the [`TauriPlugin`]. + /// + /// # Panics + /// + /// If the builder returns an error during [`Self::try_build`], then this method will panic. + pub fn build(self) -> TauriPlugin { + self.try_build().expect("valid plugin") + } +} + +/// Plugin struct that is returned by the [`Builder`]. Should only be constructed through the builder. +pub struct TauriPlugin { + name: &'static str, + app: Option>, + invoke_handler: Box>, + setup: Option>>, + js_init_script: Option, + on_navigation: Box>, + on_page_load: Box>, + on_window_ready: Box>, + on_webview_ready: Box>, + on_event: Box>, + on_cleanup_before_exit: Box>, + on_drop: Option>>, + uri_scheme_protocols: HashMap>>, +} + +impl Drop for TauriPlugin { + fn drop(&mut self) { + if let (Some(on_drop), Some(app)) = (self.on_drop.take(), self.app.take()) { + on_drop(app); + } + } +} + +impl Plugin for TauriPlugin { + fn name(&self) -> &'static str { + self.name + } + + fn initialize( + &mut self, + app: &AppHandle, + config: JsonValue, + ) -> Result<(), Box> { + self.app.replace(app.clone()); + if let Some(s) = self.setup.take() { + (s)( + app, + PluginApi { + name: self.name, + handle: app.clone(), + raw_config: Arc::new(config.clone()), + config: serde_json::from_value(config).map_err(|err| { + format!( + "Error deserializing 'plugins.{}' within your Tauri configuration: {err}", + self.name + ) + })?, + }, + )?; + } + + for (uri_scheme, protocol) in &self.uri_scheme_protocols { + app + .manager + .webview + .register_uri_scheme_protocol(uri_scheme, protocol.clone()) + } + Ok(()) + } + + fn initialization_script(&self) -> Option { + self + .js_init_script + .clone() + .map(|initialization_script| initialization_script.script) + } + + fn initialization_script_2(&self) -> Option { + self.js_init_script.clone() + } + + fn window_created(&mut self, window: Window) { + (self.on_window_ready)(window) + } + + fn webview_created(&mut self, webview: Webview) { + (self.on_webview_ready)(webview) + } + + fn on_navigation(&mut self, webview: &Webview, url: &Url) -> bool { + (self.on_navigation)(webview, url) + } + + fn on_page_load(&mut self, webview: &Webview, payload: &PageLoadPayload<'_>) { + (self.on_page_load)(webview, payload) + } + + fn on_event(&mut self, app: &AppHandle, event: &RunEvent) { + (self.on_event)(app, event) + } + + fn cleanup_before_exit(&mut self, app: &AppHandle) { + (self.on_cleanup_before_exit)(app) + } + + fn extend_api(&mut self, invoke: Invoke) -> bool { + (self.invoke_handler)(invoke) + } +} + +/// Plugin collection type. +#[default_runtime(crate::Wry, wry)] +pub(crate) struct PluginStore { + store: Vec>>, +} + +impl fmt::Debug for PluginStore { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let plugins: Vec<&str> = self.store.iter().map(|plugins| plugins.name()).collect(); + f.debug_struct("PluginStore") + .field("plugins", &plugins) + .finish() + } +} + +impl Default for PluginStore { + fn default() -> Self { + Self { store: Vec::new() } + } +} + +impl PluginStore { + /// Adds a plugin to the store. + /// + /// Returns `true` if a plugin with the same name is already in the store. + pub fn register(&mut self, plugin: Box>) -> bool { + let len = self.store.len(); + self.store.retain(|p| p.name() != plugin.name()); + let result = len != self.store.len(); + self.store.push(plugin); + result + } + + /// Removes the plugin with the given name from the store. + pub fn unregister(&mut self, plugin: &str) -> bool { + let len = self.store.len(); + self.store.retain(|p| p.name() != plugin); + len != self.store.len() + } + + /// Initializes the given plugin. + pub(crate) fn initialize( + &self, + plugin: &mut Box>, + app: &AppHandle, + config: &PluginConfig, + ) -> crate::Result<()> { + initialize(plugin, app, config) + } + + /// Initializes all plugins in the store. + pub(crate) fn initialize_all( + &mut self, + app: &AppHandle, + config: &PluginConfig, + ) -> crate::Result<()> { + self + .store + .iter_mut() + .try_for_each(|plugin| initialize(plugin, app, config)) + } + + /// Generates an initialization script from all plugins in the store. + pub(crate) fn initialization_script(&self) -> Vec { + self + .store + .iter() + .filter_map(|p| p.initialization_script_2()) + .map( + |InitializationScript { + script, + for_main_frame_only, + }| InitializationScript { + script: format!("(function () {{ {script} }})();"), + for_main_frame_only, + }, + ) + .collect() + } + + /// Runs the created hook for all plugins in the store. + pub(crate) fn window_created(&mut self, window: Window) { + self.store.iter_mut().for_each(|plugin| { + #[cfg(feature = "tracing")] + let _span = tracing::trace_span!("plugin::hooks::created", name = plugin.name()).entered(); + plugin.window_created(window.clone()) + }) + } + + /// Runs the webview created hook for all plugins in the store. + pub(crate) fn webview_created(&mut self, webview: Webview) { + self + .store + .iter_mut() + .for_each(|plugin| plugin.webview_created(webview.clone())) + } + + pub(crate) fn on_navigation(&mut self, webview: &Webview, url: &Url) -> bool { + for plugin in self.store.iter_mut() { + #[cfg(feature = "tracing")] + let _span = + tracing::trace_span!("plugin::hooks::on_navigation", name = plugin.name()).entered(); + if !plugin.on_navigation(webview, url) { + return false; + } + } + true + } + + /// Runs the on_page_load hook for all plugins in the store. + pub(crate) fn on_page_load(&mut self, webview: &Webview, payload: &PageLoadPayload<'_>) { + self.store.iter_mut().for_each(|plugin| { + #[cfg(feature = "tracing")] + let _span = + tracing::trace_span!("plugin::hooks::on_page_load", name = plugin.name()).entered(); + plugin.on_page_load(webview, payload) + }) + } + + /// Runs the on_event hook for all plugins in the store. + pub(crate) fn on_event(&mut self, app: &AppHandle, event: &RunEvent) { + self + .store + .iter_mut() + .for_each(|plugin| plugin.on_event(app, event)) + } + + /// Runs the cleanup_before_exit hook for all plugins in the store. + pub(crate) fn cleanup_before_exit(&mut self, app: &AppHandle) { + self.store.iter_mut().for_each(|plugin| { + #[cfg(feature = "tracing")] + let _span = + tracing::trace_span!("plugin::hooks::cleanup_before_exit", name = plugin.name()).entered(); + plugin.cleanup_before_exit(app) + }) + } + + /// Runs the plugin [`Plugin::extend_api`] hook if it exists. Returns whether the invoke message was handled or not. + /// + /// The message is not handled when the plugin exists **and** the command does not. + pub(crate) fn run_invoke_handler(&mut self, plugin: &str, invoke: Invoke) -> bool { + for p in self.store.iter_mut() { + if p.name() == plugin { + #[cfg(feature = "tracing")] + let _span = tracing::trace_span!("plugin::hooks::ipc", name = plugin).entered(); + return p.extend_api(invoke); + } + } + invoke.resolver.reject(format!("plugin {plugin} not found")); + true + } +} + +#[cfg_attr(feature = "tracing", tracing::instrument(name = "plugin::hooks::initialize", skip(plugin, app), fields(name = plugin.name())))] +fn initialize( + plugin: &mut Box>, + app: &AppHandle, + config: &PluginConfig, +) -> crate::Result<()> { + plugin + .initialize( + app, + config.0.get(plugin.name()).cloned().unwrap_or_default(), + ) + .map_err(|e| Error::PluginInitialization(plugin.name().to_string(), e.to_string())) +} + +/// Permission state. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[cfg_attr(feature = "specta", derive(specta::Type))] +pub enum PermissionState { + /// Permission access has been granted. + Granted, + /// Permission access has been denied. + Denied, + /// Permission must be requested. + #[default] + Prompt, + /// Permission must be requested, but you must explain to the user why your app needs that permission. **Android only**. + PromptWithRationale, +} + +impl std::fmt::Display for PermissionState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Granted => write!(f, "granted"), + Self::Denied => write!(f, "denied"), + Self::Prompt => write!(f, "prompt"), + Self::PromptWithRationale => write!(f, "prompt-with-rationale"), + } + } +} + +impl Serialize for PermissionState { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +impl<'de> Deserialize<'de> for PermissionState { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = ::deserialize(deserializer)?; + match s.to_lowercase().as_str() { + "granted" => Ok(Self::Granted), + "denied" => Ok(Self::Denied), + "prompt" => Ok(Self::Prompt), + "prompt-with-rationale" => Ok(Self::PromptWithRationale), + _ => Err(DeError::custom(format!("unknown permission state '{s}'"))), + } + } +} + +#[cfg(test)] +mod tests { + use super::{Builder, TauriPlugin}; + use crate::test::{MockRuntime, mock_builder, mock_context, noop_assets}; + use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }; + + #[test] + fn builder_cleanup_before_exit_hook_runs_on_app_cleanup() { + let called = Arc::new(AtomicBool::new(false)); + let called_ = called.clone(); + let plugin: TauriPlugin = Builder::new("cleanup-test") + .on_cleanup_before_exit(move |_app| called_.store(true, Ordering::SeqCst)) + .build(); + + let app = mock_builder() + .plugin(plugin) + .build(mock_context(noop_assets())) + .unwrap(); + + app.cleanup_before_exit(); + + assert!(called.load(Ordering::SeqCst)); + } +} diff --git a/crates/tauri/src/plugin/mobile.rs b/crates/tauri/src/plugin/mobile.rs new file mode 100644 index 0000000..c9187b0 --- /dev/null +++ b/crates/tauri/src/plugin/mobile.rs @@ -0,0 +1,511 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{PluginApi, PluginHandle}; + +use crate::{AppHandle, Runtime, ipc::Channel}; +#[cfg(target_os = "android")] +use crate::{ + runtime::RuntimeHandle, + sealed::{ManagerBase, RuntimeOrDispatch}, +}; + +#[cfg(mobile)] +use std::sync::atomic::{AtomicI32, Ordering}; +#[cfg(mobile)] +use tokio::sync::oneshot; + +use serde::{Serialize, de::DeserializeOwned}; + +use std::{ + collections::HashMap, + fmt, + sync::{Mutex, OnceLock, mpsc::channel}, +}; + +type PluginResponse = Result; + +type PendingPluginCallHandler = Box; + +#[cfg(mobile)] +static PENDING_PLUGIN_CALLS_ID: AtomicI32 = AtomicI32::new(0); +static PENDING_PLUGIN_CALLS: OnceLock>> = + OnceLock::new(); +static CHANNELS: OnceLock>>> = OnceLock::new(); + +/// Possible errors when invoking a plugin. +#[derive(Debug, thiserror::Error)] +pub enum PluginInvokeError { + /// Failed to reach platform webview handle. + #[error("the webview is unreachable")] + UnreachableWebview, + /// JNI error. + #[cfg(target_os = "android")] + #[error("jni error: {0}")] + Jni(#[from] jni::errors::Error), + /// Error returned from direct mobile plugin invoke. + #[error(transparent)] + InvokeRejected(#[from] ErrorResponse), + /// Failed to deserialize response. + #[error("failed to deserialize response: {0}")] + CannotDeserializeResponse(serde_json::Error), + /// Failed to serialize request payload. + #[error("failed to serialize payload: {0}")] + CannotSerializePayload(serde_json::Error), +} + +pub(crate) fn register_channel(channel: Channel) { + CHANNELS + .get_or_init(Default::default) + .lock() + .unwrap() + .insert(channel.id(), channel); +} + +/// Glue between Rust and the Kotlin code that sends the plugin response back. +#[cfg(target_os = "android")] +pub fn handle_android_plugin_response( + env: &mut jni::JNIEnv<'_>, + id: i32, + success: jni::objects::JString<'_>, + error: jni::objects::JString<'_>, +) { + let (payload, is_ok): (serde_json::Value, bool) = match ( + env + .is_same_object(&success, jni::objects::JObject::default()) + .unwrap_or_default(), + env + .is_same_object(&error, jni::objects::JObject::default()) + .unwrap_or_default(), + ) { + // both null + (true, true) => (serde_json::Value::Null, true), + // error null + (false, true) => ( + serde_json::from_str(env.get_string(&success).unwrap().to_str().unwrap()).unwrap(), + true, + ), + // success null + (true, false) => ( + serde_json::from_str(env.get_string(&error).unwrap().to_str().unwrap()).unwrap(), + false, + ), + // both are set - impossible in the Kotlin code + (false, false) => unreachable!(), + }; + + // Drop the lock before invoking the handler: it delivers the command response + // to the webview (which can block on the UI thread), and holding + // PENDING_PLUGIN_CALLS across that call deadlocks a concurrent `run_command`. + let handler = PENDING_PLUGIN_CALLS + .get_or_init(Default::default) + .lock() + .unwrap() + .remove(&id); + if let Some(handler) = handler { + handler(if is_ok { Ok(payload) } else { Err(payload) }); + } +} + +/// Glue between Rust and the Kotlin code that sends the channel data. +#[cfg(target_os = "android")] +pub fn send_channel_data( + env: &mut jni::JNIEnv<'_>, + channel_id: i64, + data_str: jni::objects::JString<'_>, +) { + let data: serde_json::Value = + serde_json::from_str(env.get_string(&data_str).unwrap().to_str().unwrap()).unwrap(); + + // Clone the channel out and drop the lock before send(): send() can block + // delivering to the webview, and holding CHANNELS across it deadlocks a + // concurrent channel registration/send. + let channel = CHANNELS + .get_or_init(Default::default) + .lock() + .unwrap() + .get(&(channel_id as u32)) + .cloned(); + if let Some(channel) = channel { + let _ = channel.send(data); + } +} + +/// Error response from the Kotlin and Swift backends. +#[derive(Debug, thiserror::Error, Clone, serde::Deserialize)] +pub struct ErrorResponse { + /// Error code. + pub code: Option, + /// Error message. + pub message: Option, + /// Optional error data. + #[serde(flatten)] + pub data: T, +} + +impl fmt::Display for ErrorResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(code) = &self.code { + write!(f, "[{code}]")?; + if self.message.is_some() { + write!(f, " - ")?; + } + } + if let Some(message) = &self.message { + write!(f, "{message}")?; + } + Ok(()) + } +} + +impl PluginApi { + /// Registers an iOS plugin. + #[cfg(all(target_os = "ios", feature = "wry"))] + pub fn register_ios_plugin( + &self, + init_fn: unsafe fn() -> *const std::ffi::c_void, + ) -> Result, PluginInvokeError> { + if let Some(webview) = self.handle.manager.webviews().values().next() { + let (tx, rx) = channel(); + let name = self.name; + let config = self.raw_config.clone(); + webview + .with_webview(move |w| { + unsafe { + crate::ios::register_plugin( + &name.into(), + init_fn(), + &serde_json::to_string(&config).unwrap().as_str().into(), + w.inner() as _, + ) + }; + tx.send(()).unwrap(); + }) + .map_err(|_| PluginInvokeError::UnreachableWebview)?; + rx.recv().unwrap(); + } else { + unsafe { + crate::ios::register_plugin( + &self.name.into(), + init_fn(), + &serde_json::to_string(&self.raw_config) + .unwrap() + .as_str() + .into(), + std::ptr::null(), + ) + }; + } + Ok(PluginHandle { + name: self.name, + handle: self.handle.clone(), + }) + } + + /// Registers an Android plugin. + #[cfg(target_os = "android")] + pub fn register_android_plugin( + &self, + plugin_identifier: &str, + class_name: &str, + ) -> Result, PluginInvokeError> { + use jni::{JNIEnv, errors::Error as JniError, objects::JObject}; + + fn initialize_plugin( + env: &mut JNIEnv<'_>, + activity: &JObject<'_>, + webview: &JObject<'_>, + runtime_handle: &R::Handle, + plugin_name: &'static str, + plugin_class: String, + plugin_config: &serde_json::Value, + ) -> Result<(), JniError> { + // instantiate plugin + let plugin_class = runtime_handle.find_class(env, activity, plugin_class)?; + let plugin = env.new_object( + plugin_class, + "(Landroid/app/Activity;)V", + &[activity.into()], + )?; + + // load plugin + + let plugin_manager = env + .call_method( + activity, + "getPluginManager", + "()Lapp/tauri/plugin/PluginManager;", + &[], + )? + .l()?; + + let plugin_name = env.new_string(plugin_name)?; + let config = env.new_string(serde_json::to_string(plugin_config).unwrap())?; + env.call_method( + plugin_manager, + "load", + "(Landroid/webkit/WebView;Ljava/lang/String;Lapp/tauri/plugin/Plugin;Ljava/lang/String;)V", + &[ + webview.into(), + (&plugin_name).into(), + (&plugin).into(), + (&config).into(), + ], + )?; + + Ok(()) + } + + let plugin_class = format!("{}/{}", plugin_identifier.replace('.', "/"), class_name); + let plugin_name = self.name; + let plugin_config = self.raw_config.clone(); + let runtime_handle = self.handle.runtime_handle.clone(); + let (tx, rx) = channel(); + self + .handle + .runtime_handle + .run_on_android_context(move |env, activity, webview| { + let result = initialize_plugin::( + env, + activity, + webview, + &runtime_handle, + plugin_name, + plugin_class, + &plugin_config, + ); + tx.send(result).unwrap(); + }); + + rx.recv().unwrap()?; + + Ok(PluginHandle { + name: self.name, + handle: self.handle.clone(), + }) + } +} + +impl PluginHandle { + /// Executes the given mobile command. + /// This is an async optimized variant of run_mobile_plugin + pub async fn run_mobile_plugin_async( + &self, + command: impl AsRef, + payload: impl Serialize, + ) -> Result { + let (tx, rx) = oneshot::channel(); + // the closure is an FnOnce but on Android we need to clone it (error handling) + let tx = std::sync::Arc::new(std::sync::Mutex::new(Some(tx))); + run_command( + self.name, + &self.handle, + command, + serde_json::to_value(payload).map_err(PluginInvokeError::CannotSerializePayload)?, + move |response| { + tx.lock().unwrap().take().unwrap().send(response).unwrap(); + }, + )?; + + let response = rx.await.unwrap(); + + match response { + Ok(r) => serde_json::from_value(r).map_err(PluginInvokeError::CannotDeserializeResponse), + Err(r) => Err( + serde_json::from_value::(r) + .map(Into::into) + .map_err(PluginInvokeError::CannotDeserializeResponse)?, + ), + } + } + + /// Executes the given mobile command. + pub fn run_mobile_plugin( + &self, + command: impl AsRef, + payload: impl Serialize, + ) -> Result { + let (tx, rx) = channel(); + run_command( + self.name, + &self.handle, + command, + serde_json::to_value(payload).map_err(PluginInvokeError::CannotSerializePayload)?, + move |response| { + tx.send(response).unwrap(); + }, + )?; + + let response = rx.recv().unwrap(); + match response { + Ok(r) => serde_json::from_value(r).map_err(PluginInvokeError::CannotDeserializeResponse), + Err(r) => Err( + serde_json::from_value::(r) + .map(Into::into) + .map_err(PluginInvokeError::CannotDeserializeResponse)?, + ), + } + } +} + +#[cfg(target_os = "ios")] +pub(crate) fn run_command, F: FnOnce(PluginResponse) + Send + 'static>( + name: &str, + _handle: &AppHandle, + command: C, + payload: serde_json::Value, + handler: F, +) -> Result<(), PluginInvokeError> { + use std::{ + ffi::CStr, + os::raw::{c_char, c_int, c_ulonglong}, + }; + + let id: i32 = PENDING_PLUGIN_CALLS_ID.fetch_add(1, Ordering::Relaxed); + PENDING_PLUGIN_CALLS + .get_or_init(Default::default) + .lock() + .unwrap() + .insert(id, Box::new(handler)); + + unsafe { + extern "C" fn plugin_command_response_handler( + id: c_int, + success: c_int, + payload: *const c_char, + ) { + let payload = unsafe { + assert!(!payload.is_null()); + CStr::from_ptr(payload) + }; + + let handler = PENDING_PLUGIN_CALLS + .get_or_init(Default::default) + .lock() + .unwrap() + .remove(&id); + if let Some(handler) = handler { + let json = payload.to_str().unwrap(); + match serde_json::from_str(json) { + Ok(payload) => { + handler(if success == 1 { + Ok(payload) + } else { + Err(payload) + }); + } + Err(err) => { + handler(Err(format!("{err}, data: {}", json).into())); + } + } + } + } + + extern "C" fn send_channel_data_handler(id: c_ulonglong, payload: *const c_char) { + let payload = unsafe { + assert!(!payload.is_null()); + CStr::from_ptr(payload) + }; + + // Clone the channel out and drop the lock before send(): send() can block + // delivering to the webview, and holding CHANNELS across it deadlocks a + // concurrent channel registration/send. + let channel = CHANNELS + .get_or_init(Default::default) + .lock() + .unwrap() + .get(&(id as u32)) + .cloned(); + if let Some(channel) = channel { + let payload: serde_json::Value = serde_json::from_str(payload.to_str().unwrap()).unwrap(); + let _ = channel.send(payload); + } + } + + crate::ios::run_plugin_command( + id, + &name.into(), + &command.as_ref().into(), + &serde_json::to_string(&payload).unwrap().as_str().into(), + crate::ios::PluginMessageCallback(plugin_command_response_handler), + crate::ios::ChannelSendDataCallback(send_channel_data_handler), + ); + } + + Ok(()) +} + +#[cfg(target_os = "android")] +pub(crate) fn run_command< + R: Runtime, + C: AsRef, + F: FnOnce(PluginResponse) + Send + Clone + 'static, +>( + name: &str, + handle: &AppHandle, + command: C, + payload: serde_json::Value, + handler: F, +) -> Result<(), PluginInvokeError> { + use jni::{JNIEnv, errors::Error as JniError, objects::JObject}; + + fn run( + id: i32, + plugin: &str, + command: String, + payload: &serde_json::Value, + env: &mut JNIEnv<'_>, + activity: &JObject<'_>, + ) -> Result<(), JniError> { + let plugin = env.new_string(plugin)?; + let command = env.new_string(&command)?; + let data = env.new_string(serde_json::to_string(payload).unwrap())?; + let plugin_manager = env + .call_method( + activity, + "getPluginManager", + "()Lapp/tauri/plugin/PluginManager;", + &[], + )? + .l()?; + + env.call_method( + plugin_manager, + "runCommand", + "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", + &[ + id.into(), + (&plugin).into(), + (&command).into(), + (&data).into(), + ], + )?; + + Ok(()) + } + + let handle = match handle.runtime() { + RuntimeOrDispatch::Runtime(r) => r.handle(), + RuntimeOrDispatch::RuntimeHandle(h) => h, + _ => unreachable!(), + }; + + let id: i32 = PENDING_PLUGIN_CALLS_ID.fetch_add(1, Ordering::Relaxed); + let plugin_name = name.to_string(); + let command = command.as_ref().to_string(); + + PENDING_PLUGIN_CALLS + .get_or_init(Default::default) + .lock() + .unwrap() + .insert(id, Box::new(handler.clone())); + + handle.run_on_android_context(move |env, activity, _webview| { + if let Err(e) = run(id, &plugin_name, command, &payload, env, activity) { + handler(Err(e.to_string().into())); + } + }); + + Ok(()) +} diff --git a/crates/tauri/src/process.rs b/crates/tauri/src/process.rs new file mode 100644 index 0000000..c4218a9 --- /dev/null +++ b/crates/tauri/src/process.rs @@ -0,0 +1,130 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Types and functions related to child processes management. + +use crate::Env; + +use std::path::PathBuf; + +/// Finds the current running binary's path. +/// +/// With exception to any following platform-specific behavior, the path is cached as soon as +/// possible, and then used repeatedly instead of querying for a new path every time this function +/// is called. +/// +/// # Platform-specific behavior +/// +/// ## Linux +/// +/// On Linux, this function will **attempt** to detect if it's currently running from a +/// valid [AppImage] and use that path instead. +/// +/// ## macOS +/// +/// On `macOS`, this function will return an error if the original path contained any symlinks +/// due to less protection on macOS regarding symlinks. This behavior can be disabled by setting the +/// `process-relaunch-dangerous-allow-symlink-macos` feature, although it is *highly discouraged*. +/// +/// # Security +/// +/// See [`tauri_utils::platform::current_exe`] for possible security implications. +/// +/// # Examples +/// +/// ```rust,no_run +/// use tauri::{process::current_binary, Env, Manager}; +/// let current_binary_path = current_binary(&Env::default()).unwrap(); +/// +/// tauri::Builder::default() +/// .setup(|app| { +/// let current_binary_path = current_binary(&app.env())?; +/// Ok(()) +/// }); +/// ``` +/// +/// [AppImage]: https://appimage.org/ +pub fn current_binary(_env: &Env) -> std::io::Result { + // if we are running from an AppImage, we ONLY want the set AppImage path + #[cfg(target_os = "linux")] + if let Some(app_image_path) = &_env.appimage { + return Ok(PathBuf::from(app_image_path)); + } + + tauri_utils::platform::current_exe() +} + +/// Restarts the currently running binary. +/// +/// See [`current_binary`] for platform specific behavior, and +/// [`tauri_utils::platform::current_exe`] for possible security implications. +/// +/// # Examples +/// +/// ```rust,no_run +/// use tauri::{process::restart, Env, Manager}; +/// +/// tauri::Builder::default() +/// .setup(|app| { +/// restart(&app.env()); +/// Ok(()) +/// }); +/// ``` +pub fn restart(env: &Env) -> ! { + use std::process::{Command, exit}; + + if let Ok(path) = current_binary(env) { + // on macOS on updates the binary name might have changed + // so we'll read the Contents/Info.plist file to determine the binary path + #[cfg(target_os = "macos")] + restart_macos_app(&path, env); + + if let Err(e) = Command::new(path).args(env.args_os.iter().skip(1)).spawn() { + log::error!("failed to restart app: {e}"); + } + } + + exit(0); +} + +#[cfg(target_os = "macos")] +fn restart_macos_app(current_binary: &std::path::Path, env: &Env) { + use std::process::{Command, exit}; + + if let Some(macos_directory) = current_binary.parent() { + if macos_directory.components().next_back() + != Some(std::path::Component::Normal(std::ffi::OsStr::new("MacOS"))) + { + return; + } + + if let Some(contents_directory) = macos_directory.parent() { + if contents_directory.components().next_back() + != Some(std::path::Component::Normal(std::ffi::OsStr::new( + "Contents", + ))) + { + return; + } + + if let Ok(info_plist) = + plist::from_file::<_, plist::Dictionary>(contents_directory.join("Info.plist")) + { + if let Some(binary_name) = info_plist + .get("CFBundleExecutable") + .and_then(|v| v.as_string()) + { + if let Err(e) = Command::new(macos_directory.join(binary_name)) + .args(env.args_os.iter().skip(1)) + .spawn() + { + log::error!("failed to restart app: {e}"); + } + + exit(0); + } + } + } + } +} diff --git a/crates/tauri/src/protocol/asset.rs b/crates/tauri/src/protocol/asset.rs new file mode 100644 index 0000000..7d488fa --- /dev/null +++ b/crates/tauri/src/protocol/asset.rs @@ -0,0 +1,309 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{path::SafePathBuf, scope, webview::UriSchemeProtocolHandler}; +use http::{Request, Response, header::*, status::StatusCode}; +use http_range::HttpRange; +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(move |_, request, responder| { + let scope = scope.clone(); + let window_origin = window_origin.clone(); + crate::async_runtime::spawn(async move { + match get_response(request, &scope, &window_origin).await { + Ok(response) => responder.respond(response), + Err(e) => responder.respond( + http::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + .header("Access-Control-Allow-Origin", &window_origin) + .body(e.to_string().into_bytes()) + .unwrap(), + ), + } + }); + }) +} + +async fn get_response( + request: Request>, + scope: &scope::fs::Scope, + window_origin: &str, +) -> Result>, Box> { + // skip leading `/` + let path = + percent_encoding::percent_decode(&request.uri().path().as_bytes()[1..]).decode_utf8_lossy(); + + let mut resp = Response::builder().header("Access-Control-Allow-Origin", window_origin); + + let safe_path = match SafePathBuf::new(path.as_ref().into()) { + Ok(path) => path, + Err(e) => { + log::error!("asset protocol path \"{path}\" is not valid: {e}"); + return resp.status(403).body(Vec::new().into()).map_err(Into::into); + } + }; + + if !scope.is_allowed(&safe_path) { + log::error!("asset protocol not configured to allow the path: {path}"); + return resp.status(403).body(Vec::new().into()).map_err(Into::into); + } + + // Separate block for easier error handling + let mut file = match File::open(&safe_path).await { + Ok(file) => file, + Err(e) => { + #[cfg(target_os = "android")] + { + if path.starts_with("/storage/emulated/0/Android/data/") { + log::error!( + "Failed to open Android external storage file '{path}': {e}. This may be due to missing storage permissions." + ); + } + } + return if e.kind() == std::io::ErrorKind::NotFound { + log::error!("File does not exist at path: {path}"); + return resp.status(404).body(Vec::new().into()).map_err(Into::into); + } else if e.kind() == std::io::ErrorKind::PermissionDenied { + log::error!("Missing OS permission to access path \"{path}\": {e}"); + return resp.status(403).body(Vec::new().into()).map_err(Into::into); + } else { + Err(e.into()) + }; + } + }; + + let len = file.metadata().await?.len(); + let (mime_type, read_bytes) = { + // get file mime type + 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).await?; + file.rewind().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 }, + ) + }; + + resp = resp.header(CONTENT_TYPE, &mime_type); + + // handle 206 (partial range) http requests + let response = + if let Some(range_header) = request.headers().get("range").and_then(|r| r.to_str().ok()) { + resp = resp.header(ACCEPT_RANGES, "bytes"); + resp = resp.header(ACCESS_CONTROL_EXPOSE_HEADERS, "content-range"); + + let not_satisfiable = || { + Response::builder() + .status(StatusCode::RANGE_NOT_SATISFIABLE) + .header(CONTENT_RANGE, format!("bytes */{len}")) + .body(vec![].into()) + .map_err(Into::into) + }; + + // parse range header + let ranges = if let Ok(ranges) = HttpRange::parse(range_header, len) { + ranges + .iter() + // map the output to spec range , example: 0-499 + .map(|r| (r.start, r.start + r.length - 1)) + .collect::>() + } else { + return not_satisfiable(); + }; + + /// The Maximum bytes we send in one range + const MAX_LEN: u64 = 1000 * 1024; + + // single-part range header + if ranges.len() == 1 { + let &(start, mut end) = ranges.first().unwrap(); + + // check if a range is not satisfiable + // + // this should be already taken care of by the range parsing library + // but checking here again for extra assurance + if start >= len || end >= len || end < start { + return not_satisfiable(); + } + + // adjust end byte for MAX_LEN + end = start + (end - start).min(len - start).min(MAX_LEN - 1); + + // calculate number of bytes needed to be read + let nbytes = end + 1 - start; + + 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?; + buf + }; + + resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); + resp = resp.header(CONTENT_LENGTH, end + 1 - start); + resp = resp.status(StatusCode::PARTIAL_CONTENT); + resp.body(buf.into()) + } else { + let ranges = ranges + .iter() + .filter_map(|&(start, mut end)| { + // filter out unsatisfiable ranges + // + // this should be already taken care of by the range parsing library + // but checking here again for extra assurance + if start >= len || end >= len || end < start { + None + } else { + // adjust end byte for MAX_LEN + end = start + (end - start).min(len - start).min(MAX_LEN - 1); + Some((start, end)) + } + }) + .collect::>(); + + let boundary = random_boundary(); + let boundary_sep = format!("\r\n--{boundary}\r\n"); + let boundary_closer = format!("\r\n--{boundary}--\r\n"); + + // `Builder::header` appends, we want to replace the file mime type set earlier + if let Some(headers) = resp.headers_mut() { + headers.insert( + CONTENT_TYPE, + HeaderValue::from_str(&format!("multipart/byteranges; boundary={boundary}"))?, + ); + } + + 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?; + + // 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?; + + // write the separator to indicate the start of the range body + buf.write_all("\r\n".as_bytes()).await?; + + // calculate number of bytes needed to be read + let nbytes = end + 1 - start; + + buf.reserve(nbytes as usize); + file.seek(SeekFrom::Start(start)).await?; + (&mut file).take(nbytes).read_to_end(&mut buf).await?; + } + // all ranges have been written, write the closing boundary + buf.write_all(boundary_closer.as_bytes()).await?; + + buf + }; + + resp = resp.status(StatusCode::PARTIAL_CONTENT); + resp.body(buf.into()) + } + } else if request.method() == http::Method::HEAD { + // if the HEAD method is used, we should not return a body + resp = resp.header(CONTENT_LENGTH, len); + resp.body(Vec::new().into()) + } else { + // avoid reading the file if we already read it + // as part of mime type detection + let buf = if let Some(b) = read_bytes { + b + } else { + let mut local_buf = Vec::with_capacity(len as usize); + file.read_to_end(&mut local_buf).await?; + local_buf + }; + resp = resp.header(CONTENT_LENGTH, len); + resp.body(buf.into()) + }; + + response.map_err(Into::into) +} + +fn random_boundary() -> String { + let mut x = [0_u8; 30]; + getrandom::fill(&mut x).expect("failed to get random bytes"); + (x[..]) + .iter() + .map(|&x| format!("{x:x}")) + .fold(String::new(), |mut a, x| { + a.push_str(x.as_str()); + a + }) +} + +#[cfg(test)] +mod tests { + use super::get_response; + use crate::scope::fs::Scope; + use http::{Request, header::CONTENT_TYPE, status::StatusCode}; + use tauri_utils::config::FsScope; + + #[test] + fn multi_range_request() { + let app = crate::test::mock_app(); + + let path = std::env::temp_dir().join(format!( + "tauri-asset-protocol-multi-range-{}.bin", + std::process::id() + )); + std::fs::write(&path, vec![b'a'; 1000]).unwrap(); + + let scope = Scope::new(&app, &FsScope::default()).unwrap(); + scope.allow_file(&path).unwrap(); + + let encoded_path = percent_encoding::percent_encode( + path.to_string_lossy().as_bytes(), + percent_encoding::NON_ALPHANUMERIC, + ) + .to_string(); + + let request = Request::builder() + .uri(format!("asset://localhost/{encoded_path}")) + .header("range", "bytes=0-9, 20-29") + .body(Vec::new()) + .unwrap(); + + let response = + crate::async_runtime::block_on(get_response(request, &scope, "http://tauri.localhost")) + .unwrap(); + std::fs::remove_file(&path).unwrap(); + + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + + let content_types = response + .headers() + .get_all(CONTENT_TYPE) + .iter() + .map(|v| v.to_str().unwrap().to_string()) + .collect::>(); + assert_eq!(content_types.len(), 1); + + let boundary = content_types[0] + .strip_prefix("multipart/byteranges; boundary=") + .unwrap(); + assert!( + response + .body() + .ends_with(format!("\r\n--{boundary}--\r\n").as_bytes()) + ); + } +} diff --git a/crates/tauri/src/protocol/isolation.rs b/crates/tauri/src/protocol/isolation.rs new file mode 100644 index 0000000..e140715 --- /dev/null +++ b/crates/tauri/src/protocol/isolation.rs @@ -0,0 +1,112 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::Assets; +use http::header::CONTENT_TYPE; +use serialize_to_javascript::Template; +use tauri_utils::{ + assets::EmbeddedAssets, + config::{Csp, HeaderAddition}, +}; + +use std::sync::Arc; + +use crate::{ + Runtime, + manager::{AppManager, set_csp, webview::PROCESS_IPC_MESSAGE_FN}, + webview::UriSchemeProtocolHandler, +}; + +pub fn get( + manager: Arc>, + schema: &str, + assets: Arc, + aes_gcm_key: [u8; 32], + window_origin: String, + use_https_scheme: bool, +) -> UriSchemeProtocolHandler { + let frame_src = if cfg!(any(windows, target_os = "android")) { + let https = if use_https_scheme { "https" } else { "http" }; + format!("{https}://{schema}.localhost") + } else { + format!("{schema}:") + }; + + let assets = assets as Arc>; + + Box::new(move |_, request, responder| { + let response = match request_to_path(&request).as_str() { + "index.html" => match assets.get(&"index.html".into()) { + Some(asset) => { + let mut asset = String::from_utf8_lossy(asset.as_ref()).into_owned(); + let csp_map = set_csp( + &mut asset, + &assets, + &"index.html".into(), + &manager, + Csp::Policy(format!("default-src 'none'; frame-src {frame_src}")), + ); + let csp = Csp::DirectiveMap(csp_map).to_string(); + + let template = tauri_utils::pattern::isolation::IsolationJavascriptRuntime { + runtime_aes_gcm_key: &aes_gcm_key, + origin: window_origin.clone(), + process_ipc_message_fn: PROCESS_IPC_MESSAGE_FN, + }; + match template.render(asset.as_ref(), &Default::default()) { + Ok(asset) => http::Response::builder() + .add_configured_headers(manager.config.app.security.headers.as_ref()) + .header(CONTENT_TYPE, mime::TEXT_HTML.as_ref()) + .header("Content-Security-Policy", csp) + .body(asset.into_string().into_bytes()), + Err(_) => http::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.as_ref()) + .body(Vec::new()), + } + } + + None => http::Response::builder() + .status(http::StatusCode::NOT_FOUND) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.as_ref()) + .body(Vec::new()), + }, + _ => http::Response::builder() + .status(http::StatusCode::NOT_FOUND) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.as_ref()) + .body(Vec::new()), + }; + + if let Ok(r) = response { + responder.respond(r); + } else { + responder.respond( + http::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body("failed to get response".as_bytes()) + .unwrap(), + ); + } + }) +} + +fn request_to_path(request: &http::Request>) -> String { + let path = request + .uri() + .path() + .trim_start_matches('/') + .trim_end_matches('/'); + + let path = percent_encoding::percent_decode(path.as_bytes()) + .decode_utf8_lossy() + .to_string(); + + if path.is_empty() { + // if the url has no path, we should load `index.html` + "index.html".to_string() + } else { + // skip leading `/` + path.chars().skip(1).collect() + } +} diff --git a/crates/tauri/src/protocol/mod.rs b/crates/tauri/src/protocol/mod.rs new file mode 100644 index 0000000..e642079 --- /dev/null +++ b/crates/tauri/src/protocol/mod.rs @@ -0,0 +1,53 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Custom protocol handlers + +use percent_encoding::{AsciiSet, NON_ALPHANUMERIC}; + +#[cfg(feature = "protocol-asset")] +pub mod asset; +#[cfg(feature = "isolation")] +pub mod isolation; +pub mod tauri; + +/// Percent-encoding set matching JavaScript's `encodeURIComponent`, +/// which leaves `A-Z a-z 0-9 - _ . ! ~ * ' ( )` unescaped. +pub(crate) const ENCODE_URI_COMPONENT: &AsciiSet = &NON_ALPHANUMERIC + .remove(b'-') + .remove(b'_') + .remove(b'.') + .remove(b'!') + .remove(b'~') + .remove(b'*') + .remove(b'\'') + .remove(b'(') + .remove(b')'); + +/// The origin (`scheme://host`, without a trailing slash) a custom protocol is served from. +/// +/// On Windows and Android custom protocols are served over `http(s)://{scheme}.localhost`; +/// everywhere else they are served over `{scheme}://localhost`. +pub(crate) fn origin(scheme: &str, use_https: bool) -> String { + if cfg!(windows) || cfg!(target_os = "android") { + let http = if use_https { "https" } else { "http" }; + format!("{http}://{scheme}.localhost") + } else { + format!("{scheme}://localhost") + } +} + +#[cfg(test)] +mod tests { + #[test] + fn encode_uri_component_matches_js() { + // encodeURIComponent("aZ09-_.!~*'() /\\?#&%+é") + let input = "aZ09-_.!~*'() /\\?#&%+é"; + let expected = "aZ09-_.!~*'()%20%2F%5C%3F%23%26%25%2B%C3%A9"; + assert_eq!( + percent_encoding::utf8_percent_encode(input, super::ENCODE_URI_COMPONENT).to_string(), + expected + ); + } +} diff --git a/crates/tauri/src/protocol/tauri.rs b/crates/tauri/src/protocol/tauri.rs new file mode 100644 index 0000000..3b940b4 --- /dev/null +++ b/crates/tauri/src/protocol/tauri.rs @@ -0,0 +1,267 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{borrow::Cow, sync::Arc}; + +use http::{Request, Response as HttpResponse, StatusCode, header::CONTENT_TYPE}; +use tauri_utils::config::HeaderAddition; + +use crate::{ + Runtime, + manager::{AppManager, webview::PROXY_DEV_SERVER}, + webview::{UriSchemeProtocolHandler, WebResourceRequestHandler}, +}; + +#[cfg(all(dev, mobile))] +use std::{collections::HashMap, sync::Mutex}; + +#[cfg(all(dev, mobile))] +#[derive(Clone)] +struct CachedResponse { + status: http::StatusCode, + headers: http::HeaderMap, + body: Vec, +} + +pub fn get( + manager: Arc>, + window_origin: String, + web_resource_request_handler: Option>, +) -> UriSchemeProtocolHandler { + #[cfg(all(dev, mobile))] + let (url, client, response_cache) = { + let use_https = window_origin.starts_with("https"); + let mut url = manager.get_app_url(use_https).as_str().to_string(); + if url.ends_with('/') { + url.pop(); + } + + #[allow(unused_mut)] + let mut client_builder = reqwest::ClientBuilder::new(); + if use_https { + #[cfg(feature = "rustls-tls")] + if rustls::crypto::CryptoProvider::get_default().is_none() { + let _ = rustls::crypto::ring::default_provider().install_default(); + } + + // we can't load env vars at runtime, gotta embed them in the lib + #[allow(unused_variables)] + if let Some(cert_pem) = option_env!("TAURI_DEV_ROOT_CERTIFICATE") { + #[cfg(any( + feature = "native-tls", + feature = "native-tls-vendored", + feature = "rustls-tls" + ))] + { + log::info!("adding dev server root certificate"); + let certificate = reqwest::Certificate::from_pem(cert_pem.as_bytes()) + .expect("failed to parse TAURI_DEV_ROOT_CERTIFICATE"); + client_builder = client_builder.tls_certs_merge([certificate]); + } + + #[cfg(not(any( + feature = "native-tls", + feature = "native-tls-vendored", + feature = "rustls-tls" + )))] + { + log::warn!( + "the dev root-certificate-path option was provided, but you must enable one of the following Tauri features in Cargo.toml: native-tls, native-tls-vendored, rustls-tls" + ); + } + } else { + log::warn!( + "loading HTTPS URL; you might need to provide a certificate via the `dev --root-certificate-path` option. You must enable one of the following Tauri features in Cargo.toml: native-tls, native-tls-vendored, rustls-tls" + ); + } + } + let client = client_builder.build().unwrap(); + + let response_cache = Mutex::new(HashMap::new()); + + (url, client, response_cache) + }; + + let context = Arc::new(Context { + manager, + web_resource_request_handler, + window_origin, + #[cfg(all(dev, mobile))] + client, + #[cfg(all(dev, mobile))] + url, + #[cfg(all(dev, mobile))] + response_cache, + }); + + Box::new(move |_, request, responder| { + let context = context.clone(); + crate::async_runtime::spawn(async move { + match get_response(&context, request).await { + Ok(response) => responder.respond(response), + Err(e) => responder.respond( + HttpResponse::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + .header("Access-Control-Allow-Origin", &context.window_origin) + .body(e.to_string().into_bytes()) + .unwrap(), + ), + } + }); + }) +} + +struct Context { + manager: Arc>, + window_origin: String, + web_resource_request_handler: Option>, + + #[cfg(all(dev, mobile))] + url: String, + #[cfg(all(dev, mobile))] + client: reqwest::Client, + #[cfg(all(dev, mobile))] + response_cache: Mutex>, +} + +async fn get_response( + context: &Context, + request: Request>, +) -> Result>, Box> { + let Context { + manager, + web_resource_request_handler, + window_origin, + #[cfg(all(dev, mobile))] + client, + #[cfg(all(dev, mobile))] + url, + #[cfg(all(dev, mobile))] + response_cache, + } = context; + + // use the entire URI as we are going to proxy the request + let path = if PROXY_DEV_SERVER { + request.uri().to_string() + } else { + // ignore query string and fragment + request + .uri() + .to_string() + .split(&['?', '#']) + .next() + .unwrap() + .into() + }; + + let path = path + .strip_prefix("tauri://localhost") + .map(|p| p.to_string()) + // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows and Android + // where `$P` is not `localhost/*` + .unwrap_or_default(); + + #[allow(unused_mut)] + let mut builder = HttpResponse::builder() + .add_configured_headers(manager.config.app.security.headers.as_ref()) + .header("Access-Control-Allow-Origin", window_origin); + + #[cfg(all(dev, mobile))] + let mut response = + proxy_dev_request(client, url, response_cache, path, builder, &request).await?; + + #[cfg(not(all(dev, mobile)))] + let mut response = { + let asset = manager.get_asset( + path, + request.uri().scheme() == Some(&http::uri::Scheme::HTTPS), + )?; + builder = builder.header(CONTENT_TYPE, &asset.mime_type); + if let Some(csp) = &asset.csp_header { + builder = builder.header("Content-Security-Policy", csp); + } + builder.body(asset.bytes.into())? + }; + + if let Some(handler) = web_resource_request_handler { + handler(request, &mut response); + } + + Ok(response) +} + +#[cfg(all(dev, mobile))] +async fn proxy_dev_request( + client: &reqwest::Client, + url: &String, + response_cache: &Mutex>, + path: String, + mut builder: http::response::Builder, + request: &Request>, +) -> Result>, Box> { + let decoded_path = percent_encoding::percent_decode(path.as_bytes()) + .decode_utf8_lossy() + .to_string(); + let url = format!( + "{}/{}", + url.trim_end_matches('/'), + decoded_path.trim_start_matches('/') + ); + + let mut proxy_builder = client.request(request.method().clone(), &url); + for (name, value) in request.headers() { + proxy_builder = proxy_builder.header(name, value); + } + proxy_builder = proxy_builder.body(request.body().clone()); + + let response = proxy_builder.send().await.map_err(|e|{ + let error_message = format!( + "Failed to request {url}: {e}{}", + if let Some(s) = e.status() { + format!("status code: {}", s.as_u16()) + } else if cfg!(target_os = "ios") { + ", did you grant local network permissions? That is required to reach the development server. Please grant the permission via the prompt or in `Settings > Privacy & Security > Local Network` and restart the app. See https://support.apple.com/en-us/102229 for more information.".to_string() + } else { + "".to_string() + } + ); + log::error!("{error_message}"); + error_message + })?; + + let status = response.status(); + + if status == http::StatusCode::NOT_MODIFIED { + if let Some(response) = response_cache.lock().unwrap().get(&url).cloned() { + for (name, value) in &response.headers { + builder = builder.header(name, value); + } + + return Ok(builder.status(response.status).body(response.body.into())?); + } + } + + let headers = response.headers().clone(); + let body = response.bytes().await?.to_vec(); + let response = CachedResponse { + status, + headers, + body, + }; + + response_cache + .lock() + .unwrap() + .insert(url.clone(), response.clone()); + + for (name, value) in &response.headers { + builder = builder.header(name, value); + } + + builder + .status(response.status) + .body(response.body.into()) + .map_err(Into::into) +} diff --git a/crates/tauri/src/resources/mod.rs b/crates/tauri/src/resources/mod.rs new file mode 100644 index 0000000..9561558 --- /dev/null +++ b/crates/tauri/src/resources/mod.rs @@ -0,0 +1,223 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// a modified version of https://github.com/denoland/deno/blob/0ae83847f498a2886ae32172e50fd5bdbab2f524/core/resources.rs#L220 + +pub(crate) mod plugin; + +use std::{ + any::{Any, TypeId, type_name}, + borrow::Cow, + collections::BTreeMap, + sync::Arc, +}; + +/// Resources are Rust objects that are stored in [ResourceTable] and managed by tauri. +/// +/// They are identified in JS by a numeric ID (the resource ID, or rid). +/// Resources can be created in commands. Resources can also be retrieved in commands by +/// their rid. Resources are thread-safe. +/// +/// Resources are reference counted in Rust. This means that they can be +/// cloned and passed around. When the last reference is dropped, the resource +/// is automatically closed. As long as the resource exists in the resource +/// table, the reference count is at least 1. +pub trait Resource: Any + 'static + Send + Sync { + /// Returns a string representation of the resource. The default implementation + /// returns the Rust type name, but specific resource types may override this + /// trait method. + fn name(&self) -> Cow<'_, str> { + type_name::().into() + } + + /// Resources may implement the `close()` trait method if they need to do + /// resource specific clean-ups, such as cancelling pending futures, after a + /// resource has been removed from the resource table. + fn close(self: Arc) {} +} + +impl dyn Resource { + #[inline(always)] + fn is(&self) -> bool { + self.type_id() == TypeId::of::() + } + + #[inline(always)] + pub(crate) fn downcast_arc(self: &Arc) -> Option<&Arc> { + if self.is::() { + // A resource is stored as `Arc` in a BTreeMap + // and is safe to cast to `Arc` because of the runtime + // check done in `self.is::()` + let ptr = self as *const Arc<_> as *const Arc; + Some(unsafe { &*ptr }) + } else { + None + } + } +} + +/// A `ResourceId` is an integer value referencing a resource. It could be +/// considered to be the tauri equivalent of a `file descriptor` in POSIX like +/// operating systems. +pub type ResourceId = u32; + +/// Map-like data structure storing Tauri's resources (equivalent to file +/// descriptors). +/// +/// Provides basic methods for element access. A resource can be of any type. +/// Different types of resources can be stored in the same map, and provided +/// with a name for description. +/// +/// Each resource is identified through a _resource ID (rid)_, which acts as +/// the key in the map. +#[derive(Default)] +pub struct ResourceTable { + index: BTreeMap>, +} + +impl ResourceTable { + fn new_random_rid() -> u32 { + let mut bytes = [0_u8; 4]; + getrandom::fill(&mut bytes).expect("failed to get random bytes"); + u32::from_ne_bytes(bytes) + } + + /// Inserts resource into the resource table, which takes ownership of it. + /// + /// The resource type is erased at runtime and must be statically known + /// when retrieving it through `get()`. + /// + /// Returns a unique resource ID, which acts as a key for this resource. + pub fn add(&mut self, resource: T) -> ResourceId { + self.add_arc(Arc::new(resource)) + } + + /// Inserts a `Arc`-wrapped resource into the resource table. + /// + /// The resource type is erased at runtime and must be statically known + /// when retrieving it through `get()`. + /// + /// Returns a unique resource ID, which acts as a key for this resource. + pub fn add_arc(&mut self, resource: Arc) -> ResourceId { + let resource = resource as Arc; + self.add_arc_dyn(resource) + } + + /// Inserts a `Arc`-wrapped resource into the resource table. + /// + /// The resource type is erased at runtime and must be statically known + /// when retrieving it through `get()`. + /// + /// Returns a unique resource ID, which acts as a key for this resource. + pub fn add_arc_dyn(&mut self, resource: Arc) -> ResourceId { + let mut rid = Self::new_random_rid(); + while self.index.contains_key(&rid) { + rid = Self::new_random_rid(); + } + + let removed_resource = self.index.insert(rid, resource); + assert!(removed_resource.is_none()); + rid + } + + /// Returns true if any resource with the given `rid` exists. + pub fn has(&self, rid: ResourceId) -> bool { + self.index.contains_key(&rid) + } + + /// Returns a reference counted pointer to the resource of type `T` with the + /// given `rid`. If `rid` is not present or has a type different than `T`, + /// this function returns [`Error::BadResourceId`](crate::Error::BadResourceId). + pub fn get(&self, rid: ResourceId) -> crate::Result> { + self + .index + .get(&rid) + .and_then(|rc| rc.downcast_arc::()) + .cloned() + .ok_or_else(|| crate::Error::BadResourceId(rid)) + } + + /// Returns a reference counted pointer to the resource of the given `rid`. + /// If `rid` is not present, this function returns [`crate::Error::BadResourceId`]. + pub fn get_any(&self, rid: ResourceId) -> crate::Result> { + self + .index + .get(&rid) + .ok_or_else(|| crate::Error::BadResourceId(rid)) + .cloned() + } + + /// Replaces a resource with a new resource. + /// + /// Panics if the resource does not exist. + pub fn replace(&mut self, rid: ResourceId, resource: T) { + let result = self + .index + .insert(rid, Arc::new(resource) as Arc); + assert!(result.is_some()); + } + + /// Removes a resource of type `T` from the resource table and returns it. + /// If a resource with the given `rid` exists but its type does not match `T`, + /// it is not removed from the resource table. Note that the resource's + /// `close()` method is *not* called. + /// + /// Also note that there might be a case where + /// the returned `Arc` is referenced by other variables. That is, we cannot + /// assume that `Arc::strong_count(&returned_arc)` is always equal to 1 on success. + /// In particular, be really careful when you want to extract the inner value of + /// type `T` from `Arc`. + pub fn take(&mut self, rid: ResourceId) -> crate::Result> { + let resource = self.get::(rid)?; + self.index.remove(&rid); + Ok(resource) + } + + /// Removes a resource from the resource table and returns it. Note that the + /// resource's `close()` method is *not* called. + /// + /// Also note that there might be a + /// case where the returned `Arc` is referenced by other variables. That is, + /// we cannot assume that `Arc::strong_count(&returned_arc)` is always equal to 1 + /// on success. In particular, be really careful when you want to extract the + /// inner value of type `T` from `Arc`. + pub fn take_any(&mut self, rid: ResourceId) -> crate::Result> { + self + .index + .remove(&rid) + .ok_or_else(|| crate::Error::BadResourceId(rid)) + } + + /// Returns an iterator that yields a `(id, name)` pair for every resource + /// that's currently in the resource table. This can be used for debugging + /// purposes. Note that the order in + /// which items appear is not specified. + pub fn names(&self) -> impl Iterator)> { + self + .index + .iter() + .map(|(&id, resource)| (id, resource.name())) + } + + /// Removes the resource with the given `rid` from the resource table. If the + /// only reference to this resource existed in the resource table, this will + /// cause the resource to be dropped. However, since resources are reference + /// counted, therefore pending ops are not automatically cancelled. A resource + /// may implement the `close()` method to perform clean-ups such as canceling + /// ops. + pub fn close(&mut self, rid: ResourceId) -> crate::Result<()> { + self + .index + .remove(&rid) + .ok_or_else(|| crate::Error::BadResourceId(rid)) + .map(|resource| resource.close()) + } + + /// Removes and frees all resources stored. Note that the + /// resource's `close()` method is *not* called. + pub(crate) fn clear(&mut self) { + self.index.clear() + } +} diff --git a/crates/tauri/src/resources/plugin.rs b/crates/tauri/src/resources/plugin.rs new file mode 100644 index 0000000..548a01a --- /dev/null +++ b/crates/tauri/src/resources/plugin.rs @@ -0,0 +1,31 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + Manager, Runtime, Webview, command, + plugin::{Builder, TauriPlugin}, +}; + +use super::ResourceId; + +#[command(root = "crate")] +fn close(webview: Webview, rid: ResourceId) -> crate::Result<()> { + let mut result = webview.resources_table().close(rid); + if result.is_err() { + result = webview.window().resources_table().close(rid); + if result.is_err() { + result = webview.app_handle().resources_table().close(rid); + } + } + result +} + +pub(crate) fn init() -> TauriPlugin { + Builder::new("resources") + .invoke_handler(crate::generate_handler![ + #![plugin(resources)] + close + ]) + .build() +} diff --git a/crates/tauri/src/scope/fs.rs b/crates/tauri/src/scope/fs.rs new file mode 100644 index 0000000..4dabedd --- /dev/null +++ b/crates/tauri/src/scope/fs.rs @@ -0,0 +1,739 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::{HashMap, HashSet}, + fmt, + path::{MAIN_SEPARATOR, Path, PathBuf}, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, AtomicU32, Ordering}, + }, +}; + +use tauri_utils::config::FsScope; + +use crate::ScopeEventId; + +pub use glob::Pattern; + +/// Scope change event. +#[derive(Debug, Clone)] +pub enum Event { + /// A path has been allowed. + PathAllowed(PathBuf), + /// A path has been forbidden. + PathForbidden(PathBuf), +} + +type EventListener = Box; + +enum Pending { + Unlisten(ScopeEventId), + Listen { + id: ScopeEventId, + handler: EventListener, + }, + Emit(Event), +} + +struct ScopeInner { + allowed_patterns: Mutex>, + forbidden_patterns: Mutex>, + match_options: glob::MatchOptions, + event_listeners: Mutex>, + pending: Mutex>, + emitting: AtomicBool, + next_event_id: AtomicU32, +} + +/// Scope for filesystem access. +#[derive(Clone)] +pub struct Scope { + inner: Arc, +} + +impl Scope { + fn next_event_id(&self) -> u32 { + self.inner.next_event_id.fetch_add(1, Ordering::Relaxed) + } +} + +impl fmt::Debug for Scope { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Scope") + .field( + "allowed_patterns", + &self + .inner + .allowed_patterns + .lock() + .unwrap() + .iter() + .map(|p| p.as_str()) + .collect::>(), + ) + .field( + "forbidden_patterns", + &self + .inner + .forbidden_patterns + .lock() + .unwrap() + .iter() + .map(|p| p.as_str()) + .collect::>(), + ) + .finish() + } +} + +fn push_pattern, F: Fn(&str) -> Result>( + list: &mut HashSet, + pattern: P, + f: F, +) -> crate::Result<()> { + // 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(); + + // Add pattern as is to be matched with paths as is + let path_str = path.to_string_lossy(); + list.insert(f(&path_str)?); + + // On Windows, if path starts with a Prefix, try to strip it if possible + // so `\\?\C:\\SomeDir` would result in a scope of: + // - `\\?\C:\\SomeDir` + // - `C:\\SomeDir` + #[cfg(windows)] + { + use std::path::{Component, Prefix}; + + let mut components = path.components(); + + let is_unc = match components.next() { + Some(Component::Prefix(p)) => match p.kind() { + Prefix::VerbatimDisk(..) => true, + _ => false, // Other kinds of UNC paths + }, + _ => false, // relative or empty + }; + + if is_unc { + // we remove UNC manually, instead of `dunce::simplified` because + // `path` could have `*` in it and that's not allowed on Windows and + // `dunce::simplified` will check that and return `path` as is without simplification + let simplified = path + .to_str() + .and_then(|s| s.get(4..)) + .map_or(path.as_path(), Path::new); + + let simplified_str = simplified.to_string_lossy(); + if simplified_str != path_str { + list.insert(f(&simplified_str)?); + } + } + } + + // Add canonicalized version of the pattern or canonicalized version of its parents + // so `/data/user/0/appid/assets/*` would be canonicalized to `/data/data/appid/assets/*` + // and can then be matched against any of them. + if let Some(p) = canonicalize_parent(path) { + list.insert(f(&p.to_string_lossy())?); + } + + Ok(()) +} + +/// Attempt to canonicalize path or its parents in case we have a path like `/data/user/0/appid/**` +/// where `**` obviously does not exist but we need to canonicalize the parent. +/// +/// example: given the `/data/user/0/appid/assets/*` path, +/// it's a glob pattern so it won't exist (std::fs::canonicalize() fails); +/// +/// the second iteration needs to check `/data/user/0/appid/assets` and save the `*` component to append later. +/// +/// if it also does not exist, a third iteration is required to check `/data/user/0/appid` +/// with `assets/*` as the cached value (`checked_path` variable) +/// on Android that gets canonicalized to `/data/data/appid` so the final value will be `/data/data/appid/assets/*` +/// which is the value we want to check when we execute the `Scope::is_allowed` function +fn canonicalize_parent(mut path: PathBuf) -> Option { + let mut failed_components = None; + + loop { + if let Ok(path) = path.canonicalize() { + break Some(if let Some(p) = failed_components { + path.join(p) + } else { + path + }); + } + + // grap the last component of the path + if let Some(mut last) = path.iter().next_back().map(PathBuf::from) { + // remove the last component of the path so the next iteration checks its parent + // if there is no more parent components, we failed to canonicalize + if !path.pop() { + break None; + } + + // append the already checked path to the last component + // to construct `/` and saved it for next iteration + if let Some(failed_components) = &failed_components { + last.push(failed_components); + } + failed_components.replace(last); + } else { + break None; + } + } +} +impl Scope { + /// Creates a new scope from a [`FsScope`] configuration. + pub fn new>( + manager: &M, + scope: &FsScope, + ) -> crate::Result { + let mut allowed_patterns = HashSet::new(); + for path in scope.allowed_paths() { + if let Ok(path) = manager.path().parse(path) { + push_pattern(&mut allowed_patterns, path, Pattern::new)?; + } + } + + let mut forbidden_patterns = HashSet::new(); + if let Some(forbidden_paths) = scope.forbidden_paths() { + for path in forbidden_paths { + if let Ok(path) = manager.path().parse(path) { + push_pattern(&mut forbidden_patterns, path, Pattern::new)?; + } + } + } + + let require_literal_leading_dot = match scope { + FsScope::Scope { + require_literal_leading_dot: Some(require), + .. + } => *require, + // dotfiles are not supposed to be exposed by default on unix + #[cfg(unix)] + _ => true, + #[cfg(windows)] + _ => false, + }; + + Ok(Self { + inner: Arc::new(ScopeInner { + allowed_patterns: Mutex::new(allowed_patterns), + forbidden_patterns: Mutex::new(forbidden_patterns), + event_listeners: Default::default(), + pending: Default::default(), + emitting: AtomicBool::new(false), + next_event_id: Default::default(), + match_options: glob::MatchOptions { + // this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt` + // see: + require_literal_separator: true, + require_literal_leading_dot, + ..Default::default() + }, + }), + }) + } + + /// The list of allowed patterns. + pub fn allowed_patterns(&self) -> HashSet { + self.inner.allowed_patterns.lock().unwrap().clone() + } + + /// The list of forbidden patterns. + pub fn forbidden_patterns(&self) -> HashSet { + self.inner.forbidden_patterns.lock().unwrap().clone() + } + + /// Listen to an event on this scope. + pub fn listen(&self, f: F) -> ScopeEventId { + let id = self.next_event_id(); + self.listen_with_id(id, Box::new(f)); + id + } + + fn listen_with_id(&self, id: ScopeEventId, handler: EventListener) { + if self.inner.emitting.load(Ordering::Relaxed) { + self + .inner + .pending + .lock() + .unwrap() + .push(Pending::Listen { id, handler }); + } else { + self + .inner + .event_listeners + .lock() + .unwrap() + .insert(id, handler); + } + } + + /// Listen to an event on this scope and immediately unlisten. + pub fn once(&self, f: F) -> ScopeEventId { + let self_ = self.clone(); + let handler = std::cell::Cell::new(Some(f)); + let id = self.next_event_id(); + self.listen_with_id( + id, + Box::new(move |event| { + self_.unlisten(id); + let handler = handler + .take() + .expect("attempted to call handler more than once"); + handler(event); + }), + ); + id + } + + /// Removes an event listener on this scope. + pub fn unlisten(&self, id: ScopeEventId) { + if self.inner.emitting.load(Ordering::Relaxed) { + self + .inner + .pending + .lock() + .unwrap() + .push(Pending::Unlisten(id)); + } else { + self.inner.event_listeners.lock().unwrap().remove(&id); + } + } + + fn emit(&self, event: Event) { + let was_emitting = self.inner.emitting.swap(true, Ordering::Relaxed); + if was_emitting { + self + .inner + .pending + .lock() + .unwrap() + .push(Pending::Emit(event)); + return; + } + + { + let listeners = self.inner.event_listeners.lock().unwrap(); + let handlers = listeners.values(); + for listener in handlers { + listener(&event); + } + self.inner.emitting.store(false, Ordering::Relaxed); + } + + let pending = { + let mut lock = self.inner.pending.lock().unwrap(); + std::mem::take(&mut *lock) + }; + for action in pending { + match action { + Pending::Unlisten(id) => self.unlisten(id), + Pending::Listen { id, handler } => self.listen_with_id(id, handler), + Pending::Emit(event) => self.emit(event), + } + } + } + + /// Extend the allowed patterns with the given directory. + /// + /// After this function has been called, the frontend will be able to use the Tauri API to read + /// the directory and all of its files. If `recursive` is `true`, subdirectories will be accessible too. + pub fn allow_directory>(&self, path: P, recursive: bool) -> crate::Result<()> { + let path = path.as_ref(); + { + let mut list = self.inner.allowed_patterns.lock().unwrap(); + + // allow the directory to be read + push_pattern(&mut list, path, escaped_pattern)?; + // allow its files and subdirectories to be read + push_pattern(&mut list, path, |p| { + escaped_pattern_with(p, if recursive { "**" } else { "*" }) + })?; + } + self.emit(Event::PathAllowed(path.to_path_buf())); + Ok(()) + } + + /// Extend the allowed patterns with the given file path. + /// + /// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file. + pub fn allow_file>(&self, path: P) -> crate::Result<()> { + let path = path.as_ref(); + push_pattern( + &mut self.inner.allowed_patterns.lock().unwrap(), + path, + escaped_pattern, + )?; + self.emit(Event::PathAllowed(path.to_path_buf())); + Ok(()) + } + + /// Set the given directory path to be forbidden by this scope. + /// + /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. + pub fn forbid_directory>(&self, path: P, recursive: bool) -> crate::Result<()> { + let path = path.as_ref(); + { + let mut list = self.inner.forbidden_patterns.lock().unwrap(); + + // allow the directory to be read + push_pattern(&mut list, path, escaped_pattern)?; + // allow its files and subdirectories to be read + push_pattern(&mut list, path, |p| { + escaped_pattern_with(p, if recursive { "**" } else { "*" }) + })?; + } + self.emit(Event::PathForbidden(path.to_path_buf())); + Ok(()) + } + + /// Set the given file path to be forbidden by this scope. + /// + /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. + pub fn forbid_file>(&self, path: P) -> crate::Result<()> { + let path = path.as_ref(); + push_pattern( + &mut self.inner.forbidden_patterns.lock().unwrap(), + path, + escaped_pattern, + )?; + self.emit(Event::PathForbidden(path.to_path_buf())); + Ok(()) + } + + /// Determines if the given path is allowed on this scope. + /// + /// Returns `false` if the path was explicitly forbidden or neither allowed nor forbidden. + /// + /// May return `false` if the path points to a broken symlink. + pub fn is_allowed>(&self, path: P) -> bool { + let path = try_resolve_symlink_and_canonicalize(path); + + if let Ok(path) = path { + let path: PathBuf = path.components().collect(); + let forbidden = self + .inner + .forbidden_patterns + .lock() + .unwrap() + .iter() + .any(|p| p.matches_path_with(&path, self.inner.match_options)); + + if forbidden { + false + } else { + self + .inner + .allowed_patterns + .lock() + .unwrap() + .iter() + .any(|p| p.matches_path_with(&path, self.inner.match_options)) + } + } else { + false + } + } + + /// Determines if the given path is explicitly forbidden on this scope. + /// + /// May return `true` if the path points to a broken symlink. + pub fn is_forbidden>(&self, path: P) -> bool { + let path = try_resolve_symlink_and_canonicalize(path); + + if let Ok(path) = path { + let path: PathBuf = path.components().collect(); + self + .inner + .forbidden_patterns + .lock() + .unwrap() + .iter() + .any(|p| p.matches_path_with(&path, self.inner.match_options)) + } else { + true + } + } +} + +fn try_resolve_symlink_and_canonicalize>(path: P) -> crate::Result { + let path = path.as_ref(); + let path = if path.is_symlink() { + std::fs::read_link(path)? + } else { + path.to_path_buf() + }; + if !path.exists() { + crate::Result::Ok(path) + } else { + std::fs::canonicalize(path).map_err(Into::into) + } +} + +fn escaped_pattern(p: &str) -> Result { + Pattern::new(&glob::Pattern::escape(p)) +} + +fn escaped_pattern_with(p: &str, append: &str) -> Result { + if p.ends_with(MAIN_SEPARATOR) { + Pattern::new(&format!("{}{append}", glob::Pattern::escape(p))) + } else { + Pattern::new(&format!( + "{}{}{append}", + glob::Pattern::escape(p), + MAIN_SEPARATOR + )) + } +} + +#[cfg(test)] +mod tests { + use std::{collections::HashSet, sync::Arc}; + + use glob::Pattern; + + use crate::fs::ScopeInner; + + use super::{Scope, push_pattern}; + + fn new_scope() -> Scope { + Scope { + inner: Arc::new(ScopeInner { + allowed_patterns: Default::default(), + forbidden_patterns: Default::default(), + event_listeners: Default::default(), + pending: Default::default(), + emitting: Default::default(), + next_event_id: Default::default(), + match_options: glob::MatchOptions { + // this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt` + // see: + require_literal_separator: true, + // dotfiles are not supposed to be exposed by default on unix + #[cfg(unix)] + require_literal_leading_dot: true, + #[cfg(windows)] + require_literal_leading_dot: false, + ..Default::default() + }, + }), + } + } + + #[test] + fn path_is_escaped() { + let scope = new_scope(); + #[cfg(unix)] + { + scope.allow_directory("/home/tauri/**", false).unwrap(); + assert!(scope.is_allowed("/home/tauri/**")); + assert!(scope.is_allowed("/home/tauri/**/file")); + assert!(!scope.is_allowed("/home/tauri/anyfile")); + } + #[cfg(windows)] + { + scope.allow_directory("C:\\home\\tauri\\**", false).unwrap(); + assert!(scope.is_allowed("C:\\home\\tauri\\**")); + assert!(scope.is_allowed("C:\\home\\tauri\\**\\file")); + assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile")); + } + + let scope = new_scope(); + #[cfg(unix)] + { + scope.allow_file("/home/tauri/**").unwrap(); + assert!(scope.is_allowed("/home/tauri/**")); + assert!(!scope.is_allowed("/home/tauri/**/file")); + assert!(!scope.is_allowed("/home/tauri/anyfile")); + } + #[cfg(windows)] + { + scope.allow_file("C:\\home\\tauri\\**").unwrap(); + assert!(scope.is_allowed("C:\\home\\tauri\\**")); + assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file")); + assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile")); + } + + let scope = new_scope(); + #[cfg(unix)] + { + scope.allow_directory("/home/tauri", true).unwrap(); + scope.forbid_directory("/home/tauri/**", false).unwrap(); + assert!(!scope.is_allowed("/home/tauri/**")); + assert!(!scope.is_allowed("/home/tauri/**/file")); + assert!(scope.is_allowed("/home/tauri/**/inner/file")); + assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile")); + assert!(scope.is_allowed("/home/tauri/anyfile")); + } + #[cfg(windows)] + { + scope.allow_directory("C:\\home\\tauri", true).unwrap(); + scope + .forbid_directory("C:\\home\\tauri\\**", false) + .unwrap(); + assert!(!scope.is_allowed("C:\\home\\tauri\\**")); + assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file")); + assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file")); + assert!(scope.is_allowed("C:\\home\\tauri\\inner\\folder\\anyfile")); + assert!(scope.is_allowed("C:\\home\\tauri\\anyfile")); + } + + let scope = new_scope(); + #[cfg(unix)] + { + scope.allow_directory("/home/tauri", true).unwrap(); + scope.forbid_file("/home/tauri/**").unwrap(); + assert!(!scope.is_allowed("/home/tauri/**")); + assert!(scope.is_allowed("/home/tauri/**/file")); + assert!(scope.is_allowed("/home/tauri/**/inner/file")); + assert!(scope.is_allowed("/home/tauri/anyfile")); + } + #[cfg(windows)] + { + scope.allow_directory("C:\\home\\tauri", true).unwrap(); + scope.forbid_file("C:\\home\\tauri\\**").unwrap(); + assert!(!scope.is_allowed("C:\\home\\tauri\\**")); + assert!(scope.is_allowed("C:\\home\\tauri\\**\\file")); + assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file")); + assert!(scope.is_allowed("C:\\home\\tauri\\anyfile")); + } + + let scope = new_scope(); + #[cfg(unix)] + { + scope.allow_directory("/home/tauri", false).unwrap(); + assert!(scope.is_allowed("/home/tauri/**")); + assert!(!scope.is_allowed("/home/tauri/**/file")); + assert!(!scope.is_allowed("/home/tauri/**/inner/file")); + assert!(scope.is_allowed("/home/tauri/anyfile")); + } + #[cfg(windows)] + { + scope.allow_directory("C:\\home\\tauri", false).unwrap(); + assert!(scope.is_allowed("C:\\home\\tauri\\**")); + assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file")); + assert!(!scope.is_allowed("C:\\home\\tauri\\**\\inner\\file")); + assert!(scope.is_allowed("C:\\home\\tauri\\anyfile")); + } + } + + #[cfg(windows)] + #[test] + fn windows_root_paths() { + let scope = new_scope(); + { + // UNC network path + scope.allow_directory("\\\\localhost\\c$", true).unwrap(); + assert!(scope.is_allowed("\\\\localhost\\c$")); + assert!(scope.is_allowed("\\\\localhost\\c$\\Windows")); + assert!(scope.is_allowed("\\\\localhost\\c$\\NonExistentFile")); + assert!(!scope.is_allowed("\\\\localhost\\d$")); + assert!(!scope.is_allowed("\\\\OtherServer\\Share")); + } + + let scope = new_scope(); + { + // Verbatim UNC network path + scope + .allow_directory("\\\\?\\UNC\\localhost\\c$", true) + .unwrap(); + assert!(scope.is_allowed("\\\\localhost\\c$")); + assert!(scope.is_allowed("\\\\localhost\\c$\\Windows")); + assert!(scope.is_allowed("\\\\?\\UNC\\localhost\\c$\\Windows\\NonExistentFile")); + // A non-existent file cannot be canonicalized to a verbatim UNC path, so this will fail to match + assert!(!scope.is_allowed("\\\\localhost\\c$\\Windows\\NonExistentFile")); + assert!(!scope.is_allowed("\\\\localhost\\d$")); + assert!(!scope.is_allowed("\\\\OtherServer\\Share")); + } + + let scope = new_scope(); + { + // Device namespace + scope.allow_file("\\\\.\\COM1").unwrap(); + assert!(scope.is_allowed("\\\\.\\COM1")); + assert!(!scope.is_allowed("\\\\.\\COM2")); + } + + let scope = new_scope(); + { + // Disk root + scope.allow_directory("C:\\", true).unwrap(); + assert!(scope.is_allowed("C:\\Windows")); + assert!(scope.is_allowed("C:\\Windows\\system.ini")); + assert!(scope.is_allowed("C:\\NonExistentFile")); + assert!(!scope.is_allowed("D:\\home")); + } + + let scope = new_scope(); + { + // Verbatim disk root + scope.allow_directory("\\\\?\\C:\\", true).unwrap(); + assert!(scope.is_allowed("C:\\Windows")); + assert!(scope.is_allowed("C:\\Windows\\system.ini")); + assert!(scope.is_allowed("C:\\NonExistentFile")); + assert!(!scope.is_allowed("D:\\home")); + } + + let scope = new_scope(); + { + // Verbatim path + scope.allow_file("\\\\?\\anyfile").unwrap(); + assert!(scope.is_allowed("\\\\?\\anyfile")); + assert!(!scope.is_allowed("\\\\?\\otherfile")); + } + } + + #[test] + fn push_pattern_generated_paths() { + macro_rules! assert_pattern { + ($patterns:ident, $pattern:literal) => { + assert!($patterns.contains(&Pattern::new($pattern).unwrap())) + }; + } + + let mut patterns = HashSet::new(); + + #[cfg(not(windows))] + { + push_pattern(&mut patterns, "/path/to/dir/", Pattern::new).expect("failed to push pattern"); + push_pattern(&mut patterns, "/path/to/dir/**", Pattern::new).expect("failed to push pattern"); + + assert_pattern!(patterns, "/path/to/dir"); + assert_pattern!(patterns, "/path/to/dir/**"); + } + + #[cfg(windows)] + { + push_pattern(&mut patterns, "C:\\path\\to\\dir", Pattern::new) + .expect("failed to push pattern"); + push_pattern(&mut patterns, "C:\\path\\to\\dir\\**", Pattern::new) + .expect("failed to push pattern"); + + assert_pattern!(patterns, "C:\\path\\to\\dir"); + assert_pattern!(patterns, "C:\\path\\to\\dir\\**"); + assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir"); + assert_pattern!(patterns, "\\\\?\\C:\\path\\to\\dir\\**"); + } + } + + #[test] + fn event_no_deadlocks() { + let scope = new_scope(); + let scope_clone = scope.clone(); + scope.once(move |event| { + assert!(matches!(event, super::Event::PathAllowed(_))); + scope_clone.allow_file("/another-test-path").unwrap(); + }); + scope.allow_file("/test-path").unwrap(); + } +} diff --git a/crates/tauri/src/scope/mod.rs b/crates/tauri/src/scope/mod.rs new file mode 100644 index 0000000..af60c9d --- /dev/null +++ b/crates/tauri/src/scope/mod.rs @@ -0,0 +1,80 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Runtime access scopes. +//! +//! A scope narrows what an allowed feature may actually touch at runtime. +//! Tauri has two complementary scoping mechanisms: +//! +//! - **The IPC scope**, which is part of the [Access Control List](crate::ipc::RuntimeAuthority): +//! capabilities define which commands each window/webview and origin may call, and the scope +//! attached to a permission is read from a command with [`CommandScope`](crate::ipc::CommandScope) +//! and [`GlobalScope`](crate::ipc::GlobalScope). That scope is static - it is resolved from the +//! capability files at compile time (or added at runtime with +//! [`Manager::add_capability`](crate::Manager::add_capability)) - and it is the command +//! implementation that decides what to do with it. +//! - **The file system scope** in [`mod@fs`], a mutable list of allowed and forbidden glob patterns +//! that the [asset protocol](https://v2.tauri.app/security/asset-protocol/) checks before serving +//! a file. It is seeded from `app > security > assetProtocol > scope` in the configuration and +//! can be changed while the application is running. +//! +//! Use `Manager::asset_protocol_scope` (`protocol-asset` Cargo feature) to get the asset +//! protocol scope and [`fs::Scope::allow_directory`], [`fs::Scope::allow_file`], +//! [`fs::Scope::forbid_directory`] and [`fs::Scope::forbid_file`] to change it, for instance to +//! grant access to a file the user just picked in a dialog: +//! +//! ```rust +//! # #[cfg(feature = "protocol-asset")] +//! # fn run() { +//! use tauri::Manager; +//! +//! tauri::Builder::default() +//! .setup(|app| { +//! app.asset_protocol_scope().allow_directory("/home/user/pictures", true)?; +//! Ok(()) +//! }); +//! # } +//! ``` +//! +//! Scope changes are not persisted: they are lost when the application restarts. +//! Use the [persisted scope plugin](https://v2.tauri.app/plugin/persisted-scope/) +//! if you need them to survive a restart. + +/// FS scope. +pub mod fs; + +use std::path::Path; + +/// Unique id of a scope event. +pub type ScopeEventId = u32; + +/// Managed state for all the core scopes in a tauri application. +pub struct Scopes { + #[cfg(feature = "protocol-asset")] + pub(crate) asset_protocol: fs::Scope, +} + +#[allow(unused)] +impl Scopes { + /// Allows a directory on the scopes. + pub fn allow_directory>(&self, path: P, recursive: bool) -> crate::Result<()> { + #[cfg(feature = "protocol-asset")] + self.asset_protocol.allow_directory(path, recursive)?; + Ok(()) + } + + /// Allows a file on the scopes. + pub fn allow_file>(&self, path: P) -> crate::Result<()> { + #[cfg(feature = "protocol-asset")] + self.asset_protocol.allow_file(path)?; + Ok(()) + } + + /// Forbids a file on the scopes. + pub fn forbid_file>(&self, path: P) -> crate::Result<()> { + #[cfg(feature = "protocol-asset")] + self.asset_protocol.forbid_file(path)?; + Ok(()) + } +} diff --git a/crates/tauri/src/state.rs b/crates/tauri/src/state.rs new file mode 100644 index 0000000..fb563f5 --- /dev/null +++ b/crates/tauri/src/state.rs @@ -0,0 +1,291 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + any::{Any, TypeId}, + collections::HashMap, + hash::BuildHasherDefault, + sync::Mutex, +}; + +use crate::{ + Manager, Runtime, + ipc::{CommandArg, CommandItem, InvokeError}, +}; + +/// A guard for a state value. +/// +/// See [`Manager::manage`](`crate::Manager::manage`) for usage examples. +pub struct State<'r, T>(&'r T); + +impl<'r, T> State<'r, T> { + /// Retrieve a borrow to the underlying value with a lifetime of `'r`. + /// Using this method is typically unnecessary as `State` implements + /// [`std::ops::Deref`] with a [`std::ops::Deref::Target`] of `T`. + #[inline(always)] + pub fn inner(&self) -> &'r T { + self.0 + } +} + +impl std::ops::Deref for State<'_, T> { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &T { + self.0 + } +} + +impl Clone for State<'_, T> { + fn clone(&self) -> Self { + State(self.0) + } +} + +impl PartialEq for State<'_, T> { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl std::fmt::Debug for State<'_, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("State").field(&self.0).finish() + } +} + +impl<'r, 'de: 'r, T: Send + Sync + 'static, R: Runtime> CommandArg<'de, R> for State<'r, T> { + /// Grabs the [`State`] from the [`CommandItem`]. This will never fail. + fn from_command(command: CommandItem<'de, R>) -> Result { + command.message.webview_ref().try_state().ok_or_else(|| { + InvokeError::from_anyhow(anyhow::anyhow!( + "state not managed for field `{}` on command `{}`. You must call `.manage()` before using this command", + command.key, command.name + )) + }) + } +} + +// Taken from: https://github.com/SergioBenitez/state/blob/556c1b94db8ce8427a0e72de7983ab5a9af4cc41/src/ident_hash.rs +// This is a _super_ stupid hash. It just uses its input as the hash value. This +// hash is meant to be used _only_ for "prehashed" values. In particular, we use +// this so that hashing a TypeId is essentially a noop. This is because TypeIds +// are already unique integers. +#[derive(Default)] +struct IdentHash(u64); + +impl std::hash::Hasher for IdentHash { + fn finish(&self) -> u64 { + self.0 + } + + fn write(&mut self, bytes: &[u8]) { + for byte in bytes { + self.write_u8(*byte); + } + } + + fn write_u8(&mut self, i: u8) { + self.0 = (self.0 << 8) | (i as u64); + } + + fn write_u64(&mut self, i: u64) { + self.0 = i; + } +} + +/// Safety: +/// - The `key` must equal to `(*value).type_id()`, see the safety doc in methods of [StateManager] for details. +/// - Once you insert a value, you can't remove/mutated/move it anymore, see [StateManager::try_get] for details. +type TypeIdMap = HashMap, BuildHasherDefault>; + +/// The Tauri state manager. +// TODO: make private for v3 +#[derive(Debug)] +pub struct StateManager { + map: Mutex, +} + +impl StateManager { + pub(crate) fn new() -> Self { + Self { + map: Default::default(), + } + } + + pub(crate) fn set(&self, state: T) -> bool { + let mut map = self.map.lock().unwrap(); + let type_id = TypeId::of::(); + let already_set = map.contains_key(&type_id); + if !already_set { + let state = Box::new(state) as Box; + map.insert(type_id, state); + } + !already_set + } + + /// SAFETY: Calling this method will move the `value`, + /// which will cause references obtained through [Self::try_get] to dangle. + pub(crate) unsafe fn unmanage(&self) -> Option { + let mut map = self.map.lock().unwrap(); + let type_id = TypeId::of::(); + let state = map.remove(&type_id)?; + let value = state + .downcast::() + .expect("the type of the key should be same as the type of the value"); + Some(*value) + } + + /// Gets the state associated with the specified type. + pub fn get(&self) -> State<'_, T> { + self + .try_get() + .unwrap_or_else(|| panic!("state not found for type {}", std::any::type_name::())) + } + + /// Gets the state associated with the specified type. + pub fn try_get(&self) -> Option> { + let map = self.map.lock().unwrap(); + let type_id = TypeId::of::(); + let state = map.get(&type_id)?; + let value = state + .downcast_ref::() + .expect("the type of the key should be same as the type of the value"); + // SAFETY: We ensure the lifetime of `value` is the same as [StateManager] and `value` will not be mutated/moved. + let v_ref = unsafe { &*(value as *const T) }; + Some(State(v_ref)) + } +} + +// Ported from https://github.com/SergioBenitez/state/blob/556c1b94db8ce8427a0e72de7983ab5a9af4cc41/tests/main.rs +#[cfg(test)] +mod tests { + use super::StateManager; + + use std::sync::{Arc, RwLock}; + use std::thread; + + // Tiny structures to test that dropping works as expected. + struct DroppingStruct(Arc>); + struct DroppingStructWrap(#[allow(dead_code)] DroppingStruct); + + impl Drop for DroppingStruct { + fn drop(&mut self) { + *self.0.write().unwrap() = true; + } + } + + #[test] + #[should_panic(expected = "state not found for type core::option::Option")] + fn get_panics() { + let state = StateManager::new(); + state.get::>(); + } + + #[test] + fn simple_set_get() { + let state = StateManager::new(); + assert!(state.set(1u32)); + assert_eq!(*state.get::(), 1); + } + + #[test] + fn simple_set_get_unmanage() { + let state = StateManager::new(); + assert!(state.set(1u32)); + assert_eq!(*state.get::(), 1); + // safety: the reference returned by `try_get` is already dropped. + assert!(unsafe { state.unmanage::() }.is_some()); + assert!(unsafe { state.unmanage::() }.is_none()); + assert_eq!(state.try_get::(), None); + assert!(state.set(2u32)); + assert_eq!(*state.get::(), 2); + } + + #[test] + fn dst_set_get() { + let state = StateManager::new(); + assert!(state.set::<[u32; 4]>([1, 2, 3, 4u32])); + assert_eq!(*state.get::<[u32; 4]>(), [1, 2, 3, 4]); + } + + #[test] + fn set_get_remote() { + let state = Arc::new(StateManager::new()); + let sate_ = Arc::clone(&state); + thread::spawn(move || { + sate_.set(10isize); + }) + .join() + .unwrap(); + + assert_eq!(*state.get::(), 10); + } + + #[test] + fn two_put_get() { + let state = StateManager::new(); + assert!(state.set("Hello, world!".to_string())); + + let s_old = state.get::(); + assert_eq!(*s_old, "Hello, world!"); + + assert!(!state.set::("Bye bye!".into())); + assert_eq!(*state.get::(), "Hello, world!"); + assert_eq!(state.get::(), s_old); + } + + #[test] + fn many_puts_only_one_succeeds() { + let state = Arc::new(StateManager::new()); + let mut threads = vec![]; + for _ in 0..1000 { + let state_ = Arc::clone(&state); + threads.push(thread::spawn(move || state_.set(10i64))) + } + + let results: Vec = threads.into_iter().map(|t| t.join().unwrap()).collect(); + assert_eq!(results.into_iter().filter(|&b| b).count(), 1); + assert_eq!(*state.get::(), 10); + } + + // Ensure setting when already set doesn't cause a drop. + #[test] + fn test_no_drop_on_set() { + let state = StateManager::new(); + let drop_flag = Arc::new(RwLock::new(false)); + let dropping_struct = DroppingStruct(drop_flag.clone()); + + let _drop_flag_ignore = Arc::new(RwLock::new(false)); + let _dropping_struct_ignore = DroppingStruct(_drop_flag_ignore); + + state.set::(dropping_struct); + assert!(!state.set::(_dropping_struct_ignore)); + assert!(!*drop_flag.read().unwrap()); + } + + // Ensure dropping a type_map drops its contents. + #[test] + fn drop_inners_on_drop() { + let drop_flag_a = Arc::new(RwLock::new(false)); + let dropping_struct_a = DroppingStruct(drop_flag_a.clone()); + + let drop_flag_b = Arc::new(RwLock::new(false)); + let dropping_struct_b = DroppingStructWrap(DroppingStruct(drop_flag_b.clone())); + + { + let state = StateManager::new(); + state.set(dropping_struct_a); + assert!(!*drop_flag_a.read().unwrap()); + + state.set(dropping_struct_b); + assert!(!*drop_flag_a.read().unwrap()); + assert!(!*drop_flag_b.read().unwrap()); + } + + assert!(*drop_flag_a.read().unwrap()); + assert!(*drop_flag_b.read().unwrap()); + } +} diff --git a/crates/tauri/src/test/mock_runtime.rs b/crates/tauri/src/test/mock_runtime.rs new file mode 100644 index 0000000..ae1e6d1 --- /dev/null +++ b/crates/tauri/src/test/mock_runtime.rs @@ -0,0 +1,1430 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![allow(dead_code)] +#![allow(missing_docs)] + +use tauri_runtime::{ + DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Icon, ProgressBarState, + Result, RunEvent, Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType, UserEvent, + WebviewDispatch, WindowDispatch, WindowEventId, + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, + monitor::Monitor, + webview::{DetachedWebview, PendingWebview}, + window::{ + CursorIcon, DetachedWindow, DetachedWindowWebview, PendingWindow, RawWindow, WindowBuilder, + WindowBuilderBase, WindowEvent, WindowId, + }, +}; + +#[cfg(target_os = "macos")] +use tauri_utils::TitleBarStyle; +use tauri_utils::{Theme, config::WindowConfig}; +use url::Url; + +#[cfg(windows)] +use windows::Win32::Foundation::HWND; + +use std::{ + cell::RefCell, + collections::HashMap, + fmt, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, AtomicU32, Ordering}, + mpsc::{Receiver, SyncSender, channel, sync_channel}, + }, +}; + +type ShortcutMap = HashMap>; + +enum Message { + Task(Box), + CloseWindow(WindowId), + DestroyWindow(WindowId), +} + +struct Webview; + +struct Window { + label: String, + webviews: Vec, +} + +#[derive(Clone)] +pub struct RuntimeContext { + is_running: Arc, + windows: Arc>>, + shortcuts: Arc>, + run_tx: SyncSender, + main_thread_id: std::thread::ThreadId, + next_window_id: Arc, + next_webview_id: Arc, + next_window_event_id: Arc, + next_webview_event_id: Arc, +} + +// SAFETY: we ensure this type is only used on the main thread. +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Send for RuntimeContext {} + +// SAFETY: we ensure this type is only used on the main thread. +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Sync for RuntimeContext {} + +impl RuntimeContext { + fn send_message(&self, message: Message) -> Result<()> { + if std::thread::current().id() == self.main_thread_id { + if let Message::Task(task) = message { + task(); + return Ok(()); + } + } + if self.is_running.load(Ordering::Relaxed) { + self + .run_tx + .send(message) + .map_err(|_| Error::FailedToSendMessage) + } else { + match message { + Message::Task(task) => task(), + Message::CloseWindow(id) | Message::DestroyWindow(id) => { + self.windows.borrow_mut().remove(&id); + } + } + Ok(()) + } + } + + fn next_window_id(&self) -> WindowId { + self.next_window_id.fetch_add(1, Ordering::Relaxed).into() + } + + fn next_webview_id(&self) -> u32 { + self.next_webview_id.fetch_add(1, Ordering::Relaxed) + } + + fn next_window_event_id(&self) -> WindowEventId { + self.next_window_event_id.fetch_add(1, Ordering::Relaxed) + } + + fn next_webview_event_id(&self) -> WindowEventId { + self.next_webview_event_id.fetch_add(1, Ordering::Relaxed) + } +} + +impl fmt::Debug for RuntimeContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RuntimeContext").finish() + } +} + +#[derive(Debug, Clone)] +pub struct MockRuntimeHandle { + context: RuntimeContext, +} + +impl RuntimeHandle for MockRuntimeHandle { + type Runtime = MockRuntime; + + fn create_proxy(&self) -> EventProxy { + EventProxy {} + } + + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_activation_policy( + &self, + activation_policy: tauri_runtime::ActivationPolicy, + ) -> Result<()> { + Ok(()) + } + + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_dock_visibility(&self, visible: bool) -> Result<()> { + Ok(()) + } + + fn request_exit(&self, code: i32) -> Result<()> { + unimplemented!() + } + + /// Create a new webview window. + fn create_window) + Send + 'static>( + &self, + pending: PendingWindow, + _after_window_creation: Option, + ) -> Result> { + let id = self.context.next_window_id(); + + let (webview_id, webviews) = if let Some(w) = &pending.webview { + (Some(self.context.next_webview_id()), vec![Webview]) + } else { + (None, Vec::new()) + }; + + self.context.windows.borrow_mut().insert( + id, + Window { + label: pending.label.clone(), + webviews, + }, + ); + + let webview = webview_id.map(|id| { + let pending_webview = pending.webview.unwrap(); + DetachedWindowWebview { + webview: DetachedWebview { + label: pending.label.clone(), + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + url: Arc::new(Mutex::new(pending_webview.url)), + last_evaluated_script: Default::default(), + }, + }, + use_https_scheme: pending_webview.webview_attributes.use_https_scheme, + } + }); + + Ok(DetachedWindow { + id, + label: pending.label, + dispatcher: MockWindowDispatcher { + id, + context: self.context.clone(), + }, + webview, + }) + } + + fn create_webview( + &self, + window_id: WindowId, + pending: PendingWebview, + ) -> Result> { + let id = self.context.next_webview_id(); + let webview = Webview; + if let Some(w) = self.context.windows.borrow_mut().get_mut(&window_id) { + w.webviews.push(webview); + } + + Ok(DetachedWebview { + label: pending.label, + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + last_evaluated_script: Default::default(), + url: Arc::new(Mutex::new(pending.url)), + }, + }) + } + + /// Run a task on the main thread. + fn run_on_main_thread(&self, f: F) -> Result<()> { + self.context.send_message(Message::Task(Box::new(f))) + } + + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + #[cfg(target_os = "linux")] + return Ok(unsafe { + raw_window_handle::DisplayHandle::borrow_raw(raw_window_handle::RawDisplayHandle::Xlib( + raw_window_handle::XlibDisplayHandle::new(None, 0), + )) + }); + #[cfg(target_os = "macos")] + return Ok(unsafe { + raw_window_handle::DisplayHandle::borrow_raw(raw_window_handle::RawDisplayHandle::AppKit( + raw_window_handle::AppKitDisplayHandle::new(), + )) + }); + #[cfg(windows)] + return Ok(unsafe { + raw_window_handle::DisplayHandle::borrow_raw(raw_window_handle::RawDisplayHandle::Windows( + raw_window_handle::WindowsDisplayHandle::new(), + )) + }); + #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))] + unimplemented!(); + } + + fn primary_monitor(&self) -> Result> { + unimplemented!() + } + + fn monitor_from_point(&self, x: f64, y: f64) -> Result> { + unimplemented!() + } + + fn available_monitors(&self) -> Result> { + unimplemented!() + } + + fn set_theme(&self, theme: Option) { + unimplemented!() + } + + /// Shows the application, but does not automatically focus it. + #[cfg(target_os = "macos")] + fn show(&self) -> Result<()> { + Ok(()) + } + + /// Hides the application. + #[cfg(target_os = "macos")] + fn hide(&self) -> Result<()> { + Ok(()) + } + + fn set_device_event_filter(&self, _: DeviceEventFilter) { + // no-op + } + + #[cfg(target_os = "android")] + fn find_class<'a>( + &self, + env: &mut jni::JNIEnv<'a>, + activity: &jni::objects::JObject<'_>, + name: impl Into, + ) -> std::result::Result, jni::errors::Error> { + todo!() + } + + #[cfg(target_os = "android")] + fn run_on_android_context(&self, f: F) + where + F: FnOnce(&mut jni::JNIEnv<'_>, &jni::objects::JObject<'_>, &jni::objects::JObject<'_>) + + Send + + 'static, + { + todo!() + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] + fn fetch_data_store_identifiers) + Send + 'static>( + &self, + cb: F, + ) -> Result<()> { + todo!() + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] + fn remove_data_store) + Send + 'static>( + &self, + uuid: [u8; 16], + cb: F, + ) -> Result<()> { + todo!() + } + + fn cursor_position(&self) -> Result> { + Ok(PhysicalPosition::new(0.0, 0.0)) + } +} + +#[derive(Debug, Clone)] +pub struct MockWebviewDispatcher { + id: u32, + context: RuntimeContext, + url: Arc>, + last_evaluated_script: Arc>>, +} + +impl MockWebviewDispatcher { + pub fn last_evaluated_script(&self) -> Option { + self.last_evaluated_script.lock().unwrap().clone() + } +} + +#[derive(Debug, Clone)] +pub struct MockWindowDispatcher { + id: WindowId, + context: RuntimeContext, +} + +#[derive(Debug, Clone)] +pub struct MockWindowBuilder {} + +impl WindowBuilderBase for MockWindowBuilder {} + +impl WindowBuilder for MockWindowBuilder { + fn new() -> Self { + Self {} + } + + fn with_config(config: &WindowConfig) -> Self { + Self {} + } + + fn center(self) -> Self { + self + } + + fn position(self, x: f64, y: f64) -> Self { + self + } + + fn inner_size(self, min_width: f64, min_height: f64) -> Self { + self + } + + fn min_inner_size(self, min_width: f64, min_height: f64) -> Self { + self + } + + fn max_inner_size(self, max_width: f64, max_height: f64) -> Self { + self + } + + fn inner_size_constraints( + self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> Self { + self + } + + fn prevent_overflow(self) -> Self { + self + } + + fn prevent_overflow_with_margin(self, margin: tauri_runtime::dpi::Size) -> Self { + self + } + + fn resizable(self, resizable: bool) -> Self { + self + } + + fn maximizable(self, resizable: bool) -> Self { + self + } + + fn minimizable(self, resizable: bool) -> Self { + self + } + + fn closable(self, resizable: bool) -> Self { + self + } + + fn title>(self, title: S) -> Self { + self + } + + fn fullscreen(self, fullscreen: bool) -> Self { + self + } + + fn focused(self, focused: bool) -> Self { + self + } + + fn focusable(self, focusable: bool) -> Self { + self + } + + fn maximized(self, maximized: bool) -> Self { + self + } + + fn visible(self, visible: bool) -> Self { + self + } + + #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] + #[cfg_attr( + docsrs, + doc(cfg(any(not(target_os = "macos"), feature = "macos-private-api"))) + )] + fn transparent(self, transparent: bool) -> Self { + self + } + + fn decorations(self, decorations: bool) -> Self { + self + } + + fn always_on_bottom(self, always_on_bottom: bool) -> Self { + self + } + + fn always_on_top(self, always_on_top: bool) -> Self { + self + } + + fn visible_on_all_workspaces(self, visible_on_all_workspaces: bool) -> Self { + self + } + + fn content_protected(self, protected: bool) -> Self { + self + } + + fn icon(self, icon: Icon<'_>) -> Result { + Ok(self) + } + + fn skip_taskbar(self, skip: bool) -> Self { + self + } + + fn window_classname>(self, classname: S) -> Self { + self + } + + fn no_redirection_bitmap(self, enable: bool) -> Self { + self + } + + fn shadow(self, enable: bool) -> Self { + self + } + + #[cfg(windows)] + fn owner(self, owner: HWND) -> Self { + self + } + + #[cfg(windows)] + fn parent(self, parent: HWND) -> Self { + self + } + + #[cfg(target_os = "macos")] + fn parent(self, parent: *mut std::ffi::c_void) -> Self { + self + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn transient_for(self, parent: &impl gtk::glib::IsA) -> Self { + self + } + + #[cfg(windows)] + fn drag_and_drop(self, enabled: bool) -> Self { + self + } + + #[cfg(target_os = "macos")] + fn title_bar_style(self, style: TitleBarStyle) -> Self { + self + } + + #[cfg(target_os = "macos")] + fn traffic_light_position>(self, position: P) -> Self { + self + } + + #[cfg(target_os = "macos")] + fn hidden_title(self, transparent: bool) -> Self { + self + } + + #[cfg(target_os = "macos")] + fn tabbing_identifier(self, identifier: &str) -> Self { + self + } + + fn theme(self, theme: Option) -> Self { + self + } + + fn has_icon(&self) -> bool { + false + } + + fn get_theme(&self) -> Option { + None + } + + 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 { + type Runtime = MockRuntime; + + fn run_on_main_thread(&self, f: F) -> Result<()> { + self.context.send_message(Message::Task(Box::new(f))) + } + + fn on_webview_event( + &self, + f: F, + ) -> tauri_runtime::WebviewEventId { + self.context.next_window_event_id() + } + + fn with_webview) + Send + 'static>(&self, f: F) -> Result<()> { + Ok(()) + } + + #[cfg(any(debug_assertions, feature = "devtools"))] + fn open_devtools(&self) {} + + #[cfg(any(debug_assertions, feature = "devtools"))] + fn close_devtools(&self) {} + + #[cfg(any(debug_assertions, feature = "devtools"))] + fn is_devtools_open(&self) -> Result { + Ok(false) + } + + fn set_zoom(&self, scale_factor: f64) -> Result<()> { + Ok(()) + } + + fn eval_script>(&self, script: S) -> Result<()> { + self + .last_evaluated_script + .lock() + .unwrap() + .replace(script.into()); + 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()) + } + + fn bounds(&self) -> Result { + Ok(tauri_runtime::dpi::Rect::default()) + } + + fn position(&self) -> Result> { + Ok(PhysicalPosition { x: 0, y: 0 }) + } + + fn size(&self) -> Result> { + Ok(PhysicalSize { + width: 0, + height: 0, + }) + } + + fn navigate(&self, url: Url) -> Result<()> { + *self.url.lock().unwrap() = url.to_string(); + Ok(()) + } + + fn reload(&self) -> Result<()> { + Ok(()) + } + + fn print(&self) -> Result<()> { + Ok(()) + } + + fn close(&self) -> Result<()> { + Ok(()) + } + + fn set_bounds(&self, bounds: tauri_runtime::dpi::Rect) -> Result<()> { + Ok(()) + } + + fn set_size(&self, _size: Size) -> Result<()> { + Ok(()) + } + + fn set_position(&self, _position: Position) -> Result<()> { + Ok(()) + } + + fn set_focus(&self) -> Result<()> { + Ok(()) + } + + fn reparent(&self, window_id: WindowId) -> Result<()> { + Ok(()) + } + + fn cookies(&self) -> Result>> { + Ok(Vec::new()) + } + + fn cookies_for_url(&self, url: Url) -> Result>> { + Ok(Vec::new()) + } + + fn set_cookie(&self, cookie: tauri_runtime::Cookie<'_>) -> Result<()> { + Ok(()) + } + + fn delete_cookie(&self, cookie: tauri_runtime::Cookie<'_>) -> Result<()> { + Ok(()) + } + + fn set_auto_resize(&self, auto_resize: bool) -> Result<()> { + Ok(()) + } + + fn clear_all_browsing_data(&self) -> Result<()> { + Ok(()) + } + + fn hide(&self) -> Result<()> { + Ok(()) + } + + fn show(&self) -> Result<()> { + Ok(()) + } + + fn set_background_color(&self, color: Option) -> Result<()> { + Ok(()) + } +} + +impl WindowDispatch for MockWindowDispatcher { + type Runtime = MockRuntime; + + type WindowBuilder = MockWindowBuilder; + + fn run_on_main_thread(&self, f: F) -> Result<()> { + self.context.send_message(Message::Task(Box::new(f))) + } + + fn on_window_event(&self, f: F) -> WindowEventId { + self.context.next_window_event_id() + } + + fn scale_factor(&self) -> Result { + Ok(1.0) + } + + fn inner_position(&self) -> Result> { + Ok(PhysicalPosition { x: 0, y: 0 }) + } + + fn outer_position(&self) -> Result> { + Ok(PhysicalPosition { x: 0, y: 0 }) + } + + fn inner_size(&self) -> Result> { + Ok(PhysicalSize { + width: 0, + height: 0, + }) + } + + fn outer_size(&self) -> Result> { + Ok(PhysicalSize { + width: 0, + height: 0, + }) + } + + fn is_fullscreen(&self) -> Result { + Ok(false) + } + + fn is_minimized(&self) -> Result { + Ok(false) + } + + fn is_maximized(&self) -> Result { + Ok(false) + } + + fn is_focused(&self) -> Result { + Ok(false) + } + + fn is_decorated(&self) -> Result { + Ok(false) + } + + fn is_resizable(&self) -> Result { + Ok(false) + } + + fn is_maximizable(&self) -> Result { + Ok(true) + } + + fn is_minimizable(&self) -> Result { + Ok(true) + } + + fn is_closable(&self) -> Result { + Ok(true) + } + + fn is_visible(&self) -> Result { + Ok(true) + } + + fn title(&self) -> Result { + Ok(String::new()) + } + + fn current_monitor(&self) -> Result> { + Ok(None) + } + + fn primary_monitor(&self) -> Result> { + Ok(None) + } + + fn monitor_from_point(&self, x: f64, y: f64) -> Result> { + Ok(None) + } + + fn available_monitors(&self) -> Result> { + Ok(Vec::new()) + } + + fn theme(&self) -> Result { + Ok(Theme::Light) + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn gtk_window(&self) -> Result { + unimplemented!() + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn default_vbox(&self) -> Result { + 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> { + #[cfg(target_os = "linux")] + return unsafe { + Ok(raw_window_handle::WindowHandle::borrow_raw( + raw_window_handle::RawWindowHandle::Xlib(raw_window_handle::XlibWindowHandle::new(0)), + )) + }; + #[cfg(target_os = "macos")] + return unsafe { + Ok(raw_window_handle::WindowHandle::borrow_raw( + raw_window_handle::RawWindowHandle::AppKit(raw_window_handle::AppKitWindowHandle::new( + std::ptr::NonNull::from(&()).cast(), + )), + )) + }; + #[cfg(windows)] + return unsafe { + Ok(raw_window_handle::WindowHandle::borrow_raw( + raw_window_handle::RawWindowHandle::Win32(raw_window_handle::Win32WindowHandle::new( + std::num::NonZeroIsize::MIN, + )), + )) + }; + #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))] + unimplemented!(); + } + + fn center(&self) -> Result<()> { + Ok(()) + } + + fn request_user_attention(&self, request_type: Option) -> Result<()> { + Ok(()) + } + + fn create_window) + Send + 'static>( + &mut self, + pending: PendingWindow, + _after_window_creation: Option, + ) -> Result> { + let id = self.context.next_window_id(); + + let (webview_id, webviews) = if let Some(w) = &pending.webview { + (Some(self.context.next_webview_id()), vec![Webview]) + } else { + (None, Vec::new()) + }; + + self.context.windows.borrow_mut().insert( + id, + Window { + label: pending.label.clone(), + webviews, + }, + ); + + let webview = webview_id.map(|id| { + let pending_webview = pending.webview.unwrap(); + DetachedWindowWebview { + webview: DetachedWebview { + label: pending.label.clone(), + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + url: Arc::new(Mutex::new(pending_webview.url)), + last_evaluated_script: Default::default(), + }, + }, + use_https_scheme: pending_webview.webview_attributes.use_https_scheme, + } + }); + + Ok(DetachedWindow { + id, + label: pending.label, + dispatcher: MockWindowDispatcher { + id, + context: self.context.clone(), + }, + webview, + }) + } + + fn create_webview( + &mut self, + pending: PendingWebview, + ) -> Result> { + let id = self.context.next_webview_id(); + let webview = Webview; + if let Some(w) = self.context.windows.borrow_mut().get_mut(&self.id) { + w.webviews.push(webview); + } + + Ok(DetachedWebview { + label: pending.label, + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + last_evaluated_script: Default::default(), + url: Arc::new(Mutex::new(pending.url)), + }, + }) + } + + fn set_resizable(&self, resizable: bool) -> Result<()> { + Ok(()) + } + + fn set_maximizable(&self, maximizable: bool) -> Result<()> { + Ok(()) + } + + fn set_minimizable(&self, minimizable: bool) -> Result<()> { + Ok(()) + } + + fn set_closable(&self, closable: bool) -> Result<()> { + Ok(()) + } + + fn set_title>(&self, title: S) -> Result<()> { + Ok(()) + } + + fn maximize(&self) -> Result<()> { + Ok(()) + } + + fn unmaximize(&self) -> Result<()> { + Ok(()) + } + + fn minimize(&self) -> Result<()> { + Ok(()) + } + + fn unminimize(&self) -> Result<()> { + Ok(()) + } + + fn show(&self) -> Result<()> { + Ok(()) + } + + fn hide(&self) -> Result<()> { + Ok(()) + } + + fn close(&self) -> Result<()> { + self.context.send_message(Message::CloseWindow(self.id))?; + Ok(()) + } + + fn destroy(&self) -> Result<()> { + self.context.send_message(Message::DestroyWindow(self.id))?; + Ok(()) + } + + fn set_decorations(&self, decorations: bool) -> Result<()> { + Ok(()) + } + + fn set_shadow(&self, shadow: bool) -> Result<()> { + Ok(()) + } + + fn set_always_on_bottom(&self, always_on_bottom: bool) -> Result<()> { + Ok(()) + } + + fn set_always_on_top(&self, always_on_top: bool) -> Result<()> { + Ok(()) + } + + fn set_visible_on_all_workspaces(&self, visible_on_all_workspaces: bool) -> Result<()> { + Ok(()) + } + + fn set_content_protected(&self, protected: bool) -> Result<()> { + Ok(()) + } + + fn set_size(&self, size: Size) -> Result<()> { + Ok(()) + } + + fn set_min_size(&self, size: Option) -> Result<()> { + Ok(()) + } + + fn set_max_size(&self, size: Option) -> Result<()> { + Ok(()) + } + + fn set_position(&self, position: Position) -> Result<()> { + Ok(()) + } + + fn set_fullscreen(&self, fullscreen: bool) -> Result<()> { + Ok(()) + } + + fn set_fullscreen_on_monitor(&self, position: PhysicalPosition) -> Result<()> { + Ok(()) + } + + #[cfg(target_os = "macos")] + fn set_simple_fullscreen(&self, enable: bool) -> Result<()> { + Ok(()) + } + + fn set_focus(&self) -> Result<()> { + Ok(()) + } + + fn set_focusable(&self, focusable: bool) -> Result<()> { + Ok(()) + } + + fn set_icon(&self, icon: Icon<'_>) -> Result<()> { + Ok(()) + } + + fn set_skip_taskbar(&self, skip: bool) -> Result<()> { + Ok(()) + } + + fn set_cursor_grab(&self, grab: bool) -> Result<()> { + Ok(()) + } + + fn set_cursor_visible(&self, visible: bool) -> Result<()> { + Ok(()) + } + + fn set_cursor_icon(&self, icon: CursorIcon) -> Result<()> { + Ok(()) + } + + fn set_cursor_position>(&self, position: Pos) -> Result<()> { + Ok(()) + } + + fn set_ignore_cursor_events(&self, ignore: bool) -> Result<()> { + Ok(()) + } + + fn start_dragging(&self) -> Result<()> { + Ok(()) + } + + fn start_resize_dragging(&self, direction: tauri_runtime::ResizeDirection) -> Result<()> { + Ok(()) + } + + fn set_progress_bar(&self, progress_state: ProgressBarState) -> Result<()> { + Ok(()) + } + + fn set_badge_count(&self, count: Option, desktop_filename: Option) -> Result<()> { + Ok(()) + } + + fn set_badge_label(&self, label: Option) -> Result<()> { + Ok(()) + } + + fn set_overlay_icon(&self, icon: Option>) -> Result<()> { + Ok(()) + } + + fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> Result<()> { + Ok(()) + } + + fn set_traffic_light_position(&self, position: Position) -> Result<()> { + Ok(()) + } + + fn set_size_constraints( + &self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> Result<()> { + Ok(()) + } + + fn set_theme(&self, theme: Option) -> Result<()> { + Ok(()) + } + + fn set_enabled(&self, enabled: bool) -> Result<()> { + Ok(()) + } + + fn is_enabled(&self) -> Result { + Ok(true) + } + + fn is_always_on_top(&self) -> Result { + Ok(false) + } + + fn set_background_color(&self, color: Option) -> Result<()> { + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct EventProxy {} + +impl EventLoopProxy for EventProxy { + fn send_event(&self, event: T) -> Result<()> { + Ok(()) + } +} + +#[derive(Debug)] +pub struct MockRuntime { + is_running: Arc, + pub context: RuntimeContext, + run_rx: Receiver, +} + +impl MockRuntime { + fn init() -> Self { + let is_running = Arc::new(AtomicBool::new(false)); + let (tx, rx) = sync_channel(256); + let context = RuntimeContext { + is_running: is_running.clone(), + windows: Default::default(), + shortcuts: Default::default(), + run_tx: tx, + main_thread_id: std::thread::current().id(), + next_window_id: Default::default(), + next_webview_id: Default::default(), + next_window_event_id: Default::default(), + next_webview_event_id: Default::default(), + }; + Self { + is_running, + context, + run_rx: rx, + } + } +} + +impl Runtime for MockRuntime { + type WindowDispatcher = MockWindowDispatcher; + type WebviewDispatcher = MockWebviewDispatcher; + type Handle = MockRuntimeHandle; + type EventLoopProxy = EventProxy; + + fn new(_args: RuntimeInitArgs) -> Result { + Ok(Self::init()) + } + + #[cfg(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn new_any_thread(_args: RuntimeInitArgs) -> Result { + Ok(Self::init()) + } + + fn create_proxy(&self) -> EventProxy { + EventProxy {} + } + + fn handle(&self) -> Self::Handle { + MockRuntimeHandle { + context: self.context.clone(), + } + } + + fn create_window) + Send + 'static>( + &self, + pending: PendingWindow, + _after_window_creation: Option, + ) -> Result> { + let id = self.context.next_window_id(); + + let (webview_id, webviews) = if let Some(w) = &pending.webview { + (Some(self.context.next_webview_id()), vec![Webview]) + } else { + (None, Vec::new()) + }; + + self.context.windows.borrow_mut().insert( + id, + Window { + label: pending.label.clone(), + webviews, + }, + ); + + let webview = webview_id.map(|id| { + let pending_webview = pending.webview.unwrap(); + DetachedWindowWebview { + webview: DetachedWebview { + label: pending.label.clone(), + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + url: Arc::new(Mutex::new(pending_webview.url)), + last_evaluated_script: Default::default(), + }, + }, + use_https_scheme: pending_webview.webview_attributes.use_https_scheme, + } + }); + + Ok(DetachedWindow { + id, + label: pending.label, + dispatcher: MockWindowDispatcher { + id, + context: self.context.clone(), + }, + webview, + }) + } + + fn create_webview( + &self, + window_id: WindowId, + pending: PendingWebview, + ) -> Result> { + let id = self.context.next_webview_id(); + let webview = Webview; + if let Some(w) = self.context.windows.borrow_mut().get_mut(&window_id) { + w.webviews.push(webview); + } + + Ok(DetachedWebview { + label: pending.label, + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + last_evaluated_script: Default::default(), + url: Arc::new(Mutex::new(pending.url)), + }, + }) + } + + fn primary_monitor(&self) -> Option { + unimplemented!() + } + + fn monitor_from_point(&self, x: f64, y: f64) -> Option { + unimplemented!() + } + + fn available_monitors(&self) -> Vec { + unimplemented!() + } + + fn set_theme(&self, theme: Option) { + unimplemented!() + } + + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_activation_policy(&mut self, activation_policy: tauri_runtime::ActivationPolicy) {} + + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_dock_visibility(&mut self, visible: bool) {} + + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn set_activate_ignoring_other_apps(&mut self, ignore: bool) {} + + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn show(&self) {} + + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + fn hide(&self) {} + + fn set_device_event_filter(&mut self, filter: DeviceEventFilter) {} + + #[cfg(any( + target_os = "macos", + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn run_iteration)>(&mut self, callback: F) {} + + fn run_return) + 'static>(self, callback: F) -> i32 { + self.run(callback); + + 0 + } + + fn run) + 'static>(self, mut callback: F) { + self.is_running.store(true, Ordering::Relaxed); + callback(RunEvent::Ready); + + loop { + if let Ok(m) = self.run_rx.try_recv() { + match m { + Message::Task(p) => p(), + Message::CloseWindow(id) => { + let label = self + .context + .windows + .borrow() + .get(&id) + .map(|w| w.label.clone()); + if let Some(label) = label { + let (tx, rx) = channel(); + callback(RunEvent::WindowEvent { + label, + event: WindowEvent::CloseRequested { signal_tx: tx }, + }); + + let should_prevent = matches!(rx.try_recv(), Ok(true)); + if !should_prevent { + self.context.windows.borrow_mut().remove(&id); + + let is_empty = self.context.windows.borrow().is_empty(); + if is_empty { + let (tx, rx) = channel(); + callback(RunEvent::ExitRequested { code: None, tx }); + + let recv = rx.try_recv(); + let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); + + if !should_prevent { + break; + } + } + } + } + } + Message::DestroyWindow(id) => { + let removed = self.context.windows.borrow_mut().remove(&id).is_some(); + if removed { + let is_empty = self.context.windows.borrow().is_empty(); + if is_empty { + let (tx, rx) = channel(); + callback(RunEvent::ExitRequested { code: None, tx }); + + let recv = rx.try_recv(); + let should_prevent = matches!(recv, Ok(ExitRequestedEventAction::Prevent)); + + if !should_prevent { + break; + } + } + } + } + } + } + + callback(RunEvent::MainEventsCleared); + + std::thread::sleep(std::time::Duration::from_secs(1)); + } + + callback(RunEvent::Exit); + } + + fn cursor_position(&self) -> Result> { + Ok(PhysicalPosition::new(0.0, 0.0)) + } +} diff --git a/crates/tauri/src/test/mod.rs b/crates/tauri/src/test/mod.rs new file mode 100644 index 0000000..60e42d0 --- /dev/null +++ b/crates/tauri/src/test/mod.rs @@ -0,0 +1,338 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Utilities for unit testing on Tauri applications. +//! +//! # Stability +//! +//! This module is unstable. +//! +//! # Examples +//! +//! ```rust +//! use tauri::test::{mock_builder, mock_context, noop_assets}; +//! +//! #[tauri::command] +//! fn ping() -> &'static str { +//! "pong" +//! } +//! +//! fn create_app(builder: tauri::Builder) -> tauri::App { +//! builder +//! .invoke_handler(tauri::generate_handler![ping]) +//! // remove the string argument to use your app's config file +//! .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) +//! .expect("failed to build app") +//! } +//! +//! fn main() { +//! // Use `tauri::Builder::default()` to use the default runtime rather than the `MockRuntime`; +//! // let app = create_app(tauri::Builder::default()); +//! let app = create_app(mock_builder()); +//! let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default()).build().unwrap(); +//! +//! // run the `ping` command and assert it returns `pong` +//! let res = tauri::test::get_ipc_response( +//! &webview, +//! tauri::webview::InvokeRequest { +//! cmd: "ping".into(), +//! callback: tauri::ipc::CallbackFn(0), +//! error: tauri::ipc::CallbackFn(1), +//! // alternatively use "tauri://localhost" +//! url: "http://tauri.localhost".parse().unwrap(), +//! body: tauri::ipc::InvokeBody::default(), +//! headers: Default::default(), +//! invoke_key: tauri::test::INVOKE_KEY.to_string(), +//! }, +//! ).map(|b| b.deserialize::().unwrap()); +//! } +//! ``` + +#![allow(unused_variables)] + +mod mock_runtime; +pub use mock_runtime::*; +use serde::Serialize; +use serialize_to_javascript::DefaultTemplate; + +use std::{borrow::Cow, collections::HashMap, fmt::Debug}; + +use crate::{ + App, Assets, Builder, Context, Pattern, Runtime, Webview, + ipc::{InvokeError, InvokeResponse, InvokeResponseBody}, + webview::InvokeRequest, +}; +use tauri_utils::{ + acl::resolved::Resolved, + assets::{AssetKey, AssetsIter, CspHash}, + config::{AppConfig, Config}, +}; + +/// The invoke key used for tests. +pub const INVOKE_KEY: &str = "__invoke-key__"; + +/// An empty [`Assets`] implementation. +pub struct NoopAsset { + assets: HashMap>, + csp_hashes: Vec>, +} + +impl Assets for NoopAsset { + fn get(&self, key: &AssetKey) -> Option> { + None + } + + fn iter(&self) -> Box> { + Box::new( + self + .assets + .iter() + .map(|(k, b)| (Cow::Borrowed(k.as_str()), Cow::Borrowed(b.as_slice()))), + ) + } + + fn csp_hashes(&self, html_path: &AssetKey) -> Box> + '_> { + Box::new(self.csp_hashes.iter().copied()) + } +} + +/// Creates a new empty [`Assets`] implementation. +pub fn noop_assets() -> NoopAsset { + NoopAsset { + assets: Default::default(), + csp_hashes: Default::default(), + } +} + +/// Creates a new [`crate::Context`] for testing. +pub fn mock_context>(assets: A) -> crate::Context { + Context { + config: Config { + schema: None, + product_name: Default::default(), + main_binary_name: Default::default(), + version: Default::default(), + identifier: Default::default(), + app: AppConfig { + with_global_tauri: Default::default(), + windows: Vec::new(), + security: Default::default(), + tray_icon: None, + macos_private_api: false, + enable_gtk_app_id: false, + }, + bundle: Default::default(), + build: Default::default(), + plugins: Default::default(), + }, + assets: Box::new(assets), + default_window_icon: None, + app_icon: None, + #[cfg(all(desktop, feature = "tray-icon"))] + tray_icon: None, + package_info: crate::PackageInfo { + name: "test".into(), + version: "0.1.0".parse().unwrap(), + authors: "Tauri", + description: "Tauri test", + crate_name: "test", + }, + pattern: Pattern::Brownfield, + runtime_authority: crate::runtime_authority!(Default::default(), Resolved::default()), + plugin_global_api_scripts: None, + + #[cfg(dev)] + config_parent: None, + } +} + +/// Creates a new [`Builder`] using the [`MockRuntime`]. +/// +/// To use a dummy [`Context`], see [`mock_app`]. +/// +/// # Examples +/// +/// ```rust +/// #[cfg(test)] +/// fn do_something() { +/// let app = tauri::test::mock_builder() +/// // remove the string argument to use your app's config file +/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) +/// .unwrap(); +/// } +/// ``` +pub fn mock_builder() -> Builder { + let mut builder = Builder::::new().enable_macos_default_menu(false); + + builder.invoke_initialization_script = crate::app::InvokeInitializationScript { + process_ipc_message_fn: crate::manager::webview::PROCESS_IPC_MESSAGE_FN, + os_name: std::env::consts::OS, + fetch_channel_data_command: crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND, + invoke_key: INVOKE_KEY, + } + .render_default(&Default::default()) + .unwrap() + .into_string(); + + builder.invoke_key = INVOKE_KEY.to_string(); + + builder +} + +/// Creates a new [`App`] for testing using the [`mock_context`] with a [`noop_assets`]. +pub fn mock_app() -> App { + mock_builder().build(mock_context(noop_assets())).unwrap() +} + +/// Executes the given IPC message and assert the response matches the expected value. +/// +/// # Examples +/// +/// ```rust +/// use tauri::test::{mock_builder, mock_context, noop_assets}; +/// +/// #[tauri::command] +/// fn ping() -> &'static str { +/// "pong" +/// } +/// +/// fn create_app(builder: tauri::Builder) -> tauri::App { +/// builder +/// .invoke_handler(tauri::generate_handler![ping]) +/// // remove the string argument to use your app's config file +/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) +/// .expect("failed to build app") +/// } +/// +/// fn main() { +/// let app = create_app(mock_builder()); +/// let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default()).build().unwrap(); +/// +/// // run the `ping` command and assert it returns `pong` +/// tauri::test::assert_ipc_response( +/// &webview, +/// tauri::webview::InvokeRequest { +/// cmd: "ping".into(), +/// callback: tauri::ipc::CallbackFn(0), +/// error: tauri::ipc::CallbackFn(1), +/// 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(), +/// }, +/// Ok("pong") +/// ); +/// } +/// ``` +pub fn assert_ipc_response< + T: Serialize + Debug + Send + Sync + 'static, + W: AsRef>, +>( + webview: &W, + request: InvokeRequest, + expected: Result, +) { + let response = + get_ipc_response(webview, request).map(|b| b.deserialize::().unwrap()); + assert_eq!( + response, + expected + .map(|e| serde_json::to_value(e).unwrap()) + .map_err(|e| serde_json::to_value(e).unwrap()) + ); +} + +#[allow(clippy::needless_doctest_main)] +/// Executes the given IPC message and get the return value. +/// +/// # Examples +/// +/// ```rust +/// use tauri::test::{mock_builder, mock_context, noop_assets}; +/// +/// #[tauri::command] +/// fn ping() -> &'static str { +/// "pong" +/// } +/// +/// fn create_app(builder: tauri::Builder) -> tauri::App { +/// builder +/// .invoke_handler(tauri::generate_handler![ping]) +/// // remove the string argument to use your app's config file +/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) +/// .expect("failed to build app") +/// } +/// +/// fn main() { +/// let app = create_app(mock_builder()); +/// let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default()).build().unwrap(); +/// +/// // run the `ping` command and assert it returns `pong` +/// let res = tauri::test::get_ipc_response( +/// &webview, +/// tauri::webview::InvokeRequest { +/// cmd: "ping".into(), +/// callback: tauri::ipc::CallbackFn(0), +/// error: tauri::ipc::CallbackFn(1), +/// 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(), +/// }, +/// ); +/// assert!(res.is_ok()); +/// assert_eq!(res.unwrap().deserialize::().unwrap(), String::from("pong")); +/// } +///``` +pub fn get_ipc_response>>( + webview: &W, + request: InvokeRequest, +) -> Result { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + webview.as_ref().clone().on_message( + request, + Box::new(move |_window, _cmd, response, _callback, _error| { + tx.send(response).unwrap(); + }), + ); + + let res = rx.recv().expect("Failed to receive result from command"); + match res { + InvokeResponse::Ok(b) => Ok(b), + InvokeResponse::Err(InvokeError(v)) => Err(v), + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::mock_app; + + #[test] + fn run_app() { + let app = mock_app(); + + let w = crate::WebviewWindowBuilder::new(&app, "main", Default::default()) + .build() + .unwrap(); + + std::thread::spawn(move || { + std::thread::sleep(Duration::from_secs(1)); + w.close().unwrap(); + }); + + app.run(|_app, event| { + println!("{event:?}"); + }); + } +} diff --git a/crates/tauri/src/tray/mod.rs b/crates/tauri/src/tray/mod.rs new file mode 100644 index 0000000..dea5fef --- /dev/null +++ b/crates/tauri/src/tray/mod.rs @@ -0,0 +1,701 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Tray icon types and utilities. + +pub(crate) mod plugin; + +use crate::app::{GlobalMenuEventListener, GlobalTrayIconEventListener}; +use crate::menu::ContextMenu; +use crate::menu::MenuEvent; +use crate::resources::Resource; +use crate::{ + AppHandle, Manager, PhysicalPosition, Rect, Runtime, image::Image, menu::run_item_main_thread, +}; +use crate::{ResourceId, UnsafeSend}; +use serde::Serialize; +use std::path::Path; +pub use tray_icon::TrayIconId; + +/// Describes the mouse button state. +#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Serialize)] +pub enum MouseButtonState { + /// Mouse button pressed. + #[default] + Up, + /// Mouse button released. + Down, +} + +impl From for MouseButtonState { + fn from(value: tray_icon::MouseButtonState) -> Self { + match value { + tray_icon::MouseButtonState::Up => MouseButtonState::Up, + tray_icon::MouseButtonState::Down => MouseButtonState::Down, + } + } +} + +/// Describes which mouse button triggered the event.. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Default)] +pub enum MouseButton { + /// Left mouse button. + #[default] + Left, + /// Right mouse button. + Right, + /// Middle mouse button. + Middle, +} + +impl From for MouseButton { + fn from(value: tray_icon::MouseButton) -> Self { + match value { + tray_icon::MouseButton::Left => MouseButton::Left, + tray_icon::MouseButton::Right => MouseButton::Right, + tray_icon::MouseButton::Middle => MouseButton::Middle, + } + } +} + +/// Describes a tray icon event. +/// +/// ## Platform-specific: +/// +/// - **Linux**: Unsupported. The event is not emitted even though the icon is shown +/// and will still show a context menu on right click. +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "type")] +#[non_exhaustive] +pub enum TrayIconEvent { + /// A click happened on the tray icon. + #[serde(rename_all = "camelCase")] + Click { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + /// Mouse button that triggered this event. + button: MouseButton, + /// Mouse button state when this event was triggered. + button_state: MouseButtonState, + }, + /// A double click happened on the tray icon. **Windows Only** + DoubleClick { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + /// Mouse button that triggered this event. + button: MouseButton, + }, + /// The mouse entered the tray icon region. + Enter { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + }, + /// The mouse moved over the tray icon region. + Move { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + }, + /// The mouse left the tray icon region. + Leave { + /// Id of the tray icon which triggered this event. + id: TrayIconId, + /// Physical Position of this event. + position: PhysicalPosition, + /// Position and size of the tray icon. + rect: Rect, + }, +} + +impl TrayIconEvent { + /// Get the id of the tray icon that triggered this event. + pub fn id(&self) -> &TrayIconId { + match self { + TrayIconEvent::Click { id, .. } => id, + TrayIconEvent::DoubleClick { id, .. } => id, + TrayIconEvent::Enter { id, .. } => id, + TrayIconEvent::Move { id, .. } => id, + TrayIconEvent::Leave { id, .. } => id, + } + } +} + +impl From for TrayIconEvent { + fn from(value: tray_icon::TrayIconEvent) -> Self { + match value { + tray_icon::TrayIconEvent::Click { + id, + position, + rect, + button, + button_state, + } => TrayIconEvent::Click { + id, + position, + rect: Rect { + position: rect.position.into(), + size: rect.size.into(), + }, + button: button.into(), + button_state: button_state.into(), + }, + tray_icon::TrayIconEvent::DoubleClick { + id, + position, + rect, + button, + } => TrayIconEvent::DoubleClick { + id, + position, + rect: Rect { + position: rect.position.into(), + size: rect.size.into(), + }, + button: button.into(), + }, + tray_icon::TrayIconEvent::Enter { id, position, rect } => TrayIconEvent::Enter { + id, + position, + rect: Rect { + position: rect.position.into(), + size: rect.size.into(), + }, + }, + tray_icon::TrayIconEvent::Move { id, position, rect } => TrayIconEvent::Move { + id, + position, + rect: Rect { + position: rect.position.into(), + size: rect.size.into(), + }, + }, + tray_icon::TrayIconEvent::Leave { id, position, rect } => TrayIconEvent::Leave { + id, + position, + rect: Rect { + position: rect.position.into(), + size: rect.size.into(), + }, + }, + _ => todo!(), + } + } +} + +/// [`TrayIcon`] builder struct and associated methods. +#[derive(Default)] +pub struct TrayIconBuilder { + on_menu_event: Option>>, + on_tray_icon_event: Option>>, + inner: tray_icon::TrayIconBuilder, +} + +impl TrayIconBuilder { + /// Creates a new tray icon builder. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. + /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + pub fn new() -> Self { + Self { + inner: tray_icon::TrayIconBuilder::new(), + on_menu_event: None, + on_tray_icon_event: None, + } + } + + /// Creates a new tray icon builder with the specified id. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. + /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + pub fn with_id>(id: I) -> Self { + let mut builder = Self::new(); + builder.inner = builder.inner.with_id(id); + builder + } + + /// Set the a menu for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux**: once a menu is set, it cannot be removed or replaced but you can change its content. + pub fn menu(mut self, menu: &M) -> Self { + self.inner = self.inner.with_menu(menu.inner_context_owned()); + self + } + + /// Set an icon for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. + /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + pub fn icon(mut self, icon: Image<'_>) -> Self { + let icon = icon.try_into().ok(); + if let Some(icon) = icon { + self.inner = self.inner.with_icon(icon); + } + self + } + + /// Set a tooltip for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn tooltip>(mut self, s: S) -> Self { + self.inner = self.inner.with_tooltip(s); + self + } + + /// Set the tray icon title. + /// + /// ## Platform-specific + /// + /// - **Linux:** The title will not be shown unless there is an icon + /// as well. The title is useful for numerical and other frequently + /// updated information. In general, it shouldn't be shown unless a + /// user requests it as it can take up a significant amount of space + /// on the user's panel. This may not be shown in all visualizations. + /// - **Windows:** Unsupported. + pub fn title>(mut self, title: S) -> Self { + self.inner = self.inner.with_title(title); + self + } + + /// Set tray icon temp dir path. **Linux only**. + /// + /// On Linux, we need to write the icon to the disk and usually it will + /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + pub fn temp_dir_path>(mut self, s: P) -> Self { + self.inner = self.inner.with_temp_dir_path(s); + self + } + + /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. + pub fn icon_as_template(mut self, is_template: bool) -> Self { + self.inner = self.inner.with_icon_as_template(is_template); + self + } + + /// Whether to show the tray menu on left click or not, default is `true`. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + #[deprecated( + since = "2.2.0", + note = "Use `TrayIconBuilder::show_menu_on_left_click` instead." + )] + pub fn menu_on_left_click(mut self, enable: bool) -> Self { + self.inner = self.inner.with_menu_on_left_click(enable); + self + } + + /// Whether to show the tray menu on left click or not, default is `true`. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn show_menu_on_left_click(mut self, enable: bool) -> Self { + self.inner = self.inner.with_menu_on_left_click(enable); + self + } + + /// Set a handler for menu events. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + pub fn on_menu_event, MenuEvent) + Sync + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.on_menu_event.replace(Box::new(f)); + self + } + + /// Set a handler for this tray icon events. + pub fn on_tray_icon_event, TrayIconEvent) + Sync + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.on_tray_icon_event.replace(Box::new(f)); + self + } + + /// Access the unique id that will be assigned to the tray icon + /// this builder will create. + pub fn id(&self) -> &TrayIconId { + self.inner.id() + } + + pub(crate) fn build_inner( + self, + app_handle: &AppHandle, + ) -> crate::Result<(TrayIcon, ResourceId)> { + let id = self.id().clone(); + + // SAFETY: + // the menu within this builder was created on main thread + // and will be accessed on the main thread + let unsafe_builder = UnsafeSend(self.inner); + + let (tx, rx) = std::sync::mpsc::channel(); + let unsafe_tray = app_handle + .run_on_main_thread(move || { + // SAFETY: will only be accessed on main thread + let _ = tx.send(unsafe_builder.take().build().map(UnsafeSend)); + }) + .and_then(|_| rx.recv().map_err(|_| crate::Error::FailedToReceiveMessage))??; + + let icon = TrayIcon { + id, + inner: unsafe_tray.take(), + app_handle: app_handle.clone(), + }; + + let rid = icon.register( + &icon.app_handle, + self.on_menu_event, + self.on_tray_icon_event, + ); + + Ok((icon, rid)) + } + + /// Builds and adds a new [`TrayIcon`] to the system tray. + pub fn build>(self, manager: &M) -> crate::Result> { + let (icon, _rid) = self.build_inner(manager.app_handle())?; + Ok(icon) + } +} + +/// Tray icon struct and associated methods. +/// +/// This type is reference-counted and the icon is removed when the last instance is dropped. +/// +/// See [`TrayIconBuilder`] to construct this type. +#[tauri_macros::default_runtime(crate::Wry, wry)] +pub struct TrayIcon { + id: TrayIconId, + inner: tray_icon::TrayIcon, + app_handle: AppHandle, +} + +impl Clone for TrayIcon { + fn clone(&self) -> Self { + Self { + id: self.id.clone(), + inner: self.inner.clone(), + app_handle: self.app_handle.clone(), + } + } +} + +/// # Safety +/// +/// We make sure it always runs on the main thread. +unsafe impl Sync for TrayIcon {} +unsafe impl Send for TrayIcon {} + +impl TrayIcon { + fn register( + &self, + app_handle: &AppHandle, + on_menu_event: Option>>, + on_tray_icon_event: Option>>, + ) -> ResourceId { + if let Some(handler) = on_menu_event { + app_handle + .manager + .menu + .global_event_listeners + .lock() + .unwrap() + .push(handler); + } + + if let Some(handler) = on_tray_icon_event { + app_handle + .manager + .tray + .event_listeners + .lock() + .unwrap() + .insert(self.id.clone(), handler); + } + + let rid = app_handle.resources_table().add(self.clone()); + app_handle + .manager + .tray + .icons + .lock() + .unwrap() + .push((self.id().clone(), rid)); + rid + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.app_handle + } + + /// Register a handler for menu events. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + pub fn on_menu_event, MenuEvent) + Sync + Send + 'static>(&self, f: F) { + self + .app_handle + .manager + .menu + .global_event_listeners + .lock() + .unwrap() + .push(Box::new(f)); + } + + /// Register a handler for this tray icon events. + pub fn on_tray_icon_event, TrayIconEvent) + Sync + Send + 'static>( + &self, + f: F, + ) { + self + .app_handle + .manager + .tray + .event_listeners + .lock() + .unwrap() + .insert(self.id.clone(), Box::new(f)); + } + + /// Returns the id associated with this tray icon. + pub fn id(&self) -> &TrayIconId { + &self.id + } + + /// Sets a new tray icon. If `None` is provided, it will remove the icon. + pub fn set_icon(&self, icon: Option>) -> crate::Result<()> { + let icon = match icon { + Some(i) => Some(i.try_into()?), + None => None, + }; + run_item_main_thread!(self, |self_: Self| self_.inner.set_icon(icon))?.map_err(Into::into) + } + + /// Sets a new tray menu. + /// + /// ## Platform-specific: + /// + /// - **Linux**: once a menu is set it cannot be removed so `None` has no effect + pub fn set_menu(&self, menu: Option) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| { + self_.inner.set_menu(menu.map(|m| m.inner_context_owned())) + }) + } + + /// Sets the tooltip for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported + pub fn set_tooltip>(&self, tooltip: Option) -> crate::Result<()> { + let s = tooltip.map(|s| s.as_ref().to_string()); + run_item_main_thread!(self, |self_: Self| self_.inner.set_tooltip(s))?.map_err(Into::into) + } + + /// Sets the title for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** The title will not be shown unless there is an icon + /// as well. The title is useful for numerical and other frequently + /// updated information. In general, it shouldn't be shown unless a + /// user requests it as it can take up a significant amount of space + /// on the user's panel. This may not be shown in all visualizations. + /// - **Windows:** Unsupported + pub fn set_title>(&self, title: Option) -> crate::Result<()> { + let s = title.map(|s| s.as_ref().to_string()); + run_item_main_thread!(self, |self_: Self| self_.inner.set_title(s)) + } + + /// Show or hide this tray icon. + pub fn set_visible(&self, visible: bool) -> crate::Result<()> { + run_item_main_thread!(self, |self_: Self| self_.inner.set_visible(visible))?.map_err(Into::into) + } + + /// Sets the tray icon temp dir path. **Linux only**. + /// + /// On Linux, we need to write the icon to the disk and usually it will + /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + pub fn set_temp_dir_path>(&self, path: Option

) -> crate::Result<()> { + #[allow(unused)] + let p = path.map(|p| p.as_ref().to_path_buf()); + #[cfg(target_os = "linux")] + run_item_main_thread!(self, |self_: Self| self_.inner.set_temp_dir_path(p))?; + Ok(()) + } + + /// Sets the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. + pub fn set_icon_as_template(&self, #[allow(unused)] is_template: bool) -> crate::Result<()> { + #[cfg(target_os = "macos")] + run_item_main_thread!(self, |self_: Self| { + self_.inner.set_icon_as_template(is_template) + })?; + 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. + /// + /// + /// ## Platform-specific: + /// + /// - **Linux**: Unsupported. + pub fn set_show_menu_on_left_click(&self, #[allow(unused)] enable: bool) -> crate::Result<()> { + #[cfg(any(target_os = "macos", windows))] + run_item_main_thread!(self, |self_: Self| { + self_.inner.set_show_menu_on_left_click(enable) + })?; + Ok(()) + } + + /// Get tray icon rect. + /// + /// ## Platform-specific: + /// + /// - **Linux**: Unsupported, always returns `None`. + pub fn rect(&self) -> crate::Result> { + run_item_main_thread!(self, |self_: Self| { + self_.inner.rect().map(|rect| Rect { + position: rect.position.into(), + size: rect.size.into(), + }) + }) + } + + /// Do something with the inner [`tray_icon::TrayIcon`] on main thread + /// + /// Note that `tray-icon` crate may be updated in minor releases of Tauri. + /// Therefore, it’s recommended to pin Tauri to at least a minor version when you’re using `with_inner_tray_icon`. + pub fn with_inner_tray_icon(&self, f: F) -> crate::Result + where + F: FnOnce(&tray_icon::TrayIcon) -> T + Send + 'static, + T: Send + 'static, + { + run_item_main_thread!(self, |self_: Self| { f(&self_.inner) }) + } +} + +impl Resource for TrayIcon { + fn close(self: std::sync::Arc) { + let mut icons = self.app_handle.manager.tray.icons.lock().unwrap(); + for (i, (tray_icon_id, _rid)) in icons.iter_mut().enumerate() { + if tray_icon_id == &self.id { + icons.swap_remove(i); + return; + } + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn tray_event_json_serialization() { + // NOTE: if this test is ever changed, you probably need to change `TrayIconEvent` in JS as well + + use super::*; + let event = TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Down, + id: TrayIconId::new("id"), + position: crate::PhysicalPosition::default(), + rect: crate::Rect { + position: tray_icon::Rect::default().position.into(), + size: tray_icon::Rect::default().size.into(), + }, + }; + + let value = serde_json::to_value(&event).unwrap(); + assert_eq!( + value, + serde_json::json!({ + "type": "Click", + "button": "Left", + "buttonState": "Down", + "id": "id", + "position": { + "x": 0.0, + "y": 0.0, + }, + "rect": { + "size": { + "Physical": { + "width": 0, + "height": 0, + } + }, + "position": { + "Physical": { + "x": 0, + "y": 0, + } + }, + } + }) + ); + } +} diff --git a/crates/tauri/src/tray/plugin.rs b/crates/tauri/src/tray/plugin.rs new file mode 100644 index 0000000..aed4873 --- /dev/null +++ b/crates/tauri/src/tray/plugin.rs @@ -0,0 +1,253 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Context; +use serde::Deserialize; + +use crate::{ + AppHandle, Manager, Runtime, Webview, command, + image::JsImage, + ipc::Channel, + menu::{Menu, Submenu, plugin::ItemKind}, + plugin::{Builder, TauriPlugin}, + resources::ResourceId, + tray::TrayIconBuilder, +}; + +use super::{TrayIcon, TrayIconEvent}; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TrayIconOptions { + id: Option, + menu: Option<(ResourceId, ItemKind)>, + icon: Option, + tooltip: Option, + title: Option, + temp_dir_path: Option, + icon_as_template: Option, + menu_on_left_click: Option, + show_menu_on_left_click: Option, +} + +#[command(root = "crate")] +fn new( + webview: Webview, + options: TrayIconOptions, + handler: Channel, +) -> crate::Result<(ResourceId, String)> { + let mut builder = if let Some(id) = options.id { + TrayIconBuilder::::with_id(id) + } else { + TrayIconBuilder::::new() + }; + + builder = builder.on_tray_icon_event(move |_tray, e| { + let _ = handler.send(e); + }); + + let resources_table = webview.resources_table(); + + if let Some((rid, kind)) = options.menu { + match kind { + ItemKind::Menu => { + let menu = resources_table.get::>(rid)?; + builder = builder.menu(&*menu); + } + ItemKind::Submenu => { + let submenu = resources_table.get::>(rid)?; + builder = builder.menu(&*submenu); + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + } + if let Some(icon) = options.icon { + builder = builder.icon(Arc::unwrap_or_clone(icon.into_img(&resources_table)?)); + } + if let Some(tooltip) = options.tooltip { + builder = builder.tooltip(tooltip); + } + if let Some(title) = options.title { + builder = builder.title(title); + } + if let Some(temp_dir_path) = options.temp_dir_path { + builder = builder.temp_dir_path(temp_dir_path); + } + if let Some(icon_as_template) = options.icon_as_template { + builder = builder.icon_as_template(icon_as_template); + } + #[allow(deprecated)] + if let Some(menu_on_left_click) = options.menu_on_left_click { + builder = builder.menu_on_left_click(menu_on_left_click); + } + if let Some(show_menu_on_left_click) = options.show_menu_on_left_click { + builder = builder.show_menu_on_left_click(show_menu_on_left_click); + } + + let (tray, rid) = builder.build_inner(webview.app_handle())?; + let id = tray.id().as_ref().to_string(); + + Ok((rid, id)) +} + +#[command(root = "crate")] +fn get_by_id(app: AppHandle, id: &str) -> Option { + app.manager.tray.tray_resource_by_id(id) +} + +#[command(root = "crate")] +fn remove_by_id(app: AppHandle, id: &str) -> crate::Result<()> { + app + .remove_tray_by_id(id) + .with_context(|| format!("Can't find a tray associated with this id: {id}"))?; + Ok(()) +} + +#[command(root = "crate")] +fn set_icon( + app: AppHandle, + webview: Webview, + rid: ResourceId, + icon: Option, +) -> 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(Arc::unwrap_or_clone(i.into_img(&webview_resources_table)?)), + None => None, + }; + tray.set_icon(icon) +} + +#[command(root = "crate")] +fn set_menu( + app: AppHandle, + webview: Webview, + rid: ResourceId, + menu: Option<(ResourceId, ItemKind)>, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + if let Some((rid, kind)) = menu { + let webview_resources_table = webview.resources_table(); + match kind { + ItemKind::Menu => { + let menu = webview_resources_table.get::>(rid)?; + tray.set_menu(Some((*menu).clone()))?; + } + ItemKind::Submenu => { + let submenu = webview_resources_table.get::>(rid)?; + tray.set_menu(Some((*submenu).clone()))?; + } + _ => return Err(crate::Error::UnexpectedMenuKind), + }; + } else { + tray.set_menu(None::>)?; + } + Ok(()) +} + +#[command(root = "crate")] +fn set_tooltip( + app: AppHandle, + rid: ResourceId, + tooltip: Option, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + tray.set_tooltip(tooltip) +} + +#[command(root = "crate")] +fn set_title( + app: AppHandle, + rid: ResourceId, + title: Option, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + tray.set_title(title) +} + +#[command(root = "crate")] +fn set_visible(app: AppHandle, rid: ResourceId, visible: bool) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + tray.set_visible(visible) +} + +#[command(root = "crate")] +fn set_temp_dir_path( + app: AppHandle, + rid: ResourceId, + path: Option, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + tray.set_temp_dir_path(path) +} + +#[command(root = "crate")] +fn set_icon_as_template( + app: AppHandle, + rid: ResourceId, + as_template: bool, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + 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(Arc::unwrap_or_clone(i.into_img(&webview_resources_table)?)), + None => None, + }; + tray.set_icon_with_as_template(icon, as_template) +} + +#[command(root = "crate")] +fn set_show_menu_on_left_click( + app: AppHandle, + rid: ResourceId, + on_left: bool, +) -> crate::Result<()> { + let resources_table = app.resources_table(); + let tray = resources_table.get::>(rid)?; + tray.set_show_menu_on_left_click(on_left) +} + +pub(crate) fn init() -> TauriPlugin { + Builder::new("tray") + .invoke_handler(crate::generate_handler![ + #![plugin(tray)] + new, + get_by_id, + remove_by_id, + set_icon, + set_menu, + set_tooltip, + set_title, + 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/vibrancy/macos.rs b/crates/tauri/src/vibrancy/macos.rs new file mode 100644 index 0000000..823cab5 --- /dev/null +++ b/crates/tauri/src/vibrancy/macos.rs @@ -0,0 +1,120 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![allow(deprecated)] + +use crate::utils::config::WindowEffectsConfig; +use crate::window::{Effect, EffectState}; +use raw_window_handle::HasWindowHandle; +use window_vibrancy::{ + LiquidGlassOptions, NSGlassEffectViewStyle, NSVisualEffectMaterial, NSVisualEffectState, +}; + +pub fn apply_effects(window: impl HasWindowHandle, effects: WindowEffectsConfig) { + let WindowEffectsConfig { + effects, + radius, + state, + color, + interactive, + } = effects; + + // window-vibrancy inserts a new subview on every call, so drop the previous effect first + clear_effects(&window); + + if let Some(effect) = effects + .iter() + .find(|e| matches!(e, Effect::LiquidGlassRegular | Effect::LiquidGlassClear)) + { + let mut options = LiquidGlassOptions::new(match effect { + Effect::LiquidGlassRegular => NSGlassEffectViewStyle::Regular, + Effect::LiquidGlassClear => NSGlassEffectViewStyle::Clear, + _ => unreachable!(), + }) + .interactive(interactive); + + if let Some(color) = color { + options = options.tint_color(color.into()); + } + if let Some(radius) = radius { + options = options.radius(radius); + } + + match window_vibrancy::apply_liquid_glass(&window, options) { + Ok(()) => return, + // macOS 15 and below: fall back to the Visual Effect material, if any + Err(window_vibrancy::Error::UnsupportedPlatformVersion(_)) => {} + Err(e) => { + log::error!("failed to apply liquid glass effect: {e}"); + return; + } + } + } + + let effect = if let Some(effect) = effects.into_iter().find(|e| { + matches!( + e, + Effect::AppearanceBased + | Effect::Light + | Effect::Dark + | Effect::MediumLight + | Effect::UltraDark + | Effect::Titlebar + | Effect::Selection + | Effect::Menu + | Effect::Popover + | Effect::Sidebar + | Effect::HeaderView + | Effect::Sheet + | Effect::WindowBackground + | Effect::HudWindow + | Effect::FullScreenUI + | Effect::Tooltip + | Effect::ContentBackground + | Effect::UnderWindowBackground + | Effect::UnderPageBackground + ) + }) { + effect + } else { + return; + }; + + window_vibrancy::apply_vibrancy( + window, + match effect { + Effect::AppearanceBased => NSVisualEffectMaterial::AppearanceBased, + Effect::Light => NSVisualEffectMaterial::Light, + Effect::Dark => NSVisualEffectMaterial::Dark, + Effect::MediumLight => NSVisualEffectMaterial::MediumLight, + Effect::UltraDark => NSVisualEffectMaterial::UltraDark, + Effect::Titlebar => NSVisualEffectMaterial::Titlebar, + Effect::Selection => NSVisualEffectMaterial::Selection, + Effect::Menu => NSVisualEffectMaterial::Menu, + Effect::Popover => NSVisualEffectMaterial::Popover, + Effect::Sidebar => NSVisualEffectMaterial::Sidebar, + Effect::HeaderView => NSVisualEffectMaterial::HeaderView, + Effect::Sheet => NSVisualEffectMaterial::Sheet, + Effect::WindowBackground => NSVisualEffectMaterial::WindowBackground, + Effect::HudWindow => NSVisualEffectMaterial::HudWindow, + Effect::FullScreenUI => NSVisualEffectMaterial::FullScreenUI, + Effect::Tooltip => NSVisualEffectMaterial::Tooltip, + Effect::ContentBackground => NSVisualEffectMaterial::ContentBackground, + Effect::UnderWindowBackground => NSVisualEffectMaterial::UnderWindowBackground, + Effect::UnderPageBackground => NSVisualEffectMaterial::UnderPageBackground, + _ => unreachable!(), + }, + state.map(|s| match s { + EffectState::FollowsWindowActiveState => NSVisualEffectState::FollowsWindowActiveState, + EffectState::Active => NSVisualEffectState::Active, + EffectState::Inactive => NSVisualEffectState::Inactive, + }), + radius, + ); +} + +pub fn clear_effects(window: impl HasWindowHandle) { + let _ = window_vibrancy::clear_vibrancy(&window); + let _ = window_vibrancy::clear_liquid_glass(&window); +} diff --git a/crates/tauri/src/vibrancy/mod.rs b/crates/tauri/src/vibrancy/mod.rs new file mode 100644 index 0000000..b5a6740 --- /dev/null +++ b/crates/tauri/src/vibrancy/mod.rs @@ -0,0 +1,32 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![allow(unused)] + +use tauri_utils::config::WindowEffectsConfig; + +use crate::{Runtime, Window}; + +#[cfg(target_os = "macos")] +mod macos; +#[cfg(windows)] +mod windows; + +pub fn set_window_effects( + window: &Window, + effects: Option, +) -> crate::Result<()> { + if let Some(_effects) = effects { + #[cfg(windows)] + windows::apply_effects(window, _effects); + #[cfg(target_os = "macos")] + macos::apply_effects(window, _effects); + } else { + #[cfg(windows)] + windows::clear_effects(window); + #[cfg(target_os = "macos")] + macos::clear_effects(window); + } + Ok(()) +} diff --git a/crates/tauri/src/vibrancy/windows.rs b/crates/tauri/src/vibrancy/windows.rs new file mode 100644 index 0000000..ace786d --- /dev/null +++ b/crates/tauri/src/vibrancy/windows.rs @@ -0,0 +1,53 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] +#![allow(clippy::upper_case_acronyms)] + +use std::ffi::c_void; + +use crate::utils::config::WindowEffectsConfig; +use crate::window::{Color, Effect}; +use raw_window_handle::HasWindowHandle; +use windows::Win32::Foundation::HWND; + +pub fn apply_effects(window: impl HasWindowHandle, effects: WindowEffectsConfig) { + let WindowEffectsConfig { effects, color, .. } = effects; + let effect = if let Some(effect) = effects.iter().find(|e| { + matches!( + e, + Effect::Mica + | Effect::MicaDark + | Effect::MicaLight + | Effect::Acrylic + | Effect::Blur + | Effect::Tabbed + | Effect::TabbedDark + | Effect::TabbedLight + ) + }) { + effect + } else { + return; + }; + + match effect { + Effect::Blur => window_vibrancy::apply_blur(window, color.map(Into::into)), + Effect::Acrylic => window_vibrancy::apply_acrylic(window, color.map(Into::into)), + Effect::Mica => window_vibrancy::apply_mica(window, None), + Effect::MicaDark => window_vibrancy::apply_mica(window, Some(true)), + Effect::MicaLight => window_vibrancy::apply_mica(window, Some(false)), + Effect::Tabbed => window_vibrancy::apply_tabbed(window, None), + Effect::TabbedDark => window_vibrancy::apply_tabbed(window, Some(true)), + Effect::TabbedLight => window_vibrancy::apply_tabbed(window, Some(false)), + _ => unreachable!(), + }; +} + +pub fn clear_effects(window: impl HasWindowHandle) { + window_vibrancy::clear_blur(&window); + window_vibrancy::clear_acrylic(&window); + window_vibrancy::clear_mica(&window); +} diff --git a/crates/tauri/src/webview/mod.rs b/crates/tauri/src/webview/mod.rs new file mode 100644 index 0000000..7fc1778 --- /dev/null +++ b/crates/tauri/src/webview/mod.rs @@ -0,0 +1,2842 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The Tauri webview types and functions. + +pub(crate) mod plugin; +mod webview_window; + +pub use webview_window::{WebviewWindow, WebviewWindowBuilder}; + +/// Cookie crate used for [`Webview::set_cookie`] and [`Webview::delete_cookie`]. +/// +/// # Stability +/// +/// This re-exported crate is still on an alpha release and might receive updates in minor Tauri releases. +pub use cookie; +use http::HeaderMap; +use serde::Serialize; +use tauri_macros::default_runtime; +pub use tauri_runtime::webview::{ + NewWindowFeatures, PageLoadEvent, PermissionKind, PermissionResponse, ScrollBarStyle, +}; +// Remove this re-export in v3 +pub use tauri_runtime::Cookie; +use tauri_runtime::{ + WebviewDispatch, + webview::{DetachedWebview, InitializationScript, PendingWebview, WebviewAttributes}, +}; +#[cfg(desktop)] +use tauri_runtime::{ + WindowDispatch, + dpi::{PhysicalPosition, PhysicalSize, Position, Size}, +}; +pub use tauri_utils::config::Color; +use tauri_utils::config::{BackgroundThrottlingPolicy, WebviewUrl, WindowConfig}; +pub use url::Url; + +use crate::{ + AppHandle, Emitter, Event, EventId, EventLoopMessage, EventName, Listener, Manager, + ResourceTable, Runtime, Window, + app::{UriSchemeResponder, WebviewEvent}, + event::{EmitArgs, EventTarget}, + ipc::{ + CallbackFn, CommandArg, CommandItem, CommandScope, GlobalScope, Invoke, InvokeBody, + InvokeError, InvokeMessage, InvokeResolver, Origin, OwnedInvokeResponder, ScopeObject, + }, + manager::AppManager, + path::SafePathBuf, + sealed::{ManagerBase, RuntimeOrDispatch}, +}; + +use std::{ + borrow::Cow, + hash::{Hash, Hasher}, + path::{Path, PathBuf}, + sync::{Arc, Mutex, MutexGuard}, +}; + +pub(crate) type WebResourceRequestHandler = + dyn Fn(http::Request>, &mut http::Response>) + Send + Sync; +pub(crate) type NavigationHandler = dyn Fn(&Url) -> bool + Send; +pub(crate) type NewWindowHandler = dyn Fn(Url, NewWindowFeatures) -> NewWindowResponse + Send; +pub(crate) type UriSchemeProtocolHandler = + Box>, UriSchemeResponder) + Send + Sync>; +pub(crate) type OnPageLoad = dyn Fn(Webview, PageLoadPayload<'_>) + Send + Sync + 'static; +pub(crate) type OnDocumentTitleChanged = dyn Fn(Webview, String) + Send + 'static; +pub(crate) type DownloadHandler = dyn Fn(Webview, DownloadEvent<'_>) -> bool + Send + Sync; +pub(crate) type PermissionRequestHandler = + dyn Fn(Webview, PermissionKind) -> PermissionResponse + Send + Sync + 'static; + +#[derive(Clone, Serialize)] +pub(crate) struct CreatedEvent { + pub(crate) label: String, +} + +/// Download event for the [`WebviewBuilder#method.on_download`] hook. +#[non_exhaustive] +pub enum DownloadEvent<'a> { + /// Download requested. + Requested { + /// The url being downloaded. + url: Url, + /// Represents where the file will be downloaded to. + /// Can be used to set the download location by assigning a new path to it. + /// The assigned path _must_ be absolute. + destination: &'a mut PathBuf, + }, + /// Download finished. + Finished { + /// The URL of the original download request. + url: Url, + /// Potentially representing the filesystem path the file was downloaded to. + /// + /// A value of `None` being passed instead of a `PathBuf` does not necessarily indicate that the download + /// did not succeed, and may instead indicate some other failure - always check the third parameter if you need to + /// know if the download succeeded. + /// + /// ## Platform-specific: + /// + /// - **macOS**: The second parameter indicating the path the file was saved to is always empty, due to API + /// limitations. + path: Option, + /// Indicates if the download succeeded or not. + success: bool, + }, +} + +/// The payload for the [`WebviewBuilder::on_page_load`] hook. +#[derive(Debug, Clone)] +pub struct PageLoadPayload<'a> { + pub(crate) url: &'a Url, + pub(crate) event: PageLoadEvent, +} + +impl<'a> PageLoadPayload<'a> { + /// The page URL. + pub fn url(&self) -> &'a Url { + self.url + } + + /// The page load event. + pub fn event(&self) -> PageLoadEvent { + self.event + } +} + +/// The IPC invoke request. +/// +/// # Stability +/// +/// This struct is **NOT** part of the public stable API and is only meant to be used +/// by internal code and external testing/fuzzing tools or custom invoke systems. +#[derive(Debug)] +pub struct InvokeRequest { + /// The invoke command. + pub cmd: String, + /// The success callback. + pub callback: CallbackFn, + /// The error callback. + pub error: CallbackFn, + /// URL of the frame that requested this command. + pub url: Url, + /// The body of the request. + pub body: InvokeBody, + /// The request headers. + pub headers: HeaderMap, + /// The invoke key. Must match what was passed to the app manager. + pub invoke_key: String, +} + +/// The platform webview handle. Accessed with [`Webview#method.with_webview`]; +#[cfg(feature = "wry")] +#[cfg_attr(docsrs, doc(cfg(feature = "wry")))] +pub struct PlatformWebview(tauri_runtime_wry::Webview); + +#[cfg(feature = "wry")] +impl PlatformWebview { + /// Returns [`webkit2gtk::WebView`] handle. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + #[cfg_attr( + docsrs, + doc(cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))) + )] + pub fn inner(&self) -> webkit2gtk::WebView { + self.0.clone() + } + + /// Returns the WebView2 controller. + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + pub fn controller( + &self, + ) -> webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller { + self.0.controller.clone() + } + + /// Returns the WebView2 environment. + #[cfg(windows)] + #[cfg_attr(docsrs, doc(cfg(windows)))] + pub fn environment( + &self, + ) -> webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Environment { + self.0.environment.clone() + } + + /// Returns the [WKWebView] handle. + /// + /// [WKWebView]: https://developer.apple.com/documentation/webkit/wkwebview + #[cfg(any(target_os = "macos", target_os = "ios"))] + #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))] + pub fn inner(&self) -> *mut std::ffi::c_void { + self.0.webview + } + + /// Returns WKWebView [controller] handle. + /// + /// [controller]: https://developer.apple.com/documentation/webkit/wkusercontentcontroller + #[cfg(any(target_os = "macos", target_os = "ios"))] + #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))] + pub fn controller(&self) -> *mut std::ffi::c_void { + self.0.manager + } + + /// Returns [NSWindow] associated with the WKWebView webview. + /// + /// [NSWindow]: https://developer.apple.com/documentation/appkit/nswindow + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub fn ns_window(&self) -> *mut std::ffi::c_void { + self.0.ns_window + } + + /// Returns [UIViewController] used by the WKWebView webview NSWindow. + /// + /// [UIViewController]: https://developer.apple.com/documentation/uikit/uiviewcontroller + #[cfg(target_os = "ios")] + #[cfg_attr(docsrs, doc(cfg(target_os = "ios")))] + pub fn view_controller(&self) -> *mut std::ffi::c_void { + self.0.view_controller + } + + /// Returns handle for JNI execution. + #[cfg(target_os = "android")] + pub fn jni_handle(&self) -> tauri_runtime_wry::wry::JniHandle { + self.0 + } +} + +/// Response for the new window request handler. +pub enum NewWindowResponse { + /// Allow the window to be opened with the default implementation. + Allow, + /// Allow the window to be opened, with the given window. + /// + /// ## Platform-specific: + /// + /// **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. + window: crate::WebviewWindow, + }, + /// Deny the window from being opened. + Deny, +} + +macro_rules! unstable_struct { + (#[doc = $doc:expr] $($tokens:tt)*) => { + #[cfg(any(test, feature = "unstable"))] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + #[doc = $doc] + pub $($tokens)* + + #[cfg(not(any(test, feature = "unstable")))] + pub(crate) $($tokens)* + } +} + +unstable_struct!( + #[doc = "A builder for a webview."] + struct WebviewBuilder { + pub(crate) label: String, + pub(crate) webview_attributes: WebviewAttributes, + pub(crate) web_resource_request_handler: Option>, + pub(crate) navigation_handler: Option>, + pub(crate) new_window_handler: Option>>, + pub(crate) on_page_load_handler: Option>>, + pub(crate) document_title_changed_handler: Option>>, + pub(crate) download_handler: Option>>, + pub(crate) permission_request_handler: Option>>, + } +); + +#[cfg_attr(not(feature = "unstable"), allow(dead_code))] +impl WebviewBuilder { + /// Initializes a webview builder with the given webview label and URL to load. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when creating webviews. + /// + /// # Examples + /// + /// - Create a webview in the setup hook: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::App("index.html".into())); + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap()); + Ok(()) + }); +``` + "#### + )] + /// + /// - Create a webview in a separate thread: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +tauri::Builder::default() + .setup(|app| { + let handle = app.handle().clone(); + std::thread::spawn(move || { + let window = tauri::window::WindowBuilder::new(&handle, "label").build().unwrap(); + let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::App("index.html".into())); + window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap()); + }); + Ok(()) + }); +``` + "#### + )] + /// + /// - Create a webview in a command: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +#[tauri::command] +async fn create_window(app: tauri::AppHandle) { + let window = tauri::window::WindowBuilder::new(&app, "label").build().unwrap(); + let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::External("https://tauri.app/".parse().unwrap())); + window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap()); +} +``` + "#### + )] + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn new>(label: L, url: WebviewUrl) -> Self { + Self { + label: label.into(), + webview_attributes: WebviewAttributes::new(url), + web_resource_request_handler: None, + navigation_handler: None, + new_window_handler: None, + on_page_load_handler: None, + document_title_changed_handler: None, + download_handler: None, + permission_request_handler: None, + } + } + + /// Initializes a webview builder from a [`WindowConfig`] from tauri.conf.json. + /// Keep in mind that you can't create 2 webviews with the same `label` so make sure + /// that the initial webview was closed or change the label of the new [`WebviewBuilder`]. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when creating webviews. + /// + /// # Examples + /// + /// - Create a webview in a command: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +#[tauri::command] +async fn create_window(app: tauri::AppHandle) { + let window = tauri::window::WindowBuilder::new(&app, "label").build().unwrap(); + let webview_builder = tauri::webview::WebviewBuilder::from_config(&app.config().app.windows.get(0).unwrap().clone()); + window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap()); +} +``` + "#### + )] + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn from_config(config: &WindowConfig) -> Self { + let mut config = config.to_owned(); + + if let Some(data_directory) = &config.data_directory { + let resolve_data_dir_res = dirs::data_local_dir() + .or({ + #[cfg(feature = "tracing")] + tracing::error!("failed to resolve data directory"); + None + }) + .and_then(|local_dir| { + SafePathBuf::new(data_directory.clone()) + .inspect_err(|_err| { + #[cfg(feature = "tracing")] + tracing::error!( + "data_directory `{}` is not a safe path, ignoring config. Validation error was: {_err}", + data_directory.display() + ); + }) + .map(|p| (local_dir, p)) + .ok() + }) + .and_then(|(local_dir, data_directory)| { + if data_directory.as_ref().is_relative() { + Some(local_dir.join(&config.label).join(data_directory.as_ref())) + } else { + #[cfg(feature = "tracing")] + tracing::error!( + "data_directory `{}` is not a relative path, ignoring config.", + data_directory.display() + ); + None + } + }); + if let Some(resolved_data_directory) = resolve_data_dir_res { + config.data_directory = Some(resolved_data_directory); + } + } + + Self { + label: config.label.clone(), + webview_attributes: WebviewAttributes::from(&config), + web_resource_request_handler: None, + navigation_handler: None, + new_window_handler: None, + on_page_load_handler: None, + document_title_changed_handler: None, + download_handler: None, + permission_request_handler: None, + } + } + + /// Defines a closure to be executed when the webview makes an HTTP request for a web resource, allowing you to modify the response. + /// + /// Currently only implemented for the `tauri` URI protocol. + /// + /// **NOTE:** Currently this is **not** executed when using external URLs such as a development server, + /// but it might be implemented in the future. **Always** check the request URL. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::{ + utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + window::WindowBuilder, + webview::WebviewBuilder, +}; +use http::header::HeaderValue; +use std::collections::HashMap; +tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + + let webview_builder = WebviewBuilder::new("core", WebviewUrl::App("index.html".into())) + .on_web_resource_request(|request, response| { + if request.uri().scheme_str() == Some("tauri") { + // if we have a CSP header, Tauri is loading an HTML file + // for this example, let's dynamically change the CSP + if let Some(csp) = response.headers_mut().get_mut("Content-Security-Policy") { + // use the tauri helper to parse the CSP policy to a map + let mut csp_map: HashMap = Csp::Policy(csp.to_str().unwrap().to_string()).into(); + csp_map.entry("script-src".to_string()).or_insert_with(Default::default).push("'unsafe-inline'"); + // use the tauri helper to get a CSP string from the map + let csp_string = Csp::from(csp_map).to_string(); + *csp = HeaderValue::from_str(&csp_string).unwrap(); + } + } + }); + + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + + Ok(()) + }); +``` + "#### + )] + pub fn on_web_resource_request< + F: Fn(http::Request>, &mut http::Response>) + Send + Sync + 'static, + >( + mut self, + f: F, + ) -> Self { + self.web_resource_request_handler.replace(Box::new(f)); + self + } + + /// Defines a closure to be executed when the webview navigates to a URL. Returning `false` cancels the navigation. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::{ + utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + window::WindowBuilder, + webview::WebviewBuilder, +}; +use http::header::HeaderValue; +use std::collections::HashMap; +tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + + let webview_builder = WebviewBuilder::new("core", WebviewUrl::App("index.html".into())) + .on_navigation(|url| { + // allow the production URL or localhost on dev + url.scheme() == "tauri" || (cfg!(dev) && url.host_str() == Some("localhost")) + }); + + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + Ok(()) + }); +``` + "#### + )] + pub fn on_navigation bool + Send + 'static>(mut self, f: F) -> Self { + self.navigation_handler.replace(Box::new(f)); + self + } + + /// Set a new window request handler to decide if incoming url is allowed to be opened. + /// + /// A new window is requested to be opened by the [window.open] API. + /// + /// The closure take the URL to open and the window features object and returns [`NewWindowResponse`] to determine whether the window should open. + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::{ + utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + window::WindowBuilder, + webview::WebviewBuilder, +}; +use http::header::HeaderValue; +use std::collections::HashMap; +tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + + let app_ = app.handle().clone(); + let webview_builder = WebviewBuilder::new("core", WebviewUrl::App("index.html".into())) + .on_new_window(move |url, features| { + let builder = tauri::WebviewWindowBuilder::new( + &app_, + // note: add an ID counter or random label generator to support multiple opened windows at the same time + "opened-window", + tauri::WebviewUrl::External("about:blank".parse().unwrap()), + ) + .window_features(features) + .on_document_title_changed(|window, title| { + window.set_title(&title).unwrap(); + }) + .title(url.as_str()); + + let window = builder.build().unwrap(); + tauri::webview::NewWindowResponse::Create { window } + }); + + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + Ok(()) + }); +``` + "#### + )] + /// + /// # Platform-specific + /// + /// - **Android / iOS**: Not supported. + /// + /// [window.open]: https://developer.mozilla.org/en-US/docs/Web/API/Window/open + pub fn on_new_window NewWindowResponse + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.new_window_handler.replace(Box::new(f)); + self + } + + /// Defines a closure to be executed when document title change. + pub fn on_document_title_changed, String) + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.document_title_changed_handler.replace(Box::new(f)); + self + } + + /// Set a download event handler to be notified when a download is requested or finished. + /// + /// Returning `false` prevents the download from happening on a [`DownloadEvent::Requested`] event. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::{ + utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + window::WindowBuilder, + webview::{DownloadEvent, WebviewBuilder}, +}; + +tauri::Builder::default() + .setup(|app| { + let window = WindowBuilder::new(app, "label").build()?; + let webview_builder = WebviewBuilder::new("core", WebviewUrl::App("index.html".into())) + .on_download(|webview, event| { + match event { + DownloadEvent::Requested { url, destination } => { + println!("downloading {}", url); + *destination = "/home/tauri/target/path".into(); + } + DownloadEvent::Finished { url, path, success } => { + println!("downloaded {} to {:?}, success: {}", url, path, success); + } + _ => (), + } + // let the download start + true + }); + + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + Ok(()) + }); +``` + "#### + )] + pub fn on_download, DownloadEvent<'_>) -> bool + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.download_handler.replace(Arc::new(f)); + self + } + + /// Defines a closure to be executed when a page load event is triggered. + /// The event can be either [`PageLoadEvent::Started`] if the page has started loading + /// or [`PageLoadEvent::Finished`] when the page finishes loading. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::{ + utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + window::WindowBuilder, + webview::{PageLoadEvent, WebviewBuilder}, +}; +use http::header::HeaderValue; +use std::collections::HashMap; +tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + let webview_builder = WebviewBuilder::new("core", WebviewUrl::App("index.html".into())) + .on_page_load(|webview, payload| { + match payload.event() { + PageLoadEvent::Started => { + println!("{} finished loading", payload.url()); + } + PageLoadEvent::Finished => { + println!("{} finished loading", payload.url()); + } + } + }); + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + Ok(()) + }); +``` + "#### + )] + pub fn on_page_load, PageLoadPayload<'_>) + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.on_page_load_handler.replace(Box::new(f)); + self + } + + /// Defines a closure to be executed when a permission is requested. + /// + /// The handler receives the [`PermissionKind`] and should return + /// the desired [`PermissionResponse`]. + /// + /// This is called before [`crate::Builder::on_permission_request`], + /// and skips it if the returned response is not [`crate::webview::PermissionResponse::Default`]. + /// + /// > [!NOTE] + /// > This handler only triggers for new permission requests. If the user has already + /// > allowed or denied a permission persistently within the webview, the browser + /// > will use the saved preference instead of calling this handler. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Fully supported via WebView2's PermissionRequested event. + /// - **macOS / iOS**: Fully supported via WKUIDelegate's requestMediaCapturePermission. + /// - **Linux**: Fully supported via WebKitGTK's permission-request signal. + /// - **Android**: Supported via JNI bridge for geolocation, microphone, camera, + /// protected media, and MIDI requests. Android runtime permissions may still + /// trigger native OS prompts before access is granted. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::webview::{WebviewBuilder, PermissionKind, PermissionResponse}; +tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + let webview_builder = WebviewBuilder::new("core", tauri::WebviewUrl::App("index.html".into())) + .on_permission_request(|webview, kind| { + match kind { + PermissionKind::Geolocation => PermissionResponse::Allow, + PermissionKind::Notifications => PermissionResponse::Allow, + _ => PermissionResponse::Default, + } + }); + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + Ok(()) + }); +``` + "#### + )] + pub fn on_permission_request< + F: Fn(Webview, PermissionKind) -> PermissionResponse + Send + Sync + 'static, + >( + mut self, + f: F, + ) -> Self { + self.permission_request_handler.replace(Box::new(f)); + self + } + + pub(crate) fn into_pending_webview>( + mut self, + manager: &M, + window_label: &str, + ) -> crate::Result> { + let mut pending = PendingWebview::new(self.webview_attributes, self.label.clone())?; + pending.navigation_handler = self.navigation_handler.take(); + pending.new_window_handler = self.new_window_handler.take().map(|handler| { + Box::new( + move |url, features: NewWindowFeatures| match handler(url, features) { + NewWindowResponse::Allow => tauri_runtime::webview::NewWindowResponse::Allow, + #[cfg(mobile)] + NewWindowResponse::Create { window: _ } => { + tauri_runtime::webview::NewWindowResponse::Allow + } + #[cfg(desktop)] + NewWindowResponse::Create { window } => { + tauri_runtime::webview::NewWindowResponse::Create { + window_id: window.window.window.id, + } + } + NewWindowResponse::Deny => tauri_runtime::webview::NewWindowResponse::Deny, + }, + ) + as Box< + dyn Fn(Url, NewWindowFeatures) -> tauri_runtime::webview::NewWindowResponse + + Send + + 'static, + > + }); + + if let Some(document_title_changed_handler) = self.document_title_changed_handler.take() { + let label = pending.label.clone(); + let manager = manager.manager_owned(); + pending + .document_title_changed_handler + .replace(Box::new(move |title| { + if let Some(w) = manager.get_webview(&label) { + document_title_changed_handler(w, title); + } + })); + } + pending.web_resource_request_handler = self.web_resource_request_handler.take(); + + if let Some(download_handler) = self.download_handler.take() { + let label = pending.label.clone(); + let manager = manager.manager_owned(); + pending.download_handler.replace(Arc::new(move |event| { + if let Some(w) = manager.get_webview(&label) { + download_handler( + w, + match event { + tauri_runtime::webview::DownloadEvent::Requested { url, destination } => { + DownloadEvent::Requested { url, destination } + } + tauri_runtime::webview::DownloadEvent::Finished { url, path, success } => { + DownloadEvent::Finished { url, path, success } + } + }, + ) + } else { + false + } + })); + } + + let label_ = pending.label.clone(); + let manager_ = manager.manager_owned(); + pending + .on_page_load_handler + .replace(Box::new(move |url, event| { + if let Some(w) = manager_.get_webview(&label_) { + if let Some(handler) = self.on_page_load_handler.as_ref() { + handler(w, PageLoadPayload { url: &url, event }); + } + } + })); + + let label_ = pending.label.clone(); + let manager_ = manager.manager_owned(); + if let Some(handler) = self.permission_request_handler { + pending.permission_request_handler = Some(Box::new(move |kind| { + if let Some(w) = manager_.get_webview(&label_) { + handler(w, kind) + } else { + PermissionResponse::Default + } + })); + } + + manager + .manager() + .webview + .prepare_webview(manager, pending, window_label) + } + + /// Creates a new webview on the given window. + #[cfg(desktop)] + pub(crate) fn build( + self, + window: Window, + position: Position, + size: Size, + ) -> crate::Result> { + let app_manager = window.manager(); + + let mut pending = self.into_pending_webview(&window, window.label())?; + + pending.webview_attributes.bounds = Some(tauri_runtime::dpi::Rect { size, position }); + + let use_https_scheme = pending.webview_attributes.use_https_scheme; + + let webview = match &mut window.runtime() { + RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_webview(pending), + _ => unimplemented!(), + } + .map(|webview| { + app_manager + .webview + .attach_webview(window.clone(), webview, use_https_scheme) + })?; + + Ok(webview) + } +} + +/// Webview attributes. +impl WebviewBuilder { + /// Sets whether clicking an inactive window also clicks through to the webview. + #[must_use] + pub fn accept_first_mouse(mut self, accept: bool) -> Self { + self.webview_attributes.accept_first_mouse = accept; + self + } + + /// Adds the provided JavaScript to a list of scripts that should be run after the global object has been created, + /// but before the HTML document has been parsed and before any other script included by the HTML document is run. + /// + /// Since it runs on all top-level document navigations, + /// it's recommended to check the `window.location` to guard your script from running on unexpected origins. + /// + /// This is executed only on the main frame. + /// If you only want to run it in all frames, use [`Self::initialization_script_for_all_frames`] instead. + /// + /// ## Platform-specific + /// + /// - **Windows:** scripts are always added to subframes. + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust +use tauri::{WindowBuilder, Runtime}; + +const INIT_SCRIPT: &str = r#" + if (window.location.origin === 'https://tauri.app') { + console.log("hello world from js init script"); + + window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' }; + } +"#; + +fn main() { + tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::App("index.html".into())) + .initialization_script(INIT_SCRIPT); + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + Ok(()) + }); +} +``` + "#### + )] + /// + /// [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_attributes + .initialization_scripts + .push(InitializationScript { + script: script.into(), + for_main_frame_only: true, + }); + self + } + + /// Adds the provided JavaScript to a list of scripts that should be run after the global object has been created, + /// but before the HTML document has been parsed and before any other script included by the HTML document is run. + /// + /// Since it runs on all top-level document navigations and also child frame page navigations, + /// it's recommended to check the `window.location` to guard your script from running on unexpected origins. + /// + /// This is executed on all frames (main frame and also sub frames). + /// If you only want to run the script in the main frame, use [`Self::initialization_script`] instead. + /// + /// ## Platform-specific + /// + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust +use tauri::{WindowBuilder, Runtime}; + +const INIT_SCRIPT: &str = r#" + if (window.location.origin === 'https://tauri.app') { + console.log("hello world from js init script"); + + window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' }; + } +"#; + +fn main() { + tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label").build()?; + let webview_builder = tauri::webview::WebviewBuilder::new("label", tauri::WebviewUrl::App("index.html".into())) + .initialization_script_for_all_frames(INIT_SCRIPT); + let webview = window.add_child(webview_builder, tauri::LogicalPosition::new(0, 0), window.inner_size().unwrap())?; + Ok(()) + }); +} +``` + "#### + )] + /// + /// [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_attributes + .initialization_scripts + .push(InitializationScript { + script: script.into(), + for_main_frame_only: false, + }); + self + } + + /// Set the user agent for the webview + #[must_use] + pub fn user_agent(mut self, user_agent: &str) -> Self { + self.webview_attributes.user_agent = Some(user_agent.to_string()); + self + } + + /// Set additional arguments for the webview. + /// + /// ## Platform-specific + /// + /// - **macOS / Linux / Android / iOS**: Unsupported. + /// + /// ## Warning + /// + /// Webview instances with different browser arguments must also have different [data directories](Self::data_directory). + /// + /// By default wry passes `--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection` + /// so if you use this method, you also need to disable these components by yourself if you want. + #[must_use] + pub fn additional_browser_args(mut self, additional_args: &str) -> Self { + self.webview_attributes.additional_browser_args = Some(additional_args.to_string()); + self + } + + /// Data directory for the webview. + #[must_use] + pub fn data_directory(mut self, data_directory: PathBuf) -> Self { + self + .webview_attributes + .data_directory + .replace(data_directory); + self + } + + /// Disables the drag and drop handler used internally to generate [`DragDropEvent`](crate::DragDropEvent)s. + /// + /// This is required to use HTML5 drag and drop APIs on the frontend on Windows since we replace the drag drop handler of WebView2. + #[must_use] + pub fn disable_drag_drop_handler(mut self) -> Self { + self.webview_attributes.drag_drop_handler_enabled = false; + self + } + + /// Enables clipboard access for the page rendered on **Linux** and **Windows**. + /// + /// **macOS** doesn't provide such method and is always enabled by default, + /// but you still need to add menu item accelerators to use shortcuts. + #[must_use] + pub fn enable_clipboard_access(mut self) -> Self { + self.webview_attributes.clipboard = true; + self + } + + /// Enable or disable incognito mode for the WebView. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Requires WebView2 Runtime version 101.0.1210.39 or higher, does nothing on older versions, + /// see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039 + /// - **Android**: Unsupported. + /// - **macOS / iOS**: Uses the nonPersistent DataStore + #[must_use] + pub fn incognito(mut self, incognito: bool) -> Self { + self.webview_attributes.incognito = incognito; + self + } + + /// Set a proxy URL for the WebView for all network requests. + /// + /// Must be either a `http://` or a `socks5://` URL. + /// + /// ## Platform-specific + /// + /// - **macOS**: Requires the `macos-proxy` feature flag and only compiles for macOS 14+. + #[must_use] + pub fn proxy_url(mut self, url: Url) -> Self { + self.webview_attributes.proxy_url = Some(url); + self + } + + /// Enable or disable transparency for the WebView. + #[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.webview_attributes.transparent = transparent; + self + } + + /// Whether the webview should be focused or not. + #[must_use] + pub fn focused(mut self, focus: bool) -> Self { + self.webview_attributes.focus = focus; + self + } + + /// Sets the webview to automatically grow and shrink its size and position when the parent window resizes. + #[must_use] + pub fn auto_resize(mut self) -> Self { + self.webview_attributes.auto_resize = true; + self + } + + /// Whether page zooming by hotkeys and mousewheel should be enabled or not. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Controls WebView2's [`IsZoomControlEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2settings?view=webview2-winrt-1.0.2420.47#iszoomcontrolenabled) setting. + /// - **MacOS / Linux**: Injects a polyfill that zooms in and out with `Ctrl/Cmd + [- = +]` hotkeys or mousewheel events, + /// 20% in each step, ranging from 20% to 1000%. Requires `core:webview:allow-set-webview-zoom` permission + /// + /// - **Android / iOS**: Unsupported. + #[must_use] + pub fn zoom_hotkeys_enabled(mut self, enabled: bool) -> Self { + self.webview_attributes.zoom_hotkeys_enabled = enabled; + self + } + + /// Whether browser extensions can be installed for the webview process + /// + /// ## Platform-specific: + /// + /// - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled) + /// - **MacOS / Linux / iOS / Android** - Unsupported. + #[must_use] + pub fn browser_extensions_enabled(mut self, enabled: bool) -> Self { + self.webview_attributes.browser_extensions_enabled = enabled; + self + } + + /// Set the path from which to load extensions from. Extensions stored in this path should be unpacked Chrome extensions on Windows, and compiled `.so` extensions on Linux. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Browser extensions must first be enabled. See [`browser_extensions_enabled`](Self::browser_extensions_enabled) + /// - **MacOS / iOS / Android** - Unsupported. + #[must_use] + pub fn extensions_path(mut self, path: impl AsRef) -> Self { + self.webview_attributes.extensions_path = Some(path.as_ref().to_path_buf()); + self + } + + /// Initialize the WebView with a custom data store identifier. + /// Can be used as a replacement for data_directory not being available in WKWebView. + /// + /// - **macOS / iOS**: Available on macOS >= 14 and iOS >= 17 + /// - **Windows / Linux / Android**: Unsupported. + /// + /// Note: Enable incognito mode to use the `nonPersistent` DataStore. + #[must_use] + pub fn data_store_identifier(mut self, data_store_identifier: [u8; 16]) -> Self { + self.webview_attributes.data_store_identifier = Some(data_store_identifier); + self + } + + /// Sets whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android. Defaults to `false`. + /// + /// ## Note + /// + /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `://localhost` protocols used on macOS and Linux. + /// + /// ## Warning + /// + /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data. + #[must_use] + pub fn use_https_scheme(mut self, enabled: bool) -> Self { + self.webview_attributes.use_https_scheme = enabled; + self + } + + /// Whether web inspector, which is usually called browser devtools, is enabled or not. Enabled by default. + /// + /// This API works in **debug** builds, but requires `devtools` feature flag to enable it in **release** builds. + /// + /// ## Platform-specific + /// + /// - macOS: This will call private functions on **macOS** + /// - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android. + /// - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window. + #[must_use] + pub fn devtools(mut self, enabled: bool) -> Self { + self.webview_attributes.devtools.replace(enabled); + self + } + + /// Set the webview background color. + /// + /// ## Platform-specific: + /// + /// - **macOS / iOS**: Not implemented. + /// - **Windows**: On Windows 7, alpha channel is ignored. + /// - **Windows**: On Windows 8 and newer, if alpha channel is not `0`, it will be ignored. + #[must_use] + pub fn background_color(mut self, color: Color) -> Self { + self.webview_attributes.background_color = Some(color); + self + } + + /// Change the default background throttling behaviour. + /// + /// By default, browsers use a suspend policy that will throttle timers and even unload + /// the whole tab (view) to free resources after roughly 5 minutes when a view became + /// minimized or hidden. This will pause all tasks until the documents visibility state + /// changes back from hidden to visible by bringing the view back to the foreground. + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice. + /// - **iOS**: Supported since version 17.0+. + /// - **macOS**: Supported since version 14.0+. + /// + /// see + #[must_use] + pub fn background_throttling(mut self, policy: BackgroundThrottlingPolicy) -> Self { + self.webview_attributes.background_throttling = Some(policy); + self + } + + /// Whether JavaScript should be disabled. + #[must_use] + pub fn disable_javascript(mut self) -> Self { + self.webview_attributes.javascript_disabled = true; + self + } + + /// Specifies the native scrollbar style to use with the webview. + /// CSS styles that modify the scrollbar are applied on top of the native appearance configured here. + /// + /// Defaults to [`ScrollBarStyle::Default`], which is the browser default. + /// + /// ## Platform-specific + /// + /// - **Windows**: + /// - [`ScrollBarStyle::FluentOverlay`] requires WebView2 Runtime version 125.0.2535.41 or higher, + /// and does nothing on older versions. + /// - This option must be given the same value for all webviews that target the same data directory. Use + /// [`WebviewBuilder::data_directory`] to change data directories if needed. + /// - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation. + #[must_use] + pub fn scroll_bar_style(mut self, style: ScrollBarStyle) -> Self { + self.webview_attributes = self.webview_attributes.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.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. + /// + /// See https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / Android:** Unsupported. + #[cfg(target_os = "macos")] + #[must_use] + pub fn allow_link_preview(mut self, allow_link_preview: bool) -> Self { + self.webview_attributes = self + .webview_attributes + .allow_link_preview(allow_link_preview); + self + } + /// Whether to limit navigations to App-Bound Domains. This is necessary to + /// enable Service Workers on iOS according to + /// [StackOverflow](https://stackoverflow.com/questions/49673399/service-workers-unavailable-in-wkwebview-in-ios-11-3/64155509#64155509). + /// + /// Default is false. + /// + /// Note: If you pass in `true` make sure to add localhost and any [`registrable + /// domains`](https://developer.mozilla.org/en-US/docs/Glossary/Registrable_domain) + /// used in this webview to tauri-src/Info.ios.plist: + /// + /// ```xml + /// + /// + /// WKAppBoundDomains + /// + /// localhost + /// aregistrabledomain.example + /// + /// + /// + /// ``` + /// + /// You must add `localhost` if any webview with this set to true opens a + /// local webpage, makes any localhost calls, or uses the isolation pattern + /// because Tauri uses the `localhost` domain for hosting the application + /// webpage, the IPC protocol, and the isolation pattern's iframe. + /// + /// Requests served through custom uri schemes are allowed so long as they use + /// a registrable domain specified in the `WKAppBoundDomains` array for all the + /// requests from the app, including requests for the `localhost` domain. + /// + /// In theory, you can whitelist an entire uri scheme by including the + /// protocol name followed by a colon. For example, to allow all requests + /// using a custom "stream" uri scheme (see [this tauri + /// example](https://github.com/tauri-apps/tauri/blob/dev/examples/streaming/main.rs)), + /// you could add `stream:` to the AppBoundDomains array. That said, I'm not + /// sure whether Apple would let your app through app review if you do + /// whitelist an entire protocol because this feature is not mentioned in + /// [their blog post on App-Bound + /// Domains](https://webkit.org/blog/10882/app-bound-domains/). + /// + /// See https://webkit.org/blog/10882/app-bound-domains/ and + /// https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/limitsnavigationstoappbounddomains + /// for the official documentation on App-Bound Domains. + /// + /// ## Platform-specific + /// + /// - **iOS**: Supported since version 14.0+. + /// - **Linux / Windows / Android / MacOS:** Unsupported. + #[must_use] + pub fn limit_navigations_to_app_bound_domains(mut self, limit_navigations: bool) -> Self { + self.webview_attributes = self + .webview_attributes + .limit_navigations_to_app_bound_domains(limit_navigations); + self + } + + /// Allows overriding the keyboard accessory view on iOS. + /// Returning `None` effectively removes the view. + /// + /// The closure parameter is the webview instance. + /// + /// The accessory view is the view that appears above the keyboard when a text input element is focused. + /// It usually displays a view with "Done", "Next" buttons. + /// + /// # Stability + /// + /// This relies on [`objc2_ui_kit`] which does not provide a stable API yet, so it can receive breaking changes in minor releases. + #[cfg(target_os = "ios")] + pub fn with_input_accessory_view_builder< + F: Fn(&objc2_ui_kit::UIView) -> Option> + + Send + + Sync + + 'static, + >( + mut self, + builder: F, + ) -> Self { + self + .webview_attributes + .input_accessory_view_builder + .replace(tauri_runtime::webview::InputAccessoryViewBuilder::new( + Box::new(builder), + )); + self + } + + /// Set the environment for the webview. + /// Useful if you need to share the same environment, for instance when using the [`Self::on_new_window`]. + #[cfg(all(feature = "wry", windows))] + pub fn with_environment( + mut self, + environment: webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Environment, + ) -> Self { + self.webview_attributes.environment.replace(environment); + self + } + + /// Creates a new webview sharing the same web process with the provided webview. + /// Useful if you need to link a webview to another, for instance when using the [`Self::on_new_window`]. + #[cfg(all( + feature = "wry", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ) + ))] + pub fn with_related_view(mut self, related_view: webkit2gtk::WebView) -> Self { + self.webview_attributes.related_view.replace(related_view); + self + } + + /// Set the webview configuration. + /// Useful if you need to share the use a predefined webview configuration, for instance when using the [`Self::on_new_window`]. + #[cfg(target_os = "macos")] + pub fn with_webview_configuration( + mut self, + webview_configuration: objc2::rc::Retained, + ) -> Self { + self + .webview_attributes + .webview_configuration + .replace(webview_configuration); + self + } +} + +/// Webview. +#[default_runtime(crate::Wry, wry)] +pub struct Webview { + pub(crate) window: Arc>>, + /// The webview created by the runtime. + pub(crate) webview: DetachedWebview, + /// The manager to associate this webview with. + pub(crate) manager: Arc>, + pub(crate) app_handle: AppHandle, + pub(crate) resources_table: Arc>, + use_https_scheme: bool, +} + +impl std::fmt::Debug for Webview { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Window") + .field("window", &self.window.lock().unwrap()) + .field("webview", &self.webview) + .field("use_https_scheme", &self.use_https_scheme) + .finish() + } +} + +impl Clone for Webview { + fn clone(&self) -> Self { + Self { + window: self.window.clone(), + webview: self.webview.clone(), + manager: self.manager.clone(), + app_handle: self.app_handle.clone(), + resources_table: self.resources_table.clone(), + use_https_scheme: self.use_https_scheme, + } + } +} + +impl Hash for Webview { + /// Only use the [`Webview`]'s label to represent its hash. + fn hash(&self, state: &mut H) { + self.webview.label.hash(state) + } +} + +impl Eq for Webview {} +impl PartialEq for Webview { + /// Only use the [`Webview`]'s label to compare equality. + fn eq(&self, other: &Self) -> bool { + self.webview.label.eq(&other.webview.label) + } +} + +/// Base webview functions. +impl Webview { + /// Create a new webview that is attached to the window. + pub(crate) fn new( + window: Window, + webview: DetachedWebview, + use_https_scheme: bool, + ) -> Self { + Self { + manager: window.manager.clone(), + app_handle: window.app_handle.clone(), + window: Arc::new(Mutex::new(window)), + webview, + resources_table: Default::default(), + use_https_scheme, + } + } + + /// Initializes a webview builder with the given window label and URL to load on the webview. + /// + /// Data URLs are only supported with the `webview-data-url` feature flag. + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + pub fn builder>(label: L, url: WebviewUrl) -> WebviewBuilder { + WebviewBuilder::new(label.into(), url) + } + + /// Runs the given closure on the main thread. + pub fn run_on_main_thread(&self, f: F) -> crate::Result<()> { + self + .webview + .dispatcher + .run_on_main_thread(f) + .map_err(Into::into) + } + + /// The webview label. + pub fn label(&self) -> &str { + &self.webview.label + } + + /// Whether the webview was configured to use the HTTPS scheme or not. + pub(crate) fn use_https_scheme(&self) -> bool { + self.use_https_scheme + } + + /// Registers a webview event listener. + pub fn on_webview_event(&self, f: F) { + self + .webview + .dispatcher + .on_webview_event(move |event| f(&event.clone().into())); + } + + /// Resolves the given command scope for this webview on the currently loaded URL. + /// + /// If the command is not allowed, returns None. + /// + /// If the scope cannot be deserialized to the given type, an error is returned. + /// + /// In a command context this can be directly resolved from the command arguments via [CommandScope]: + /// + /// ``` + /// use tauri::ipc::CommandScope; + /// + /// #[derive(Debug, serde::Deserialize)] + /// struct ScopeType { + /// some_value: String, + /// } + /// #[tauri::command] + /// fn my_command(scope: CommandScope) { + /// // check scope + /// } + /// ``` + /// + /// # Examples + /// + /// ``` + /// use tauri::Manager; + /// + /// #[derive(Debug, serde::Deserialize)] + /// struct ScopeType { + /// some_value: String, + /// } + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview = app.get_webview_window("main").unwrap(); + /// let scope = webview.resolve_command_scope::("my-plugin", "read"); + /// Ok(()) + /// }); + /// ``` + pub fn resolve_command_scope( + &self, + plugin: &str, + command: &str, + ) -> crate::Result>> { + let current_url = self.url()?; + let is_local = self.is_local_url(¤t_url); + let origin = if is_local { + Origin::Local + } else { + Origin::Remote { url: current_url } + }; + + let cmd_name = format!("plugin:{plugin}|{command}"); + let resolved_access = self + .manager() + .runtime_authority + .lock() + .unwrap() + .resolve_access(&cmd_name, self.window().label(), self.label(), &origin); + + if let Some(access) = resolved_access { + let scope_ids = access + .iter() + .filter_map(|cmd| cmd.scope_id) + .collect::>(); + + let command_scope = CommandScope::resolve(self, scope_ids)?; + let global_scope = GlobalScope::resolve(self, plugin)?; + + Ok(Some(ResolvedScope { + global_scope, + command_scope, + })) + } else { + Ok(None) + } + } +} + +/// Desktop webview setters and actions. +#[cfg(desktop)] +impl Webview { + /// Opens the dialog to prints the contents of the webview. + /// Currently only supported on macOS on `wry`. + /// `window.print()` works on all platforms. + pub fn print(&self) -> crate::Result<()> { + self.webview.dispatcher.print().map_err(Into::into) + } + + /// Get the cursor position relative to the top-left hand corner of the desktop. + /// + /// Note that the top-left hand corner of the desktop is not necessarily the same as the screen. + /// If the user uses a desktop with multiple monitors, + /// the top-left hand corner of the desktop is the top-left hand corner of the main monitor on Windows and macOS + /// or the top-left of the leftmost monitor on X11. + /// + /// The coordinates can be negative if the top-left hand corner of the window is outside of the visible screen region. + pub fn cursor_position(&self) -> crate::Result> { + self.app_handle.cursor_position() + } + + /// Closes this webview. + pub fn close(&self) -> crate::Result<()> { + self.webview.dispatcher.close()?; + self.manager().on_webview_close(self.label()); + Ok(()) + } + + /// Resizes this webview. + pub fn set_bounds(&self, bounds: tauri_runtime::dpi::Rect) -> crate::Result<()> { + self + .webview + .dispatcher + .set_bounds(bounds) + .map_err(Into::into) + } + + /// Resizes this webview. + pub fn set_size>(&self, size: S) -> crate::Result<()> { + self + .webview + .dispatcher + .set_size(size.into()) + .map_err(Into::into) + } + + /// Sets this webviews's position. + pub fn set_position>(&self, position: Pos) -> crate::Result<()> { + self + .webview + .dispatcher + .set_position(position.into()) + .map_err(Into::into) + } + + /// Focus the webview. + pub fn set_focus(&self) -> crate::Result<()> { + self.webview.dispatcher.set_focus().map_err(Into::into) + } + + /// Hide the webview. + pub fn hide(&self) -> crate::Result<()> { + self.webview.dispatcher.hide().map_err(Into::into) + } + + /// Show the webview. + pub fn show(&self) -> crate::Result<()> { + self.webview.dispatcher.show().map_err(Into::into) + } + + /// Move the webview to the given window. + /// + /// The webview keeps its size and its position relative to the window client area, + /// so you usually want to call [`Self::set_position`] and [`Self::set_size`] afterwards + /// (or [`Self::set_auto_resize`] to let it follow the new parent window size). + /// + /// # Errors + /// + /// Returns [`Error::CannotReparentWebviewWindow`](crate::Error::CannotReparentWebviewWindow) + /// when the `unstable` Cargo feature is disabled and either this webview or the target window + /// belongs to a [`WebviewWindow`] - without that feature a window and its webview are a single + /// object that cannot be split apart. With the `unstable` feature enabled any webview can be + /// moved to any window. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::{LogicalPosition, LogicalSize, WebviewUrl}; + +tauri::Builder::default() + .setup(|app| { + let first = tauri::Window::builder(app, "first").build()?; + let second = tauri::Window::builder(app, "second").build()?; + + let webview = first.add_child( + tauri::webview::WebviewBuilder::new("child", WebviewUrl::App(Default::default())), + LogicalPosition::new(0., 0.), + LogicalSize::new(800., 600.), + )?; + + // move the webview to the second window + webview.reparent(&second)?; + + Ok(()) + }); +``` + "#### + )] + pub fn reparent(&self, window: &Window) -> crate::Result<()> { + #[cfg(not(feature = "unstable"))] + { + if self.window_ref().is_webview_window() || window.is_webview_window() { + return Err(crate::Error::CannotReparentWebviewWindow); + } + } + + *self.window.lock().unwrap() = window.clone(); + self.webview.dispatcher.reparent(window.window.id)?; + Ok(()) + } + + /// Sets whether the webview should automatically grow and shrink its size and position when the parent window resizes. + /// + /// Child webviews created with `Window::add_child` (`unstable` feature) have a fixed position and size + /// by default: resizing the window leaves them where they are. When auto resize is enabled, the webview bounds + /// are scaled with the window client area, keeping the same proportions it had when auto resize was enabled. + /// + /// The same can be set when creating the webview with `WebviewBuilder::auto_resize`. + /// + /// This is a no-op for [`WebviewWindow`] webviews, which always fill their window. + pub fn set_auto_resize(&self, auto_resize: bool) -> crate::Result<()> { + self + .webview + .dispatcher + .set_auto_resize(auto_resize) + .map_err(Into::into) + } + + /// Returns the bounds of the webviews's client area. + pub fn bounds(&self) -> crate::Result { + self.webview.dispatcher.bounds().map_err(Into::into) + } + + /// Returns the webview position. + /// + /// - For child webviews, returns the position of the top-left hand corner of the webviews's client area relative to the top-left hand corner of the parent window. + /// - For webview window, returns the inner position of the window. + pub fn position(&self) -> crate::Result> { + self.webview.dispatcher.position().map_err(Into::into) + } + + /// Returns the physical size of the webviews's client area. + pub fn size(&self) -> crate::Result> { + self.webview.dispatcher.size().map_err(Into::into) + } +} + +/// Webview APIs. +impl Webview { + /// The window that is hosting this webview. + pub fn window(&self) -> Window { + self.window.lock().unwrap().clone() + } + + /// A reference to the window that is hosting this webview. + pub fn window_ref(&self) -> MutexGuard<'_, Window> { + self.window.lock().unwrap() + } + + pub(crate) fn window_label(&self) -> String { + self.window_ref().label().to_string() + } + + /// Executes a closure, providing it with the webview handle that is specific to the current platform. + /// + /// The closure is executed on the main thread. + /// + /// Note that `webview2-com`, `webkit2gtk`, `objc2_web_kit` and similar crates may be updated in minor releases of Tauri. + /// Therefore it's recommended to pin Tauri to at least a minor version when you're using `with_webview`. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::Manager; + +tauri::Builder::default() + .setup(|app| { + let main_webview = app.get_webview("main").unwrap(); + main_webview.with_webview(|webview| { + #[cfg(target_os = "linux")] + { + // see + // and + use webkit2gtk::WebViewExt; + webview.inner().set_zoom_level(4.); + } + + #[cfg(windows)] + unsafe { + // see https://docs.rs/webview2-com/0.19.1/webview2_com/Microsoft/Web/WebView2/Win32/struct.ICoreWebView2Controller.html + webview.controller().SetZoomFactor(4.).unwrap(); + } + + #[cfg(target_os = "macos")] + unsafe { + let view: &objc2_web_kit::WKWebView = &*webview.inner().cast(); + let controller: &objc2_web_kit::WKUserContentController = &*webview.controller().cast(); + let window: &objc2_app_kit::NSWindow = &*webview.ns_window().cast(); + + view.setPageZoom(4.); + controller.removeAllUserScripts(); + let bg_color = objc2_app_kit::NSColor::colorWithDeviceRed_green_blue_alpha(0.5, 0.2, 0.4, 1.); + window.setBackgroundColor(Some(&bg_color)); + } + + #[cfg(target_os = "android")] + { + use jni::objects::JValue; + webview.jni_handle().exec(|env, _, webview| { + env.call_method(webview, "zoomBy", "(F)V", &[JValue::Float(4.)]).unwrap(); + }) + } + }); + Ok(()) +}); +``` + "#### + )] + #[cfg(feature = "wry")] + #[cfg_attr(docsrs, doc(cfg(feature = "wry")))] + pub fn with_webview( + &self, + f: F, + ) -> crate::Result<()> { + self + .webview + .dispatcher + .with_webview(|w| f(PlatformWebview(*w.downcast().unwrap()))) + .map_err(Into::into) + } + + /// Returns the current url of the webview. + pub fn url(&self) -> crate::Result { + self + .webview + .dispatcher + .url() + .map(|url| url.parse().map_err(crate::Error::InvalidUrl))? + } + + /// Navigates the webview to the defined url. + /// + /// This is the Rust equivalent of setting `window.location` on the frontend and behaves like any other + /// top-level navigation: + /// + /// - the navigation handler defined with `WebviewBuilder::on_navigation` and the + /// [`Plugin::on_navigation`](crate::plugin::Plugin::on_navigation) hooks are called and can cancel it; + /// - the initialization scripts registered when the webview was created + /// (including Tauri's own IPC bridge) run again on the new document, so `window.__TAURI__` and the scripts + /// added with `WebviewBuilder::initialization_script` keep working after the navigation; + /// - [`Builder::on_page_load`](crate::Builder::on_page_load) is triggered for the new document; + /// - the ACL origin of the IPC changes with the URL: after navigating to a remote URL the webview can only + /// execute commands allowed by a capability whose `remote` context matches it. + /// + /// The navigation is asynchronous: this function returns as soon as the request is sent to the webview, + /// the page is not loaded yet when it returns. + pub fn navigate(&self, url: Url) -> crate::Result<()> { + self.webview.dispatcher.navigate(url).map_err(Into::into) + } + + /// Reloads the current page. + /// + /// Reloading re-runs the initialization scripts and triggers the navigation and page load hooks + /// for the same URL, exactly like [`Self::navigate`] to [`Self::url`] would. + /// In-memory state of the page (JavaScript variables, unsaved form data, channels created by the frontend) + /// is lost, but cookies and other browsing data are kept - use [`Self::clear_all_browsing_data`] to clear those. + pub fn reload(&self) -> crate::Result<()> { + self.webview.dispatcher.reload().map_err(Into::into) + } + + /// Converts a file path to a URL that can be loaded by this webview. + /// + /// This is the Rust equivalent of the JavaScript `convertFileSrc` function. + /// + /// The `protocol-asset` Cargo feature must be enabled and the file must be included in the + /// [`app.security.assetProtocol`](https://v2.tauri.app/reference/config/#assetprotocolconfig) + /// scope. The protocol origin must also be allowed by the relevant + /// [`app.security.csp`](https://v2.tauri.app/reference/config/#csp-1) directive, + /// e.g. `img-src 'self' asset: http://asset.localhost`. + /// + /// On Windows and Android the URL is `http://{protocol}.localhost/{path}` + /// (or `https://` if the webview was built with [`WebviewBuilder::use_https_scheme`]); + /// on macOS, Linux and iOS it is `{protocol}://localhost/{path}`. + /// + /// # Arguments + /// + /// * `path` - The file path to convert. + /// * `protocol` - The custom protocol to use. Defaults to `asset`; you only need to set this + /// when using a protocol registered with [`Builder::register_uri_scheme_protocol`](crate::Builder::register_uri_scheme_protocol). + /// + /// # Errors + /// + /// Returns [`Error::NonUtf8Path`](crate::Error::NonUtf8Path) if the path is not valid UTF-8, + /// since the asset protocol could not resolve such a URL back to the file. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview = app.get_webview_window("main").unwrap(); + /// let video_path = app.path().app_data_dir()?.join("video.mp4"); + /// let url = webview.convert_file_src(&video_path, None)?; + /// webview.eval(format!("document.querySelector('video').src = '{url}'"))?; + /// Ok(()) + /// }); + /// ``` + pub fn convert_file_src>( + &self, + path: P, + protocol: Option<&str>, + ) -> crate::Result { + let path = dunce::simplified(path.as_ref()); + let path = path + .to_str() + .ok_or_else(|| crate::Error::NonUtf8Path(path.to_path_buf()))?; + Ok(format!( + "{}/{}", + crate::protocol::origin(protocol.unwrap_or("asset"), self.use_https_scheme), + percent_encoding::utf8_percent_encode(path, crate::protocol::ENCODE_URI_COMPONENT) + )) + } + + fn is_local_url(&self, current_url: &Url) -> bool { + let uses_https = current_url.scheme() == "https"; + + // if from `tauri://` custom protocol + ({ + let protocol_url = self.manager().tauri_protocol_url(uses_https); + current_url.scheme() == protocol_url.scheme() + && current_url.domain() == protocol_url.domain() + }) || + + // or if relative to `devUrl` or `frontendDist` + self + .manager() + .get_app_url(uses_https) + .make_relative(current_url) + .is_some() + + // or from a custom protocol registered by the user + || ({ + let scheme = current_url.scheme(); + let protocols = self.manager().webview.uri_scheme_protocols.lock().unwrap(); + + #[cfg(all(not(windows), not(target_os = "android")))] + let local = protocols.contains_key(scheme); + + // on window and android, custom protocols are `http://.path/to/route` + // so we check using the first part of the domain + #[cfg(any(windows, target_os = "android"))] + let local = { + let scheme = scheme == self.manager().tauri_protocol_url(uses_https).scheme(); + let protocol = current_url + .domain() + .and_then(|d| d.strip_suffix(".localhost")) + .map(|protocol| protocols.contains_key(protocol)) + .unwrap_or_default(); + + scheme && protocol + }; + + local + }) + } + + /// Handles this window receiving an [`InvokeRequest`]. + pub fn on_message(self, request: InvokeRequest, responder: Box>) { + let manager = self.manager_owned(); + let is_local = self.is_local_url(&request.url); + + // ensure the passed key matches what our manager should have injected + let expected = manager.invoke_key(); + if request.invoke_key != expected { + #[cfg(feature = "tracing")] + tracing::error!( + "__TAURI_INVOKE_KEY__ expected {expected} but received {}", + request.invoke_key + ); + + #[cfg(not(feature = "tracing"))] + eprintln!( + "__TAURI_INVOKE_KEY__ expected {expected} but received {}", + request.invoke_key + ); + + return; + } + + let resolver = InvokeResolver::new( + self.clone(), + Arc::new(Mutex::new(Some(Box::new( + move |webview: Webview, cmd, response, callback, error| { + responder(webview, cmd, response, callback, error); + }, + )))), + request.cmd.clone(), + request.callback, + request.error, + ); + + #[cfg(mobile)] + let app_handle = self.app_handle.clone(); + + let message = InvokeMessage::new(self, request.cmd.to_string(), request.body, request.headers); + + let acl_origin = if is_local { + Origin::Local + } else { + Origin::Remote { + url: request.url.clone(), + } + }; + let (resolved_acl, has_app_acl_manifest) = { + let runtime_authority = manager.runtime_authority.lock().unwrap(); + let acl = runtime_authority.resolve_access( + &request.cmd, + message.webview.window_ref().label(), + message.webview.label(), + &acl_origin, + ); + (acl, runtime_authority.has_app_manifest()) + }; + + let mut invoke = Invoke { + message, + resolver: resolver.clone(), + acl: resolved_acl, + }; + + let plugin_command = request.cmd.strip_prefix("plugin:").map(|raw_command| { + let mut tokens = raw_command.split('|'); + // safe to unwrap: split always has a least one item + let plugin = tokens.next().unwrap(); + let command = tokens.next().map(|c| c.to_string()).unwrap_or_default(); + (plugin, command) + }); + + // 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() + { + #[cfg(debug_assertions)] + { + let (key, command_name) = plugin_command + .clone() + .unwrap_or_else(|| (tauri_utils::acl::APP_ACL_KEY, request.cmd.clone())); + invoke.resolver.reject( + manager + .runtime_authority + .lock() + .unwrap() + .resolve_access_message( + key, + &command_name, + invoke.message.webview.window().label(), + invoke.message.webview.label(), + &acl_origin, + ), + ); + } + #[cfg(not(debug_assertions))] + invoke + .resolver + .reject(format!("Command {} not allowed by ACL", request.cmd)); + return; + } + + if let Some((plugin, command_name)) = plugin_command { + invoke.message.command = command_name; + + #[cfg(desktop)] + let command = invoke.message.command.clone(); + + #[cfg(mobile)] + let message = invoke.message.clone(); + + #[allow(unused_mut)] + let mut handled = manager.run_plugin_invoke_handler(plugin, invoke); + + if handled { + return; + } + + #[cfg(mobile)] + { + fn load_channels(payload: &serde_json::Value, webview: &Webview) { + use std::str::FromStr; + + if let serde_json::Value::Object(map) = payload { + for v in map.values() { + if let serde_json::Value::String(s) = v { + let _ = crate::ipc::JavaScriptChannelId::from_str(s) + .map(|id| id.channel_on::(webview.clone())); + } + } + } + } + + let payload = message.payload.into_json(); + // initialize channels + load_channels(&payload, &message.webview); + + let resolver_ = resolver.clone(); + if let Err(e) = crate::plugin::mobile::run_command( + plugin, + &app_handle, + heck::AsLowerCamelCase(message.command).to_string(), + payload, + move |response| match response { + Ok(r) => resolver_.resolve(r), + Err(e) => resolver_.reject(e), + }, + ) { + resolver.reject(e.to_string()); + } + } + + #[cfg(desktop)] + resolver.reject(format!("Command {command} not found")); + } else { + let command = invoke.message.command.clone(); + let handled = manager.run_invoke_handler(invoke); + if !handled { + resolver.reject(format!("Command {command} not found")); + } + } + } + + /// Evaluates JavaScript on this window. + pub fn eval(&self, js: impl Into) -> crate::Result<()> { + self + .webview + .dispatcher + .eval_script(js.into()) + .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, + event: EventName<&str>, + target: EventTarget, + handler: CallbackFn, + ) -> crate::Result { + let listeners = self.manager().listeners(); + + let id = listeners.next_event_id(); + + self.eval(crate::event::listen_js_script( + listeners.listeners_object_name(), + &serde_json::to_string(&target)?, + event, + id, + handler, + ))?; + + listeners.listen_js(event, self.label(), target, id); + + Ok(id) + } + + /// Unregister a JS event listener. + pub(crate) fn unlisten_js(&self, event: EventName<&str>, id: EventId) -> crate::Result<()> { + let listeners = self.manager().listeners(); + + listeners.unlisten_js(event, id); + + Ok(()) + } + + pub(crate) fn emit_js(&self, emit_args: &EmitArgs, ids: &[u32]) -> crate::Result<()> { + self.eval(crate::event::emit_js_script( + self.manager().listeners().function_name(), + emit_args, + &serde_json::to_string(ids)?, + )?)?; + Ok(()) + } + + /// Opens the developer tools window (Web Inspector). + /// The devtools is only enabled on debug builds or with the `devtools` feature flag. + /// + /// ## Platform-specific + /// + /// - **macOS:** Only supported on macOS 10.15+. + /// This is a private API on macOS, so you cannot use this if your application will be published on the App Store. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::Manager; +tauri::Builder::default() + .setup(|app| { + #[cfg(debug_assertions)] + app.get_webview("main").unwrap().open_devtools(); + Ok(()) + }); +``` + "#### + )] + #[cfg(any(debug_assertions, feature = "devtools"))] + #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))] + pub fn open_devtools(&self) { + self.webview.dispatcher.open_devtools(); + } + + /// Closes the developer tools window (Web Inspector). + /// The devtools is only enabled on debug builds or with the `devtools` feature flag. + /// + /// ## Platform-specific + /// + /// - **macOS:** Only supported on macOS 10.15+. + /// This is a private API on macOS, so you cannot use this if your application will be published on the App Store. + /// - **Windows:** Unsupported. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::Manager; +tauri::Builder::default() + .setup(|app| { + #[cfg(debug_assertions)] + { + let webview = app.get_webview("main").unwrap(); + webview.open_devtools(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(10)); + webview.close_devtools(); + }); + } + Ok(()) + }); +``` + "#### + )] + #[cfg(any(debug_assertions, feature = "devtools"))] + #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))] + pub fn close_devtools(&self) { + self.webview.dispatcher.close_devtools(); + } + + /// Checks if the developer tools window (Web Inspector) is opened. + /// The devtools is only enabled on debug builds or with the `devtools` feature flag. + /// + /// ## Platform-specific + /// + /// - **macOS:** Only supported on macOS 10.15+. + /// This is a private API on macOS, so you cannot use this if your application will be published on the App Store. + /// - **Windows:** Unsupported. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::Manager; +tauri::Builder::default() + .setup(|app| { + #[cfg(debug_assertions)] + { + let webview = app.get_webview("main").unwrap(); + if !webview.is_devtools_open() { + webview.open_devtools(); + } + } + Ok(()) + }); +``` + "#### + )] + #[cfg(any(debug_assertions, feature = "devtools"))] + #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))] + pub fn is_devtools_open(&self) -> bool { + self + .webview + .dispatcher + .is_devtools_open() + .unwrap_or_default() + } + + /// Set the webview zoom level + /// + /// ## Platform-specific: + /// + /// - **Android**: Not supported. + /// - **macOS**: available on macOS 11+ only. + /// - **iOS**: available on iOS 14+ only. + pub fn set_zoom(&self, scale_factor: f64) -> crate::Result<()> { + self + .webview + .dispatcher + .set_zoom(scale_factor) + .map_err(Into::into) + } + + /// Specify the webview background color. + /// + /// ## Platform-specific: + /// + /// - **macOS / iOS**: Not implemented. + /// - **Windows**: + /// - On Windows 7, transparency is not supported and the alpha value will be ignored. + /// - On Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` + pub fn set_background_color(&self, color: Option) -> crate::Result<()> { + self + .webview + .dispatcher + .set_background_color(color) + .map_err(Into::into) + } + + /// Clear all browsing data for this webview. + /// + /// This deletes every website data type the underlying webview knows about: cookies, local storage, + /// session storage, IndexedDB databases, the HTTP cache and service workers. It does not reload the + /// current page, so the document that is currently loaded keeps its in-memory state until it navigates + /// or you call [`Self::reload`]. + /// + /// The deletion is asynchronous on every platform: the data is not necessarily gone when this function returns. + /// + /// ## Platform-specific + /// + /// - **Linux**: the data is cleared for the whole WebKit web context, which is shared by every webview + /// using the same data directory, not only this one. + /// - **macOS / iOS**: clears the [`WKWebsiteDataStore`] of this webview. Webviews created with a custom + /// `WebviewBuilder::data_store_identifier` have their own store, so only that store is affected. + /// - **Windows**: implemented with `ICoreWebView2Profile2::ClearBrowsingDataAll`, so an error is returned + /// if the installed WebView2 runtime is too old to provide it. + /// + /// [`WKWebsiteDataStore`]: https://developer.apple.com/documentation/webkit/wkwebsitedatastore + pub fn clear_all_browsing_data(&self) -> crate::Result<()> { + self + .webview + .dispatcher + .clear_all_browsing_data() + .map_err(Into::into) + } + + /// Returns all cookies in the runtime's cookie store including HTTP-only and secure cookies. + /// + /// Note that cookies will only be returned for URLs with an http or https scheme. + /// Cookies set through javascript for local files + /// (such as those served from the tauri://) protocol are not currently supported. + /// + /// # Stability + /// + /// See [Self::cookies]. + /// + /// # Known issues + /// + /// See [Self::cookies]. + pub fn cookies_for_url(&self, url: Url) -> crate::Result>> { + self + .webview + .dispatcher + .cookies_for_url(url) + .map_err(Into::into) + } + + /// Returns all cookies in the runtime's cookie store for all URLs including HTTP-only and secure cookies. + /// + /// Note that cookies will only be returned for URLs with an http or https scheme. + /// Cookies set through javascript for local files + /// (such as those served from the tauri://) protocol are not currently supported. + /// + /// # Stability + /// + /// The return value of this function leverages [`tauri_runtime::Cookie`] which re-exports the cookie crate. + /// This dependency might receive updates in minor Tauri releases. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when reading cookies. + /// + /// ## Platform-specific + /// + /// - **Android**: Unsupported, always returns an empty [`Vec`]. + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn cookies(&self) -> crate::Result>> { + self.webview.dispatcher.cookies().map_err(Into::into) + } + + /// Set a cookie for the webview. + /// + /// # Stability + /// + /// See [Self::cookies]. + pub fn set_cookie(&self, cookie: Cookie<'_>) -> crate::Result<()> { + self + .webview + .dispatcher + .set_cookie(cookie) + .map_err(Into::into) + } + + /// Delete a cookie for the webview. + /// + /// # Stability + /// + /// See [Self::cookies]. + pub fn delete_cookie(&self, cookie: Cookie<'_>) -> crate::Result<()> { + self + .webview + .dispatcher + .delete_cookie(cookie) + .map_err(Into::into) + } +} + +impl Listener for Webview { + /// Listen to an event on this webview. + /// + /// # Examples + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::{Manager, Listener}; + +tauri::Builder::default() + .setup(|app| { + let webview = app.get_webview("main").unwrap(); + webview.listen("component-loaded", move |event| { + println!("webview just loaded a component"); + }); + + Ok(()) + }); +``` + "#### + )] + fn listen(&self, event: impl Into, handler: F) -> EventId + where + F: Fn(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager.listen( + event, + EventTarget::Webview { + label: self.label().to_string(), + }, + handler, + ) + } + + /// Listen to an event on this webview only once. + /// + /// See [`Self::listen`] for more information. + fn once(&self, event: impl Into, handler: F) -> EventId + where + F: FnOnce(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager.once( + event, + EventTarget::Webview { + label: self.label().to_string(), + }, + handler, + ) + } + + /// Unlisten to an event on this webview. + /// + /// # Examples + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::{Manager, Listener}; + +tauri::Builder::default() + .setup(|app| { + let webview = app.get_webview("main").unwrap(); + let webview_ = webview.clone(); + let handler = webview.listen("component-loaded", move |event| { + println!("webview just loaded a component"); + + // we no longer need to listen to the event + // we also could have used `webview.once` instead + webview_.unlisten(event.id()); + }); + + // stop listening to the event when you do not need it anymore + webview.unlisten(handler); + + Ok(()) + }); +``` + "#### + )] + fn unlisten(&self, id: EventId) { + self.manager.unlisten(id) + } +} + +impl Emitter for Webview {} + +impl Manager for Webview { + fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + self + .resources_table + .lock() + .expect("poisoned window resources table") + } +} + +impl ManagerBase for Webview { + fn manager(&self) -> &AppManager { + &self.manager + } + + fn manager_owned(&self) -> Arc> { + self.manager.clone() + } + + fn runtime(&self) -> RuntimeOrDispatch<'_, R> { + self.app_handle.runtime() + } + + 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 { + /// Grabs the [`Webview`] from the [`CommandItem`]. This will never fail. + fn from_command(command: CommandItem<'de, R>) -> Result { + Ok(command.message.webview()) + } +} + +/// Resolved scope that can be obtained via [`Webview::resolve_command_scope`]. +pub struct ResolvedScope { + command_scope: CommandScope, + global_scope: GlobalScope, +} + +impl ResolvedScope { + /// The global plugin scope. + pub fn global_scope(&self) -> &GlobalScope { + &self.global_scope + } + + /// The command-specific scope. + pub fn command_scope(&self) -> &CommandScope { + &self.command_scope + } +} + +#[cfg(test)] +mod tests { + use url::Url; + + #[test] + fn no_permission_handler_preserves_runtime_default() { + use crate::test::{mock_builder, mock_context, noop_assets}; + + let app = mock_builder().build(mock_context(noop_assets())).unwrap(); + let pending = super::WebviewBuilder::new("test", crate::WebviewUrl::default()) + .into_pending_webview(&app, "test") + .unwrap(); + + assert!(pending.permission_request_handler.is_none()); + } + + 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::(); + } + + #[test] + fn convert_file_src_matches_js() { + use crate::test::{mock_builder, mock_context, noop_assets}; + + let app = mock_builder().build(mock_context(noop_assets())).unwrap(); + let http = crate::WebviewWindowBuilder::new(&app, "http", crate::WebviewUrl::default()) + .build() + .unwrap(); + let https = crate::WebviewWindowBuilder::new(&app, "https", crate::WebviewUrl::default()) + .use_https_scheme(true) + .build() + .unwrap(); + assert!(!http.webview.use_https_scheme()); + assert!(https.webview.use_https_scheme()); + + // `encoded` is what `encodeURIComponent(path)` returns in JS + #[cfg(windows)] + let (path, encoded) = ( + r"C:\Users\me\my-file (1).mp4", + "C%3A%5CUsers%5Cme%5Cmy-file%20(1).mp4", + ); + #[cfg(not(windows))] + let (path, encoded) = ( + "/home/me/my-file (1).mp4", + "%2Fhome%2Fme%2Fmy-file%20(1).mp4", + ); + + let convert = + |w: &crate::WebviewWindow<_>, protocol| w.convert_file_src(path, protocol).unwrap(); + + #[cfg(any(windows, target_os = "android"))] + { + assert_eq!( + convert(&http, None), + format!("http://asset.localhost/{encoded}") + ); + assert_eq!( + convert(&https, None), + format!("https://asset.localhost/{encoded}") + ); + assert_eq!( + convert(&http, Some("custom")), + format!("http://custom.localhost/{encoded}") + ); + } + #[cfg(not(any(windows, target_os = "android")))] + { + assert_eq!(convert(&http, None), format!("asset://localhost/{encoded}")); + assert_eq!( + convert(&https, None), + format!("asset://localhost/{encoded}") + ); + assert_eq!( + convert(&http, Some("custom")), + format!("custom://localhost/{encoded}") + ); + } + } + + #[cfg(unix)] + #[test] + fn convert_file_src_rejects_non_utf8_path() { + use std::os::unix::ffi::OsStrExt; + + let webview = test_webview_window(); + let path = std::path::Path::new(std::ffi::OsStr::from_bytes(b"/tmp/\xff.mp4")); + assert!(matches!( + webview.convert_file_src(path, None), + Err(crate::Error::NonUtf8Path(_)) + )); + } + + #[test] + fn tauri_protocol_is_local() { + let webview = test_webview_window().webview; + + #[cfg(all(not(windows), not(target_os = "android")))] + assert!(webview.is_local_url(&Url::parse("tauri://localhost/").unwrap())); + + #[cfg(any(windows, target_os = "android"))] + assert!(webview.is_local_url(&Url::parse("https://tauri.localhost/").unwrap())); + } + + // On Windows/Android, custom protocols are served as `https://.localhost/`. + // We ensure only `.localhost` domains are accepted to prevent a subdomain being able to + // impersonate a protocol name. + #[cfg(any(windows, target_os = "android"))] + #[test] + fn windows_custom_protocol_rejects_spoofed_domain() { + use crate::test::{mock_builder, mock_context, noop_assets}; + + 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(); + + // Legitimate Windows custom protocol URL + assert!(webview.is_local_url(&url("https://myproto.localhost/"))); + + // Attacker domain that starts with a registered protocol name — must NOT be local. + assert!(!webview.is_local_url(&url("https://myproto.evil.com/"))); + + // Subdomain of .localhost with unregistered name — must NOT be local + assert!(!webview.is_local_url(&url("https://notregistered.localhost/"))); + } + + /// 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(); + + 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); + + // We expect this to work without panicking + assert!(result.is_ok(), "set_simple_fullscreen should succeed"); + } +} diff --git a/crates/tauri/src/webview/plugin.rs b/crates/tauri/src/webview/plugin.rs new file mode 100644 index 0000000..a5c10a0 --- /dev/null +++ b/crates/tauri/src/webview/plugin.rs @@ -0,0 +1,255 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The tauri plugin to create and manipulate windows from JS. + +use crate::{ + AppHandle, Runtime, WebviewWindowBuilder, command, + plugin::{Builder, TauriPlugin}, + sealed::ManagerBase, + utils::config::WindowConfig, +}; + +#[derive(serde::Serialize)] +struct WebviewRef { + window_label: String, + label: String, +} + +#[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::{ + Webview, command, + runtime::dpi::{Position, Size}, + utils::config::Color, + }; + + fn get_webview( + webview: Webview, + label: Option, + ) -> crate::Result> { + match label { + Some(l) if !l.is_empty() => webview + .manager() + .get_webview(&l) + .ok_or(crate::Error::WebviewNotFound), + _ => Ok(webview), + } + } + + macro_rules! getter { + ($cmd: ident, $ret: ty) => { + getter!($cmd, $cmd, $ret) + }; + ($fn: ident, $cmd: ident, $ret: ty) => { + #[command(root = "crate")] + pub async fn $fn( + webview: Webview, + label: Option, + ) -> crate::Result<$ret> { + get_webview(webview, label)?.$cmd().map_err(Into::into) + } + }; + } + + macro_rules! setter { + ($cmd: ident) => { + setter!($cmd, $cmd); + }; + ($fn: ident, $cmd: ident) => { + #[command(root = "crate")] + pub async fn $fn( + webview: Webview, + label: Option, + ) -> crate::Result<()> { + get_webview(webview, label)?.$cmd().map_err(Into::into) + } + }; + ($fn: ident, $cmd: ident, $input: ty) => { + #[command(root = "crate")] + pub async fn $fn( + webview: Webview, + label: Option, + value: $input, + ) -> crate::Result<()> { + get_webview(webview, label)?.$cmd(value).map_err(Into::into) + } + }; + } + + // TODO + getter!( + webview_position, + position, + tauri_runtime::dpi::PhysicalPosition + ); + getter!(webview_size, size, tauri_runtime::dpi::PhysicalSize); + //getter!(is_focused, bool); + + setter!(print); + setter!(webview_close, close); + setter!(set_webview_size, set_size, Size); + setter!(set_webview_position, set_position, Position); + setter!(set_webview_focus, set_focus); + setter!(set_webview_auto_resize, set_auto_resize, bool); + setter!(webview_hide, hide); + setter!(webview_show, show); + setter!(set_webview_zoom, set_zoom, f64); + setter!( + set_webview_background_color, + set_background_color, + Option + ); + 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, + label: Option, + window: String, + ) -> crate::Result<()> { + let webview = get_webview(webview, label)?; + if let Some(window) = webview.manager.get_window(&window) { + webview.reparent(&window)?; + } + Ok(()) + } + + #[cfg(any(debug_assertions, feature = "devtools"))] + #[command(root = "crate")] + pub async fn internal_toggle_devtools( + webview: crate::Webview, + label: Option, + ) -> crate::Result<()> { + let webview = get_webview(webview, label)?; + if webview.is_devtools_open() { + webview.close_devtools(); + } else { + webview.open_devtools(); + } + Ok(()) + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + #[allow(unused_mut)] + let mut init_script = String::new(); + // window.print works on Linux/Windows; need to use the API on macOS + #[cfg(any(target_os = "macos", target_os = "ios"))] + { + init_script.push_str(include_str!("./scripts/print.js")); + } + + #[cfg(any(debug_assertions, feature = "devtools"))] + { + use serialize_to_javascript::{DefaultTemplate, Template, default_template}; + + #[derive(Template)] + #[default_template("./scripts/toggle-devtools.js")] + struct Devtools<'a> { + os_name: &'a str, + } + + init_script.push_str( + &Devtools { + os_name: std::env::consts::OS, + } + .render_default(&Default::default()) + .unwrap() + .into_string(), + ); + } + + let mut builder = Builder::new("webview"); + if !init_script.is_empty() { + builder = builder.js_init_script(init_script); + } + + builder + .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/scripts/print.js b/crates/tauri/src/webview/scripts/print.js new file mode 100644 index 0000000..df5b92a --- /dev/null +++ b/crates/tauri/src/webview/scripts/print.js @@ -0,0 +1,7 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +window.print = function () { + return window.__TAURI_INTERNALS__.invoke('plugin:webview|print') +} diff --git a/crates/tauri/src/webview/scripts/toggle-devtools.js b/crates/tauri/src/webview/scripts/toggle-devtools.js new file mode 100644 index 0000000..5997187 --- /dev/null +++ b/crates/tauri/src/webview/scripts/toggle-devtools.js @@ -0,0 +1,31 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +;(function () { + function toggleDevtoolsHotkey() { + const osName = __TEMPLATE_os_name__ + + const isHotkey = + osName === 'macos' + ? (event) => event.metaKey && event.altKey && event.code === 'KeyI' + : (event) => event.ctrlKey && event.shiftKey && event.code === 'KeyI' + + document.addEventListener('keydown', (event) => { + if (isHotkey(event)) { + window.__TAURI_INTERNALS__.invoke( + 'plugin:webview|internal_toggle_devtools' + ) + } + }) + } + + if ( + document.readyState === 'complete' + || document.readyState === 'interactive' + ) { + toggleDevtoolsHotkey() + } else { + window.addEventListener('DOMContentLoaded', toggleDevtoolsHotkey, true) + } +})() diff --git a/crates/tauri/src/webview/scripts/zoom-hotkey.js b/crates/tauri/src/webview/scripts/zoom-hotkey.js new file mode 100644 index 0000000..bc26224 --- /dev/null +++ b/crates/tauri/src/webview/scripts/zoom-hotkey.js @@ -0,0 +1,43 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const OS_NAME = __TEMPLATE_os_name__ + +let zoomLevel = 1 + +const MAX_ZOOM_LEVEL = 10 +const MIN_ZOOM_LEVEL = 0.2 + +window.addEventListener('keydown', (event) => { + if (OS_NAME === 'macos' ? event.metaKey : event.ctrlKey) { + if (event.key === '-') { + zoomLevel -= 0.2 + } else if (event.key === '=' || event.key === '+') { + zoomLevel += 0.2 + } else if (event.key === '0') { + zoomLevel = 1 + } else { + return + } + zoomLevel = Math.min(Math.max(zoomLevel, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL) + window.__TAURI_INTERNALS__.invoke('plugin:webview|set_webview_zoom', { + value: zoomLevel + }) + } +}) + +window.addEventListener('mousewheel', (event) => { + if (event.ctrlKey) { + event.preventDefault() + if (event.deltaY < 0) { + zoomLevel += 0.2 + } else { + zoomLevel -= 0.2 + } + zoomLevel = Math.min(Math.max(zoomLevel, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL) + window.__TAURI_INTERNALS__.invoke('plugin:webview|set_webview_zoom', { + value: zoomLevel + }) + } +}) diff --git a/crates/tauri/src/webview/webview_window.rs b/crates/tauri/src/webview/webview_window.rs new file mode 100644 index 0000000..799f351 --- /dev/null +++ b/crates/tauri/src/webview/webview_window.rs @@ -0,0 +1,2879 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! [`Window`] that hosts a single [`Webview`]. + +use std::{ + borrow::Cow, + path::{Path, PathBuf}, + sync::{Arc, MutexGuard}, +}; + +use crate::{ + Emitter, EventName, Listener, ResourceTable, Window, + event::EventTarget, + ipc::ScopeObject, + runtime::dpi::{PhysicalPosition, PhysicalSize, Position, Size}, + webview::{NewWindowResponse, ScrollBarStyle}, + window::Monitor, +}; +#[cfg(desktop)] +use crate::{ + image::Image, + menu::{ContextMenu, Menu}, + runtime::{UserAttentionType, window::CursorIcon}, +}; +use tauri_runtime::webview::NewWindowFeatures; +use tauri_utils::config::{BackgroundThrottlingPolicy, Color, WebviewUrl, WindowConfig}; +use url::Url; + +use crate::{ + AppHandle, Event, EventId, Manager, Runtime, Webview, WindowEvent, + ipc::{CommandArg, CommandItem, InvokeError, OwnedInvokeResponder}, + manager::AppManager, + sealed::{ManagerBase, RuntimeOrDispatch}, + webview::{Cookie, PageLoadPayload, WebviewBuilder, WebviewEvent}, + window::WindowBuilder, +}; + +use tauri_macros::default_runtime; + +#[cfg(windows)] +use windows::Win32::Foundation::HWND; + +use super::{DownloadEvent, ResolvedScope}; + +/// A builder for [`WebviewWindow`], a window that hosts a single webview. +pub struct WebviewWindowBuilder<'a, R: Runtime, M: Manager> { + window_builder: WindowBuilder<'a, R, M>, + webview_builder: WebviewBuilder, +} + +impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { + /// Initializes a webview window builder with the given window label. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command and event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when creating windows. + /// + /// # Examples + /// + /// - Create a window in the setup hook: + /// + /// ``` + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview_window = tauri::WebviewWindowBuilder::new(app, "label", tauri::WebviewUrl::App("index.html".into())) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + /// + /// - Create a window in a separate thread: + /// + /// ``` + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle().clone(); + /// std::thread::spawn(move || { + /// let webview_window = tauri::WebviewWindowBuilder::new(&handle, "label", tauri::WebviewUrl::App("index.html".into())) + /// .build() + /// .unwrap(); + /// }); + /// Ok(()) + /// }); + /// ``` + /// + /// - Create a window in a command: + /// + /// ``` + /// #[tauri::command] + /// async fn create_window(app: tauri::AppHandle) { + /// let webview_window = tauri::WebviewWindowBuilder::new(&app, "label", tauri::WebviewUrl::App("index.html".into())) + /// .build() + /// .unwrap(); + /// } + /// ``` + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn new>(manager: &'a M, label: L, url: WebviewUrl) -> Self { + let label = label.into(); + Self { + window_builder: WindowBuilder::new(manager, &label), + webview_builder: WebviewBuilder::new(&label, url), + } + } + + /// Initializes a webview window builder from a [`WindowConfig`] from tauri.conf.json. + /// Keep in mind that you can't create 2 windows with the same `label` so make sure + /// that the initial window was closed or change the label of the cloned [`WindowConfig`]. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when creating windows. + /// + /// # Examples + /// + /// - Create a window in a command: + /// + /// ``` + /// #[tauri::command] + /// async fn reopen_window(app: tauri::AppHandle) { + /// let webview_window = tauri::WebviewWindowBuilder::from_config(&app, &app.config().app.windows.get(0).unwrap()) + /// .unwrap() + /// .build() + /// .unwrap(); + /// } + /// ``` + /// + /// - Create a window in a command from a config with a specific label, and change its label so multiple instances can exist: + /// + /// ``` + /// #[tauri::command] + /// async fn open_window_multiple(app: tauri::AppHandle) { + /// let mut conf = app.config().app.windows.iter().find(|c| c.label == "template-for-multiwindow").unwrap().clone(); + /// // This should be a unique label for all windows. For example, we can use a random suffix: + /// let mut buf = [0u8; 1]; + /// assert_eq!(getrandom::fill(&mut buf), Ok(())); + /// conf.label = format!("my-multiwindow-{}", buf[0]); + /// let webview_window = tauri::WebviewWindowBuilder::from_config(&app, &conf) + /// .unwrap() + /// .build() + /// .unwrap(); + /// } + /// ``` + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn from_config(manager: &'a M, config: &WindowConfig) -> crate::Result { + Ok(Self { + window_builder: WindowBuilder::from_config(manager, config)?, + webview_builder: WebviewBuilder::from_config(config), + }) + } + + /// Registers a global menu event listener. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + /// + /// Also note that this handler will not be called if + /// the window used to register it was closed. + /// + /// # Examples + /// ``` + /// use tauri::menu::{Menu, Submenu, MenuItem}; + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle(); + /// let save_menu_item = MenuItem::new(handle, "Save", true, None::<&str>)?; + /// let menu = Menu::with_items(handle, &[ + /// &Submenu::with_items(handle, "File", true, &[ + /// &save_menu_item, + /// ])?, + /// ])?; + /// let webview_window = tauri::WebviewWindowBuilder::new(app, "editor", tauri::WebviewUrl::App("index.html".into())) + /// .menu(menu) + /// .on_menu_event(move |window, event| { + /// if event.id == save_menu_item.id() { + /// // save menu item + /// } + /// }) + /// .build() + /// .unwrap(); + /// + /// Ok(()) + /// }); + /// ``` + #[cfg(desktop)] + pub fn on_menu_event, crate::menu::MenuEvent) + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.window_builder = self.window_builder.on_menu_event(f); + self + } + + /// Defines a closure to be executed when the webview makes an HTTP request for a web resource, allowing you to modify the response. + /// + /// Currently only implemented for the `tauri` URI protocol. + /// + /// **NOTE:** Currently this is **not** executed when using external URLs such as a development server, + /// but it might be implemented in the future. **Always** check the request URL. + /// + /// # Examples + /// ```rust,no_run + /// use tauri::{ + /// utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + /// webview::WebviewWindowBuilder, + /// }; + /// use http::header::HeaderValue; + /// use std::collections::HashMap; + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview_window = WebviewWindowBuilder::new(app, "core", WebviewUrl::App("index.html".into())) + /// .on_web_resource_request(|request, response| { + /// if request.uri().scheme_str() == Some("tauri") { + /// // if we have a CSP header, Tauri is loading an HTML file + /// // for this example, let's dynamically change the CSP + /// if let Some(csp) = response.headers_mut().get_mut("Content-Security-Policy") { + /// // use the tauri helper to parse the CSP policy to a map + /// let mut csp_map: HashMap = Csp::Policy(csp.to_str().unwrap().to_string()).into(); + /// csp_map.entry("script-src".to_string()).or_insert_with(Default::default).push("'unsafe-inline'"); + /// // use the tauri helper to get a CSP string from the map + /// let csp_string = Csp::from(csp_map).to_string(); + /// *csp = HeaderValue::from_str(&csp_string).unwrap(); + /// } + /// } + /// }) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + pub fn on_web_resource_request< + F: Fn(http::Request>, &mut http::Response>) + Send + Sync + 'static, + >( + mut self, + f: F, + ) -> Self { + self.webview_builder = self.webview_builder.on_web_resource_request(f); + self + } + + /// Defines a closure to be executed when the webview navigates to a URL. Returning `false` cancels the navigation. + /// + /// # Examples + /// ```rust,no_run + /// use tauri::{ + /// utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + /// webview::WebviewWindowBuilder, + /// }; + /// use http::header::HeaderValue; + /// use std::collections::HashMap; + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview_window = WebviewWindowBuilder::new(app, "core", WebviewUrl::App("index.html".into())) + /// .on_navigation(|url| { + /// // allow the production URL or localhost on dev + /// url.scheme() == "tauri" || (cfg!(dev) && url.host_str() == Some("localhost")) + /// }) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + pub fn on_navigation bool + Send + 'static>(mut self, f: F) -> Self { + self.webview_builder = self.webview_builder.on_navigation(f); + self + } + + /// Set a new window request handler to decide if incoming url is allowed to be opened. + /// + /// A new window is requested to be opened by the [window.open] API. + /// + /// The closure take the URL to open and the window features object and returns [`NewWindowResponse`] to determine whether the window should open. + /// + /// # Examples + /// ```rust,no_run + /// use tauri::{ + /// utils::config::WebviewUrl, + /// webview::WebviewWindowBuilder, + /// }; + /// use http::header::HeaderValue; + /// use std::collections::HashMap; + /// tauri::Builder::default() + /// .setup(|app| { + /// let app_ = app.handle().clone(); + /// let webview_window = WebviewWindowBuilder::new(app, "core", WebviewUrl::App("index.html".into())) + /// .on_new_window(move |url, features| { + /// let builder = tauri::WebviewWindowBuilder::new( + /// &app_, + /// // note: add an ID counter or random label generator to support multiple opened windows at the same time + /// "opened-window", + /// tauri::WebviewUrl::External("about:blank".parse().unwrap()), + /// ) + /// .window_features(features) + /// .on_document_title_changed(|window, title| { + /// window.set_title(&title).unwrap(); + /// }) + /// .title(url.as_str()); + /// + /// let window = builder.build().unwrap(); + /// tauri::webview::NewWindowResponse::Create { window } + /// }) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + /// + /// # Platform-specific + /// + /// - **Android / iOS**: Not supported. + /// + /// [window.open]: https://developer.mozilla.org/en-US/docs/Web/API/Window/open + pub fn on_new_window NewWindowResponse + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.webview_builder = self.webview_builder.on_new_window(f); + self + } + + /// Defines a closure to be executed when the document title changes. + /// + /// Note that it may run before or after the navigation event. + pub fn on_document_title_changed, String) + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.webview_builder = self + .webview_builder + .on_document_title_changed(move |webview, url| { + f( + WebviewWindow { + window: webview.window(), + webview, + }, + url, + ) + }); + self + } + + /// Set a download event handler to be notified when a download is requested or finished. + /// + /// Returning `false` prevents the download from happening on a [`DownloadEvent::Requested`] event. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::{ + utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + webview::{DownloadEvent, WebviewWindowBuilder}, +}; + +tauri::Builder::default() + .setup(|app| { + let handle = app.handle(); + let webview_window = WebviewWindowBuilder::new(handle, "core", WebviewUrl::App("index.html".into())) + .on_download(|webview, event| { + match event { + DownloadEvent::Requested { url, destination } => { + println!("downloading {}", url); + *destination = "/home/tauri/target/path".into(); + } + DownloadEvent::Finished { url, path, success } => { + println!("downloaded {} to {:?}, success: {}", url, path, success); + } + _ => (), + } + // let the download start + true + }) + .build()?; + + Ok(()) + }); +``` + "#### + )] + pub fn on_download, DownloadEvent<'_>) -> bool + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.webview_builder.download_handler.replace(Arc::new(f)); + self + } + + /// Defines a closure to be executed when a page load event is triggered. + /// The event can be either [`tauri_runtime::webview::PageLoadEvent::Started`] if the page has started loading + /// or [`tauri_runtime::webview::PageLoadEvent::Finished`] when the page finishes loading. + /// + /// # Examples + /// ```rust,no_run + /// use tauri::{ + /// utils::config::{Csp, CspDirectiveSources, WebviewUrl}, + /// webview::{PageLoadEvent, WebviewWindowBuilder}, + /// }; + /// use http::header::HeaderValue; + /// use std::collections::HashMap; + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview_window = WebviewWindowBuilder::new(app, "core", WebviewUrl::App("index.html".into())) + /// .on_page_load(|window, payload| { + /// match payload.event() { + /// PageLoadEvent::Started => { + /// println!("{} finished loading", payload.url()); + /// } + /// PageLoadEvent::Finished => { + /// println!("{} finished loading", payload.url()); + /// } + /// } + /// }) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + pub fn on_page_load, PageLoadPayload<'_>) + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.webview_builder = self.webview_builder.on_page_load(move |webview, payload| { + f( + WebviewWindow { + window: webview.window(), + webview, + }, + payload, + ) + }); + self + } + + /// Defines a closure to be executed when a permission is requested. + /// + /// The handler receives the [`crate::webview::PermissionKind`] and should return + /// the desired [`crate::webview::PermissionResponse`]. + /// + /// > [!NOTE] + /// > This handler only triggers for new permission requests. If the user has already + /// > allowed or denied a permission persistently within the webview, the browser + /// > will use the saved preference instead of calling this handler. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Fully supported via WebView2's PermissionRequested event. + /// - **macOS / iOS**: Fully supported via WKUIDelegate's requestMediaCapturePermission. + /// - **Linux**: Fully supported via WebKitGTK's permission-request signal. + /// - **Android**: Supported via JNI bridge for geolocation, microphone, camera, + /// protected media, and MIDI requests. Android runtime permissions may still + /// trigger native OS prompts before access is granted. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::{ + /// webview::{PermissionKind, PermissionResponse, WebviewWindowBuilder}, + /// WebviewUrl, + /// }; + /// tauri::Builder::default() + /// .setup(|app| { + /// WebviewWindowBuilder::new(app, "core", WebviewUrl::App("index.html".into())) + /// .on_permission_request(|_, kind| match kind { + /// PermissionKind::Geolocation => PermissionResponse::Allow, + /// PermissionKind::Notifications => PermissionResponse::Allow, + /// _ => PermissionResponse::Default, + /// }) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + pub fn on_permission_request< + F: Fn(Webview, crate::webview::PermissionKind) -> crate::webview::PermissionResponse + + Send + + Sync + + 'static, + >( + mut self, + f: F, + ) -> Self { + self.webview_builder = self.webview_builder.on_permission_request(f); + self + } + + /// Creates a new window. + pub fn build(self) -> crate::Result> { + let (window, webview) = self.window_builder.with_webview(self.webview_builder)?; + Ok(WebviewWindow { window, webview }) + } +} + +/// Desktop APIs. +#[cfg(desktop)] +impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { + /// Sets the menu for the window. + #[must_use] + pub fn menu(mut self, menu: crate::menu::Menu) -> Self { + self.window_builder = self.window_builder.menu(menu); + self + } + + /// Show window in the center of the screen. + #[must_use] + pub fn center(mut self) -> Self { + self.window_builder = self.window_builder.center(); + 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` + /// + /// **NOTE**: The overflow check is only performed on window creation, resizes can still overflow + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + pub fn prevent_overflow(mut self) -> Self { + self.window_builder = self.window_builder.prevent_overflow(); + self + } + + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) + /// on creation with a margin, which means the window size will be limited to `monitor size - taskbar size - margin size` + /// + /// **NOTE**: The overflow check is only performed on window creation, resizes can still overflow + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + pub fn prevent_overflow_with_margin(mut self, margin: impl Into) -> Self { + self.window_builder = self.window_builder.prevent_overflow_with_margin(margin); + self + } + + /// Whether the window's native maximize button is enabled or not. + /// 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. + #[must_use] + pub fn maximizable(mut self, maximizable: bool) -> Self { + self.window_builder = self.window_builder.maximizable(maximizable); + self + } + + /// Whether the window's native minimize button is enabled or not. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + #[must_use] + pub fn minimizable(mut self, minimizable: bool) -> Self { + self.window_builder = self.window_builder.minimizable(minimizable); + self + } + + /// Whether the window's native close button is enabled or not. + /// + /// ## 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. + #[must_use] + pub fn closable(mut self, closable: bool) -> Self { + self.window_builder = self.window_builder.closable(closable); + self + } + + /// Whether to start the window in fullscreen or not. + #[must_use] + pub fn fullscreen(mut self, fullscreen: bool) -> Self { + self.window_builder = self.window_builder.fullscreen(fullscreen); + 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.webview_builder = self.webview_builder.focused(focused); + self + } + + /// Whether the window should be maximized upon creation. + #[must_use] + pub fn maximized(mut self, maximized: bool) -> Self { + self.window_builder = self.window_builder.maximized(maximized); + self + } + + /// Whether the window should have borders and bars. + #[must_use] + pub fn decorations(mut self, decorations: bool) -> Self { + self.window_builder = self.window_builder.decorations(decorations); + self + } + + /// Whether the window should always be below other windows. + #[must_use] + pub fn always_on_bottom(mut self, always_on_bottom: bool) -> Self { + self.window_builder = self.window_builder.always_on_bottom(always_on_bottom); + self + } + + /// Whether the window should always be on top of other windows. + #[must_use] + pub fn always_on_top(mut self, always_on_top: bool) -> Self { + self.window_builder = self.window_builder.always_on_top(always_on_top); + self + } + + /// Whether the window will be visible on all workspaces or virtual desktops. + #[must_use] + pub fn visible_on_all_workspaces(mut self, visible_on_all_workspaces: bool) -> Self { + self.window_builder = self + .window_builder + .visible_on_all_workspaces(visible_on_all_workspaces); + self + } + + /// Sets the window icon. + pub fn icon(mut self, icon: Image<'a>) -> crate::Result { + self.window_builder = self.window_builder.icon(icon)?; + Ok(self) + } + + /// Sets whether or not the window icon should be hidden from the taskbar. + /// + /// ## Platform-specific + /// + /// - **macOS**: Unsupported. + #[must_use] + pub fn skip_taskbar(mut self, skip: bool) -> Self { + self.window_builder = self.window_builder.skip_taskbar(skip); + self + } + + /// Sets custom name for Windows' window class. **Windows only**. + #[must_use] + pub fn window_classname>(mut self, classname: S) -> Self { + self.window_builder = self.window_builder.window_classname(classname); + self + } + + /// This sets `WS_EX_NOREDIRECTIONBITMAP`. + /// + /// This can avoid the white flash that may appear before the webview content is rendered + /// when using a transparent window. **Windows only**. + #[must_use] + pub fn no_redirection_bitmap(mut self, enable: bool) -> Self { + self.window_builder = self.window_builder.no_redirection_bitmap(enable); + self + } + + /// Sets whether or not the window has shadow. + /// + /// ## Platform-specific + /// + /// - **Windows:** + /// - `false` has no effect on decorated window, shadows are always ON. + /// - `true` will make undecorated window have a 1px white border, + /// and on Windows 11, it will have a rounded corners. + /// - **Linux:** Unsupported. + #[must_use] + pub fn shadow(mut self, enable: bool) -> Self { + self.window_builder = self.window_builder.shadow(enable); + self + } + + /// Sets a parent to the window to be created. + /// + /// ## Platform-specific + /// + /// - **Windows**: This sets the passed parent as an owner window to the window to be created. + /// From [MSDN owned windows docs](https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#owned-windows): + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// - **Linux**: This makes the new window transient for parent, see + /// - **macOS**: This adds the window as a child of parent, see + pub fn parent(mut self, parent: &WebviewWindow) -> crate::Result { + self.window_builder = self.window_builder.parent(&parent.window)?; + Ok(self) + } + + /// Set an owner to the window to be created. + /// + /// From MSDN: + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// + /// For more information, see + #[cfg(windows)] + pub fn owner(mut self, owner: &WebviewWindow) -> crate::Result { + self.window_builder = self.window_builder.owner(&owner.window)?; + Ok(self) + } + + /// Set an owner to the window to be created. + /// + /// From MSDN: + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// + /// For more information, see + #[cfg(windows)] + #[must_use] + pub fn owner_raw(mut self, owner: HWND) -> Self { + self.window_builder = self.window_builder.owner_raw(owner); + self + } + + /// Sets a parent to the window to be created. + /// + /// A child window has the WS_CHILD style and is confined to the client area of its parent window. + /// + /// For more information, see + #[cfg(windows)] + #[must_use] + pub fn parent_raw(mut self, parent: HWND) -> Self { + self.window_builder = self.window_builder.parent_raw(parent); + self + } + + /// Sets a parent to the window to be created. + /// + /// See + #[cfg(target_os = "macos")] + #[must_use] + pub fn parent_raw(mut self, parent: *mut std::ffi::c_void) -> Self { + self.window_builder = self.window_builder.parent_raw(parent); + self + } + + /// Sets the window to be created transient for parent. + /// + /// See + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn transient_for(mut self, parent: &WebviewWindow) -> crate::Result { + self.window_builder = self.window_builder.transient_for(&parent.window)?; + Ok(self) + } + + /// Sets the window to be created transient for parent. + /// + /// See + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + #[must_use] + pub fn transient_for_raw(mut self, parent: &impl gtk::glib::IsA) -> Self { + self.window_builder = self.window_builder.transient_for_raw(parent); + self + } + + /// Enables or disables drag and drop support of this window. + /// + /// Note: this is a different config from [`Self::disable_drag_drop_handler`] + #[cfg(windows)] + #[must_use] + pub fn drag_and_drop(mut self, enabled: bool) -> Self { + self.window_builder = self.window_builder.drag_and_drop(enabled); + self + } + + /// Sets the [`crate::TitleBarStyle`]. + #[cfg(target_os = "macos")] + #[must_use] + pub fn title_bar_style(mut self, style: crate::TitleBarStyle) -> Self { + self.window_builder = self.window_builder.title_bar_style(style); + self + } + + /// Change the position of the window controls on macOS. + /// + /// Requires titleBarStyle: Overlay and decorations: true. + #[cfg(target_os = "macos")] + #[must_use] + pub fn traffic_light_position>(mut self, position: P) -> Self { + self.webview_builder.webview_attributes = self + .webview_builder + .webview_attributes + .traffic_light_position(position.into()); + self + } + + /// Whether to show a link preview when long pressing on links. Available on macOS and iOS only. + /// + /// Default is true. + /// + /// See https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / Android:** Unsupported. + #[cfg(target_os = "macos")] + #[must_use] + pub fn allow_link_preview(mut self, allow_link_preview: bool) -> Self { + self.webview_builder = self.webview_builder.allow_link_preview(allow_link_preview); + self + } + + /// Hide the window title. + #[cfg(target_os = "macos")] + #[must_use] + pub fn hidden_title(mut self, hidden: bool) -> Self { + self.window_builder = self.window_builder.hidden_title(hidden); + self + } + + /// Defines the window [tabbing identifier] for macOS. + /// + /// Windows with matching tabbing identifiers will be grouped together. + /// If the tabbing identifier is not set, automatic tabbing will be disabled. + /// + /// [tabbing identifier]: + #[cfg(target_os = "macos")] + #[must_use] + pub fn tabbing_identifier(mut self, identifier: &str) -> Self { + self.window_builder = self.window_builder.tabbing_identifier(identifier); + self + } + + /// Sets window effects. + /// + /// Requires the window to be transparent. + /// + /// ## Platform-specific: + /// + /// - **Windows**: If using decorations or shadows, you may want to try this workaround + /// - **Linux**: Unsupported + pub fn effects(mut self, effects: crate::utils::config::WindowEffectsConfig) -> Self { + self.window_builder = self.window_builder.effects(effects); + self + } +} + +/// 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. + #[must_use] + pub fn accept_first_mouse(mut self, accept: bool) -> Self { + self.webview_builder = self.webview_builder.accept_first_mouse(accept); + self + } + + /// Adds the provided JavaScript to a list of scripts that should be run after the global object has been created, + /// but before the HTML document has been parsed and before any other script included by the HTML document is run. + /// + /// Since it runs on all top-level document navigations, + /// it's recommended to check the `window.location` to guard your script from running on unexpected origins. + /// + /// This is executed only on the main frame. + /// If you only want to run it in all frames, use [Self::initialization_script_for_all_frames] instead. + /// + /// ## Platform-specific + /// + /// - **Windows:** scripts are always added to subframes. + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// # Examples + /// + /// ```rust + /// const INIT_SCRIPT: &str = r#" + /// if (window.location.origin === 'https://tauri.app') { + /// console.log("hello world from js init script"); + /// + /// window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' }; + /// } + /// "#; + /// + /// fn main() { + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview = tauri::WebviewWindowBuilder::new(app, "label", tauri::WebviewUrl::App("index.html".into())) + /// .initialization_script(INIT_SCRIPT) + /// .build()?; + /// Ok(()) + /// }); + /// } + /// ``` + /// + /// [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); + self + } + + /// Adds the provided JavaScript to a list of scripts that should be run after the global object has been created, + /// but before the HTML document has been parsed and before any other script included by the HTML document is run. + /// + /// Since it runs on all top-level document navigations and also child frame page navigations, + /// it's recommended to check the `window.location` to guard your script from running on unexpected origins. + /// + /// This is executed on all frames (main frame and also sub frames). + /// If you only want to run the script in the main frame, use [Self::initialization_script] instead. + /// + /// ## Platform-specific + /// + /// - **Android:** When [addDocumentStartJavaScript] is not supported, + /// we prepend initialization scripts to each HTML head (implementation only supported on custom protocol URLs). + /// For remote URLs, we use [onPageStarted] which is not guaranteed to run before other scripts. + /// + /// # Examples + /// + /// ```rust + /// const INIT_SCRIPT: &str = r#" + /// if (window.location.origin === 'https://tauri.app') { + /// console.log("hello world from js init script"); + /// + /// window.__MY_CUSTOM_PROPERTY__ = { foo: 'bar' }; + /// } + /// "#; + /// + /// fn main() { + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview = tauri::WebviewWindowBuilder::new(app, "label", tauri::WebviewUrl::App("index.html".into())) + /// .initialization_script_for_all_frames(INIT_SCRIPT) + /// .build()?; + /// Ok(()) + /// }); + /// } + /// ``` + /// + /// [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 + .webview_builder + .initialization_script_for_all_frames(script); + self + } + + /// Set the user agent for the webview + #[must_use] + pub fn user_agent(mut self, user_agent: &str) -> Self { + self.webview_builder = self.webview_builder.user_agent(user_agent); + self + } + + /// Set additional arguments for the webview. + /// + /// ## Platform-specific + /// + /// - **macOS / Linux / Android / iOS**: Unsupported. + /// + /// ## Warning + /// + /// Webview instances with different browser arguments must also have different [data directories](Self::data_directory). + /// + /// By default wry passes `--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection` + /// so if you use this method, you also need to disable these components by yourself if you want. + #[must_use] + pub fn additional_browser_args(mut self, additional_args: &str) -> Self { + self.webview_builder = self + .webview_builder + .additional_browser_args(additional_args); + self + } + + /// Data directory for the webview. + #[must_use] + pub fn data_directory(mut self, data_directory: PathBuf) -> Self { + self.webview_builder = self.webview_builder.data_directory(data_directory); + self + } + + /// Disables the webview drag and drop handler used internally to generate [`DragDropEvent`](crate::DragDropEvent)s. + /// + /// This is required to use HTML5 drag and drop APIs on the frontend on Windows since we replace the drag drop handler of WebView2. + #[must_use] + pub fn disable_drag_drop_handler(mut self) -> Self { + self.webview_builder = self.webview_builder.disable_drag_drop_handler(); + self + } + + /// Enables clipboard access for the page rendered on **Linux** and **Windows**. + /// + /// **macOS** doesn't provide such method and is always enabled by default, + /// but you still need to add menu item accelerators to use shortcuts. + #[must_use] + pub fn enable_clipboard_access(mut self) -> Self { + self.webview_builder = self.webview_builder.enable_clipboard_access(); + self + } + + /// Enable or disable incognito mode for the WebView.. + /// + /// ## Platform-specific: + /// + /// **Android**: Unsupported. + #[must_use] + pub fn incognito(mut self, incognito: bool) -> Self { + self.webview_builder = self.webview_builder.incognito(incognito); + self + } + + /// Sets the webview to automatically grow and shrink its size and position when the parent window resizes. + #[must_use] + pub fn auto_resize(mut self) -> Self { + self.webview_builder = self.webview_builder.auto_resize(); + self + } + + /// Set a proxy URL for the WebView for all network requests. + /// + /// Must be either a `http://` or a `socks5://` URL. + #[must_use] + pub fn proxy_url(mut self, url: Url) -> Self { + self.webview_builder = self.webview_builder.proxy_url(url); + 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. + /// + /// On Windows, using `no_redirection_bitmap` can help avoid a white flash when + /// creating 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 { + #[cfg(desktop)] + { + self.window_builder = self.window_builder.transparent(transparent); + } + self.webview_builder = self.webview_builder.transparent(transparent); + self + } + + /// Whether page zooming by hotkeys and mousewheel should be enabled or not. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Controls WebView2's [`IsZoomControlEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2settings?view=webview2-winrt-1.0.2420.47#iszoomcontrolenabled) setting. + /// - **MacOS / Linux**: Injects a polyfill that zooms in and out with `Ctrl/Cmd + [- = +]` hotkeys or mousewheel events, + /// 20% in each step, ranging from 20% to 1000%. Requires `core:webview:allow-set-webview-zoom` permission + /// + /// - **Android / iOS**: Unsupported. + #[must_use] + pub fn zoom_hotkeys_enabled(mut self, enabled: bool) -> Self { + self.webview_builder = self.webview_builder.zoom_hotkeys_enabled(enabled); + self + } + + /// Whether browser extensions can be installed for the webview process + /// + /// ## Platform-specific: + /// + /// - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled) + /// - **MacOS / Linux / iOS / Android** - Unsupported. + #[must_use] + pub fn browser_extensions_enabled(mut self, enabled: bool) -> Self { + self.webview_builder = self.webview_builder.browser_extensions_enabled(enabled); + self + } + + /// Set the path from which to load extensions from. Extensions stored in this path should be unpacked Chrome extensions on Windows, and compiled `.so` extensions on Linux. + /// + /// ## Platform-specific: + /// + /// - **Windows**: Browser extensions must first be enabled. See [`browser_extensions_enabled`](Self::browser_extensions_enabled) + /// - **MacOS / iOS / Android** - Unsupported. + #[must_use] + pub fn extensions_path(mut self, path: impl AsRef) -> Self { + self.webview_builder = self.webview_builder.extensions_path(path); + self + } + + /// Initialize the WebView with a custom data store identifier. + /// Can be used as a replacement for data_directory not being available in WKWebView. + /// + /// - **macOS / iOS**: Available on macOS >= 14 and iOS >= 17 + /// - **Windows / Linux / Android**: Unsupported. + #[must_use] + pub fn data_store_identifier(mut self, data_store_identifier: [u8; 16]) -> Self { + self.webview_builder = self + .webview_builder + .data_store_identifier(data_store_identifier); + self + } + + /// Sets whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android. Defaults to `false`. + /// + /// ## Note + /// + /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `://localhost` protocols used on macOS and Linux. + /// + /// ## Warning + /// + /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data. + #[must_use] + pub fn use_https_scheme(mut self, enabled: bool) -> Self { + self.webview_builder = self.webview_builder.use_https_scheme(enabled); + self + } + + /// Whether web inspector, which is usually called browser devtools, is enabled or not. Enabled by default. + /// + /// This API works in **debug** builds, but requires `devtools` feature flag to enable it in **release** builds. + /// + /// ## Platform-specific + /// + /// - macOS: This will call private functions on **macOS**. + /// - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android. + /// - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window. + #[must_use] + pub fn devtools(mut self, enabled: bool) -> Self { + self.webview_builder = self.webview_builder.devtools(enabled); + self + } + + /// Set the window and webview background color. + /// + /// ## Platform-specific: + /// + /// - **Android / iOS:** Unsupported for the window layer. + /// - **macOS / iOS**: Not implemented for the webview layer. + /// - **Windows**: + /// - alpha channel is ignored for the window layer. + /// - On Windows 7, alpha channel is ignored for the webview layer. + /// - On Windows 8 and newer, if alpha channel is not `0`, it will be ignored. + #[must_use] + pub fn background_color(mut self, color: Color) -> Self { + self.window_builder = self.window_builder.background_color(color); + self.webview_builder = self.webview_builder.background_color(color); + self + } + + /// Change the default background throttling behaviour. + /// + /// By default, browsers use a suspend policy that will throttle timers and even unload + /// the whole tab (view) to free resources after roughly 5 minutes when a view became + /// minimized or hidden. This will pause all tasks until the documents visibility state + /// changes back from hidden to visible by bringing the view back to the foreground. + /// + /// ## Platform-specific + /// + /// - **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice. + /// - **iOS**: Supported since version 17.0+. + /// - **macOS**: Supported since version 14.0+. + /// + /// see + #[must_use] + pub fn background_throttling(mut self, policy: BackgroundThrottlingPolicy) -> Self { + self.webview_builder = self.webview_builder.background_throttling(policy); + self + } + + /// Whether JavaScript should be disabled. + #[must_use] + pub fn disable_javascript(mut self) -> Self { + self.webview_builder = self.webview_builder.disable_javascript(); + self + } + + /// Specifies the native scrollbar style to use with the webview. + /// CSS styles that modifier the scrollbar are applied on top of the native appearance configured here. + /// + /// Defaults to [`ScrollBarStyle::Default`], which is the browser default. + /// + /// ## Platform-specific + /// + /// - **Windows**: + /// - [`ScrollBarStyle::FluentOverlay`] requires WebView2 Runtime version 125.0.2535.41 or higher, + /// and does nothing on older versions. + /// - This option must be given the same value for all webviews that target the same data directory. Use + /// [`WebviewWindowBuilder::data_directory`] to change data directories if needed. + /// - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation. + #[must_use] + pub fn scroll_bar_style(mut self, style: ScrollBarStyle) -> Self { + self.webview_builder = self.webview_builder.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.webview_builder = self.webview_builder.general_autofill_enabled(enabled); + self + } + + /// Allows overriding the keyboard accessory view on iOS. + /// Returning `None` effectively removes the view. + /// + /// The closure parameter is the webview instance. + /// + /// The accessory view is the view that appears above the keyboard when a text input element is focused. + /// It usually displays a view with "Done", "Next" buttons. + /// + /// # Examples + /// + /// ``` + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut builder = tauri::WebviewWindowBuilder::new(app, "label", tauri::WebviewUrl::App("index.html".into())); + /// #[cfg(target_os = "ios")] + /// { + /// window_builder = window_builder.with_input_accessory_view_builder(|_webview| unsafe { + /// let mtm = objc2::MainThreadMarker::new_unchecked(); + /// let button = objc2_ui_kit::UIButton::buttonWithType(objc2_ui_kit::UIButtonType(1), mtm); + /// button.setTitle_forState( + /// Some(&objc2_foundation::NSString::from_str("Tauri")), + /// objc2_ui_kit::UIControlState(0), + /// ); + /// Some(button.downcast().unwrap()) + /// }); + /// } + /// let webview = builder.build()?; + /// Ok(()) + /// }); + /// ``` + /// + /// # Stability + /// + /// This relies on [`objc2_ui_kit`] which does not provide a stable API yet, so it can receive breaking changes in minor releases. + #[cfg(target_os = "ios")] + pub fn with_input_accessory_view_builder< + F: Fn(&objc2_ui_kit::UIView) -> Option> + + Send + + Sync + + 'static, + >( + mut self, + builder: F, + ) -> Self { + self.webview_builder = self + .webview_builder + .with_input_accessory_view_builder(builder); + self + } + + /// Whether to limit navigations to App-Bound Domains. This is necessary to + /// enable Service Workers on iOS according to + /// [StackOverflow](https://stackoverflow.com/questions/49673399/service-workers-unavailable-in-wkwebview-in-ios-11-3/64155509#64155509). + /// + /// Default is false. + /// + /// Note: If you pass in `true` make sure to add localhost and any [`registrable + /// domains`](https://developer.mozilla.org/en-US/docs/Glossary/Registrable_domain) + /// used in this webview to tauri-src/Info.ios.plist: + /// + /// ```xml + /// + /// + /// WKAppBoundDomains + /// + /// localhost + /// aregistrabledomain.example + /// + /// + /// + /// ``` + /// + /// You must add `localhost` if any webview with this set to true opens a + /// local webpage, makes any localhost calls, or uses the isolation pattern + /// because Tauri uses the `localhost` domain for hosting the application + /// webpage, the IPC protocol, and the isolation pattern's iframe. + /// + /// Requests served through custom uri schemes are allowed so long as they use + /// a registrable domain specified in the `WKAppBoundDomains` array for all the + /// requests from the app, including requests for the `localhost` domain. + /// + /// In theory, you can whitelist an entire uri scheme by including the + /// protocol name followed by a colon. For example, to allow all requests + /// using a custom "stream" uri scheme (see [this tauri + /// example](https://github.com/tauri-apps/tauri/blob/dev/examples/streaming/main.rs)), + /// you could add `stream:` to the AppBoundDomains array. That said, I'm not + /// sure whether Apple would let your app through app review if you do + /// whitelist an entire protocol because this feature is not mentioned in + /// [their blog post on App-Bound + /// Domains](https://webkit.org/blog/10882/app-bound-domains/). + /// + /// See https://webkit.org/blog/10882/app-bound-domains/ and + /// https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/limitsnavigationstoappbounddomains + /// for the official documentation on App-Bound Domains. + /// + /// ## Platform-specific + /// + /// - **iOS**: Supported since version 14.0+. + /// - **Linux / Windows / Android / MacOS:** Unsupported. + pub fn limit_navigations_to_app_bound_domains(mut self, limit_navigations: bool) -> Self { + self.webview_builder = self + .webview_builder + .limit_navigations_to_app_bound_domains(limit_navigations); + self + } + + /// Set the environment for the webview. + /// Useful if you need to share the same environment, for instance when using the [`Self::on_new_window`]. + #[cfg(all(feature = "wry", windows))] + pub fn with_environment( + mut self, + environment: webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Environment, + ) -> Self { + self.webview_builder = self.webview_builder.with_environment(environment); + self + } + + /// Creates a new webview sharing the same web process with the provided webview. + /// Useful if you need to link a webview to another, for instance when using the [`Self::on_new_window`]. + #[cfg(all( + feature = "wry", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ) + ))] + pub fn with_related_view(mut self, related_view: webkit2gtk::WebView) -> Self { + self.webview_builder = self.webview_builder.with_related_view(related_view); + self + } + + /// Set the webview configuration. + /// Useful if you need to share the same webview configuration, for instance when using the [`Self::on_new_window`]. + #[cfg(target_os = "macos")] + pub fn with_webview_configuration( + mut self, + webview_configuration: objc2::rc::Retained, + ) -> Self { + self.webview_builder = self + .webview_builder + .with_webview_configuration(webview_configuration); + self + } + + /// Set the window features. + /// Useful if you need to share the same window features, for instance when using the [`Self::on_new_window`]. + #[cfg(any( + target_os = "macos", + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn window_features(mut self, features: NewWindowFeatures) -> Self { + if let Some(position) = features.position() { + self.window_builder = self.window_builder.position(position.x, position.y); + } + + if let Some(size) = features.size() { + self.window_builder = self.window_builder.inner_size(size.width, size.height); + } + + #[cfg(target_os = "macos")] + { + self.webview_builder = self + .webview_builder + .with_webview_configuration(features.opener().target_configuration.clone()); + } + + #[cfg(all(feature = "wry", windows))] + { + self.webview_builder = self + .webview_builder + .with_environment(features.opener().environment.clone()); + } + + #[cfg(all( + feature = "wry", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + ) + ))] + { + self.webview_builder = self + .webview_builder + .with_related_view(features.opener().webview.clone()); + } + self + } +} + +// 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)] +pub struct WebviewWindow { + pub(crate) window: Window, + pub(crate) webview: Webview, +} + +impl AsRef> for WebviewWindow { + fn as_ref(&self) -> &Webview { + &self.webview + } +} + +impl Clone for WebviewWindow { + fn clone(&self) -> Self { + Self { + window: self.window.clone(), + webview: self.webview.clone(), + } + } +} + +impl Eq for WebviewWindow {} +impl PartialEq for WebviewWindow { + /// Only use the [`Webview`]'s label to compare equality. + fn eq(&self, other: &Self) -> bool { + self.webview.eq(&other.webview) + } +} + +impl raw_window_handle::HasWindowHandle for WebviewWindow { + fn window_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + Ok(unsafe { + raw_window_handle::WindowHandle::borrow_raw(self.window.window_handle()?.as_raw()) + }) + } +} + +impl raw_window_handle::HasDisplayHandle for WebviewWindow { + fn display_handle( + &self, + ) -> std::result::Result, raw_window_handle::HandleError> { + self.webview.app_handle.display_handle() + } +} + +impl<'de, R: Runtime> CommandArg<'de, R> for WebviewWindow { + /// Grabs the [`Window`] from the [`CommandItem`]. This will never fail. + fn from_command(command: CommandItem<'de, R>) -> Result { + let webview = command.message.webview(); + let window = webview.window(); + if window.is_webview_window() { + return Ok(Self { window, webview }); + } + + Err(InvokeError::from("current webview is not a WebviewWindow")) + } +} + +/// Base webview window functions. +impl WebviewWindow { + /// Initializes a [`WebviewWindowBuilder`] with the given window label and webview URL. + /// + /// Data URLs are only supported with the `webview-data-url` feature flag. + pub fn builder, L: Into>( + manager: &M, + label: L, + url: WebviewUrl, + ) -> WebviewWindowBuilder<'_, R, M> { + WebviewWindowBuilder::new(manager, label, url) + } + + /// Runs the given closure on the main thread. + pub fn run_on_main_thread(&self, f: F) -> crate::Result<()> { + self.webview.run_on_main_thread(f) + } + + /// The webview label. + pub fn label(&self) -> &str { + self.webview.label() + } + + /// Registers a window event listener. + pub fn on_window_event(&self, f: F) { + self.window.on_window_event(f); + } + + /// Registers a webview event listener. + pub fn on_webview_event(&self, f: F) { + self.webview.on_webview_event(f); + } + + /// Resolves the given command scope for this webview on the currently loaded URL. + /// + /// If the command is not allowed, returns None. + /// + /// If the scope cannot be deserialized to the given type, an error is returned. + /// + /// In a command context this can be directly resolved from the command arguments via [crate::ipc::CommandScope]: + /// + /// ``` + /// use tauri::ipc::CommandScope; + /// + /// #[derive(Debug, serde::Deserialize)] + /// struct ScopeType { + /// some_value: String, + /// } + /// #[tauri::command] + /// fn my_command(scope: CommandScope) { + /// // check scope + /// } + /// ``` + /// + /// # Examples + /// + /// ``` + /// use tauri::Manager; + /// + /// #[derive(Debug, serde::Deserialize)] + /// struct ScopeType { + /// some_value: String, + /// } + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview = app.get_webview_window("main").unwrap(); + /// let scope = webview.resolve_command_scope::("my-plugin", "read"); + /// Ok(()) + /// }); + /// ``` + pub fn resolve_command_scope( + &self, + plugin: &str, + command: &str, + ) -> crate::Result>> { + self.webview.resolve_command_scope(plugin, command) + } +} + +/// Menu APIs +#[cfg(desktop)] +impl WebviewWindow { + /// Registers a global menu event listener. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + /// + /// Also note that this handler will not be called if + /// the window used to register it was closed. + /// + /// # Examples + /// + /// ``` + /// use tauri::menu::{Menu, Submenu, MenuItem}; + /// use tauri::{WebviewWindowBuilder, WebviewUrl}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle(); + /// let save_menu_item = MenuItem::new(handle, "Save", true, None::<&str>)?; + /// let menu = Menu::with_items(handle, &[ + /// &Submenu::with_items(handle, "File", true, &[ + /// &save_menu_item, + /// ])?, + /// ])?; + /// let webview_window = WebviewWindowBuilder::new(app, "editor", WebviewUrl::default()) + /// .menu(menu) + /// .build() + /// .unwrap(); + /// + /// webview_window.on_menu_event(move |window, event| { + /// if event.id == save_menu_item.id() { + /// // save menu item + /// } + /// }); + /// + /// Ok(()) + /// }); + /// ``` + pub fn on_menu_event, crate::menu::MenuEvent) + Send + Sync + 'static>( + &self, + f: F, + ) { + self.window.on_menu_event(f) + } + + /// Returns this window menu. + pub fn menu(&self) -> Option> { + self.window.menu() + } + + /// Sets the window menu and returns the previous one. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one + /// window, if you need to set it, use [`AppHandle::set_menu`] instead. + #[cfg_attr(target_os = "macos", allow(unused_variables))] + pub fn set_menu(&self, menu: Menu) -> crate::Result>> { + self.window.set_menu(menu) + } + + /// Removes the window menu and returns it. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one + /// window, if you need to remove it, use [`AppHandle::remove_menu`] instead. + pub fn remove_menu(&self) -> crate::Result>> { + self.window.remove_menu() + } + + /// Hides the window menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + pub fn hide_menu(&self) -> crate::Result<()> { + self.window.hide_menu() + } + + /// Shows the window menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + pub fn show_menu(&self) -> crate::Result<()> { + self.window.show_menu() + } + + /// Shows the window menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + pub fn is_menu_visible(&self) -> crate::Result { + self.window.is_menu_visible() + } + + /// Shows the specified menu as a context menu at the cursor position. + pub fn popup_menu(&self, menu: &M) -> crate::Result<()> { + self.window.popup_menu(menu) + } + + /// Shows the specified menu as a context menu at the specified position. + /// + /// The position is relative to the window's top-left corner. + pub fn popup_menu_at>( + &self, + menu: &M, + position: P, + ) -> crate::Result<()> { + self.window.popup_menu_at(menu, position) + } +} + +/// Window getters. +impl WebviewWindow { + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa. + pub fn scale_factor(&self) -> crate::Result { + self.window.scale_factor() + } + + /// Returns the position of the top-left hand corner of the window's client area relative to the top-left hand corner of the desktop. + pub fn inner_position(&self) -> crate::Result> { + self.window.inner_position() + } + + /// Returns the position of the top-left hand corner of the window relative to the top-left hand corner of the desktop. + pub fn outer_position(&self) -> crate::Result> { + self.window.outer_position() + } + + /// Returns the physical size of the window's client area. + /// + /// The client area is the content of the window, excluding the title bar and borders. + pub fn inner_size(&self) -> crate::Result> { + self.window.inner_size() + } + + /// Returns the physical size of the entire window. + /// + /// These dimensions include the title bar and borders. If you don't want that (and you usually don't), use inner_size instead. + pub fn outer_size(&self) -> crate::Result> { + self.window.outer_size() + } + + /// Gets the window's current fullscreen state. + pub fn is_fullscreen(&self) -> crate::Result { + self.window.is_fullscreen() + } + + /// Gets the window's current minimized state. + pub fn is_minimized(&self) -> crate::Result { + self.window.is_minimized() + } + + /// Gets the window's current maximized state. + pub fn is_maximized(&self) -> crate::Result { + self.window.is_maximized() + } + + /// Gets the window's current focus state. + pub fn is_focused(&self) -> crate::Result { + self.window.is_focused() + } + + /// Gets the window's current decoration state. + pub fn is_decorated(&self) -> crate::Result { + self.window.is_decorated() + } + + /// Gets the window's current resizable state. + pub fn is_resizable(&self) -> crate::Result { + self.window.is_resizable() + } + + /// Whether the window is enabled or disabled. + pub fn is_enabled(&self) -> crate::Result { + self.webview.window().is_enabled() + } + + /// Determines if this window should always be on top of other windows. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + pub fn is_always_on_top(&self) -> crate::Result { + self.webview.window().is_always_on_top() + } + + /// Gets the window's native maximize button state + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn is_maximizable(&self) -> crate::Result { + self.window.is_maximizable() + } + + /// Gets the window's native minimize button state + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn is_minimizable(&self) -> crate::Result { + self.window.is_minimizable() + } + + /// Gets the window's native close button state + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn is_closable(&self) -> crate::Result { + self.window.is_closable() + } + + /// Gets the window's current visibility state. + pub fn is_visible(&self) -> crate::Result { + self.window.is_visible() + } + + /// Gets the window's current title. + pub fn title(&self) -> crate::Result { + self.window.title() + } + + /// Returns the monitor on which the window currently resides. + /// + /// Returns None if current monitor can't be detected. + pub fn current_monitor(&self) -> crate::Result> { + self.window.current_monitor() + } + + /// Returns the primary monitor of the system. + /// + /// Returns None if it can't identify any monitor as a primary one. + pub fn primary_monitor(&self) -> crate::Result> { + self.window.primary_monitor() + } + + /// Returns the monitor that contains the given point. + pub fn monitor_from_point(&self, x: f64, y: f64) -> crate::Result> { + self.window.monitor_from_point(x, y) + } + + /// Returns the list of all the monitors available on the system. + pub fn available_monitors(&self) -> crate::Result> { + self.window.available_monitors() + } + + /// Returns the native handle that is used by this window. + #[cfg(target_os = "macos")] + pub fn ns_window(&self) -> crate::Result<*mut std::ffi::c_void> { + self.window.ns_window() + } + + /// Returns the pointer to the content view of this window. + #[cfg(target_os = "macos")] + pub fn ns_view(&self) -> crate::Result<*mut std::ffi::c_void> { + self.window.ns_view() + } + + /// Returns the native handle that is used by this window. + #[cfg(windows)] + pub fn hwnd(&self) -> crate::Result { + self.window.hwnd() + } + + /// Returns the `ApplicationWindow` from gtk crate that is used by this window. + /// + /// Note that this type can only be used on the main thread. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn gtk_window(&self) -> crate::Result { + self.window.gtk_window() + } + + /// Returns the vertical [`gtk::Box`] that is added by default as the sole child of this window. + /// + /// Note that this type can only be used on the main thread. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn default_vbox(&self) -> crate::Result { + 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 + /// + /// - **macOS**: Only supported on macOS 10.14+. + pub fn theme(&self) -> crate::Result { + self.window.theme() + } +} + +/// Desktop window getters. +#[cfg(desktop)] +impl WebviewWindow { + /// Get the cursor position relative to the top-left hand corner of the desktop. + /// + /// Note that the top-left hand corner of the desktop is not necessarily the same as the screen. + /// If the user uses a desktop with multiple monitors, + /// the top-left hand corner of the desktop is the top-left hand corner of the main monitor on Windows and macOS + /// or the top-left of the leftmost monitor on X11. + /// + /// The coordinates can be negative if the top-left hand corner of the window is outside of the visible screen region. + pub fn cursor_position(&self) -> crate::Result> { + self.webview.cursor_position() + } +} + +/// Desktop window setters and actions. +#[cfg(desktop)] +impl WebviewWindow { + /// Centers the window. + pub fn center(&self) -> crate::Result<()> { + self.window.center() + } + + /// 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.request_user_attention(request_type) + } + + /// 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.set_maximizable(maximizable) + } + + /// 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.set_minimizable(minimizable) + } + + /// 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.set_closable(closable) + } + + /// Maximizes this window. + pub fn maximize(&self) -> crate::Result<()> { + self.window.maximize() + } + + /// Un-maximizes this window. + pub fn unmaximize(&self) -> crate::Result<()> { + self.window.unmaximize() + } + + /// Minimizes this window. + pub fn minimize(&self) -> crate::Result<()> { + self.window.minimize() + } + + /// Un-minimizes this window. + pub fn unminimize(&self) -> crate::Result<()> { + self.window.unminimize() + } + + /// Determines if this window should be [decorated]. + /// + /// [decorated]: https://en.wikipedia.org/wiki/Window_(computing)#Window_decoration + pub fn set_decorations(&self, decorations: bool) -> crate::Result<()> { + self.window.set_decorations(decorations) + } + + /// Determines if this window should have shadow. + /// + /// ## Platform-specific + /// + /// - **Windows:** + /// - `false` has no effect on decorated window, shadow are always ON. + /// - `true` will make undecorated window have a 1px white border, + /// and on Windows 11, it will have a rounded corners. + /// - **Linux:** Unsupported. + pub fn set_shadow(&self, enable: bool) -> crate::Result<()> { + self.window.set_shadow(enable) + } + + /// Sets window effects, pass [`None`] to clear any effects applied if possible. + /// + /// Requires the window to be transparent. + /// + /// See [`crate::window::EffectsBuilder`] for a convenient builder for [`crate::utils::config::WindowEffectsConfig`]. + /// + /// + /// ```rust,no_run + /// use tauri::{Manager, window::{Color, Effect, EffectState, EffectsBuilder}}; + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview_window = app.get_webview_window("main").unwrap(); + /// webview_window.set_effects( + /// EffectsBuilder::new() + /// .effect(Effect::Popover) + /// .state(EffectState::Active) + /// .radius(5.) + /// .color(Color(0, 0, 0, 255)) + /// .build(), + /// )?; + /// Ok(()) + /// }); + /// ``` + /// + /// ## Platform-specific: + /// + /// - **Windows**: If using decorations or shadows, you may want to try this workaround + /// - **Linux**: Unsupported + pub fn set_effects>>( + &self, + effects: E, + ) -> crate::Result<()> { + self.window.set_effects(effects) + } + + /// Determines if this window should always be below other windows. + pub fn set_always_on_bottom(&self, always_on_bottom: bool) -> crate::Result<()> { + self.window.set_always_on_bottom(always_on_bottom) + } + + /// Determines if this window should always be on top of other windows. + pub fn set_always_on_top(&self, always_on_top: bool) -> crate::Result<()> { + self.window.set_always_on_top(always_on_top) + } + + /// Sets whether the window should be visible on all workspaces or virtual desktops. + pub fn set_visible_on_all_workspaces( + &self, + visible_on_all_workspaces: bool, + ) -> crate::Result<()> { + self + .window + .set_visible_on_all_workspaces(visible_on_all_workspaces) + } + + /// Determines if this window should be fullscreen. + pub fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()> { + self.window.set_fullscreen(fullscreen) + } + + /// Sets the window as fullscreen on the monitor that contains the given physical position, + /// such as a [`Monitor::position`](crate::Monitor::position). + /// + /// Does nothing if no monitor contains the position. + pub fn set_fullscreen_on_monitor(&self, position: PhysicalPosition) -> crate::Result<()> { + self.window.set_fullscreen_on_monitor(position) + } + + /// Toggles a fullscreen mode that doesn't require a new macOS space. + /// Returns a boolean indicating whether the transition was successful (this won't work if the window was already in the native fullscreen). + /// + /// This is how fullscreen used to work on macOS in versions before Lion. + /// And allows the user to have a fullscreen window without using another space or taking control over the entire monitor. + /// + /// ## Platform-specific + /// + /// - **macOS:** Uses native simple fullscreen mode. + /// - **Other platforms:** Falls back to [`Self::set_fullscreen`]. + pub fn set_simple_fullscreen(&self, enable: bool) -> crate::Result<()> { + self.window.set_simple_fullscreen(enable) + } + + /// Sets this window' icon. + pub fn set_icon(&self, icon: Image<'_>) -> crate::Result<()> { + self.window.set_icon(icon) + } + + /// Whether to hide the window icon from the taskbar or not. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn set_skip_taskbar(&self, skip: bool) -> crate::Result<()> { + self.window.set_skip_taskbar(skip) + } + + /// Grabs the cursor, preventing it from leaving the window. + /// + /// There's no guarantee that the cursor will be hidden. You should + /// hide it by yourself if you want so. + /// + /// ## Platform-specific + /// + /// - **Linux:** Unsupported. + /// - **macOS:** This locks the cursor in a fixed location, which looks visually awkward. + pub fn set_cursor_grab(&self, grab: bool) -> crate::Result<()> { + self.window.set_cursor_grab(grab) + } + + /// Modifies the cursor's visibility. + /// + /// If `false`, this will hide the cursor. If `true`, this will show the cursor. + /// + /// ## Platform-specific + /// + /// - **Windows:** The cursor is only hidden within the confines of the window. + /// - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor is + /// outside of the window. + pub fn set_cursor_visible(&self, visible: bool) -> crate::Result<()> { + self.window.set_cursor_visible(visible) + } + + /// Modifies the cursor icon of the window. + pub fn set_cursor_icon(&self, icon: CursorIcon) -> crate::Result<()> { + self.window.set_cursor_icon(icon) + } + + /// Changes the position of the cursor in window coordinates. + pub fn set_cursor_position>(&self, position: Pos) -> crate::Result<()> { + self.window.set_cursor_position(position) + } + + /// Ignores the window cursor events. + pub fn set_ignore_cursor_events(&self, ignore: bool) -> crate::Result<()> { + self.window.set_ignore_cursor_events(ignore) + } + + /// Starts dragging the window. + pub fn start_dragging(&self) -> crate::Result<()> { + self.window.start_dragging() + } + + /// Sets the overlay icon on the taskbar **Windows only**. Using `None` will remove the icon + /// + /// The overlay icon can be unique for each window. + #[cfg(target_os = "windows")] + #[cfg_attr(docsrs, doc(cfg(target_os = "windows")))] + pub fn set_overlay_icon(&self, icon: Option>) -> crate::Result<()> { + self.window.set_overlay_icon(icon) + } + + /// Sets the taskbar badge count. Using `0` or `None` will remove the badge + /// + /// ## Platform-specific + /// - **Windows:** Unsupported, use [`WebviewWindow::set_overlay_icon`] instead. + /// - **iOS:** iOS expects i32, the value will be clamped to i32::MIN, i32::MAX. + /// - **Android:** Unsupported. + pub fn set_badge_count(&self, count: Option) -> crate::Result<()> { + self.window.set_badge_count(count) + } + + /// Sets the taskbar badge label **macOS only**. Using `None` will remove the badge + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub fn set_badge_label(&self, label: Option) -> crate::Result<()> { + self.window.set_badge_label(label) + } + + /// Sets the taskbar progress state. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS**: Progress bar is app-wide and not specific to this window. + /// - **Linux**: Only supported desktop environments with `libunity` (e.g. GNOME). + /// - **iOS / Android:** Unsupported. + pub fn set_progress_bar( + &self, + progress_state: crate::window::ProgressBarState, + ) -> crate::Result<()> { + self.window.set_progress_bar(progress_state) + } + + /// Sets the title bar style. **macOS only**. + pub fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> crate::Result<()> { + self.window.set_title_bar_style(style) + } +} + +/// 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. + /// + /// ## 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.set_theme(theme) + } +} + +/// Desktop webview APIs. +#[cfg(desktop)] +impl WebviewWindow { + /// Opens the dialog to prints the contents of the webview. + /// Currently only supported on macOS on `wry`. + /// `window.print()` works on all platforms. + pub fn print(&self) -> crate::Result<()> { + self.webview.print() + } +} + +/// Webview APIs. +impl WebviewWindow { + /// Executes a closure, providing it with the webview handle that is specific to the current platform. + /// + /// The closure is executed on the main thread. + /// + /// Note that `webview2-com`, `webkit2gtk`, `objc2_web_kit` and similar crates may be updated in minor releases of Tauri. + /// Therefore it's recommended to pin Tauri to at least a minor version when you're using `with_webview`. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// + /// fn main() { + /// tauri::Builder::default() + /// .setup(|app| { + /// let main_webview = app.get_webview_window("main").unwrap(); + /// main_webview.with_webview(|webview| { + /// #[cfg(target_os = "linux")] + /// { + /// // see + /// // and + /// use webkit2gtk::WebViewExt; + /// webview.inner().set_zoom_level(4.); + /// } + /// + /// #[cfg(windows)] + /// unsafe { + /// // see + /// webview.controller().SetZoomFactor(4.).unwrap(); + /// } + /// + /// #[cfg(target_os = "macos")] + /// unsafe { + /// let view: &objc2_web_kit::WKWebView = &*webview.inner().cast(); + /// let controller: &objc2_web_kit::WKUserContentController = &*webview.controller().cast(); + /// let window: &objc2_app_kit::NSWindow = &*webview.ns_window().cast(); + /// + /// view.setPageZoom(4.); + /// controller.removeAllUserScripts(); + /// let bg_color = objc2_app_kit::NSColor::colorWithDeviceRed_green_blue_alpha(0.5, 0.2, 0.4, 1.); + /// window.setBackgroundColor(Some(&bg_color)); + /// } + /// + /// #[cfg(target_os = "android")] + /// { + /// use jni::objects::JValue; + /// webview.jni_handle().exec(|env, _, webview| { + /// env.call_method(webview, "zoomBy", "(F)V", &[JValue::Float(4.)]).unwrap(); + /// }) + /// } + /// }); + /// Ok(()) + /// }); + /// } + /// ``` + #[allow(clippy::needless_doctest_main)] // To avoid a large diff + #[cfg(feature = "wry")] + #[cfg_attr(docsrs, doc(cfg(feature = "wry")))] + pub fn with_webview( + &self, + f: F, + ) -> crate::Result<()> { + self.webview.with_webview(f) + } + + /// Returns the current url of the webview. + pub fn url(&self) -> crate::Result { + self.webview.url() + } + + /// Navigates the webview to the defined url. + pub fn navigate(&self, url: Url) -> crate::Result<()> { + self.webview.navigate(url) + } + + /// Reloads the current page. + pub fn reload(&self) -> crate::Result<()> { + self.webview.reload() + } + + /// Converts a file path to a URL that can be loaded by this webview. + /// + /// This is the Rust equivalent of the JavaScript `convertFileSrc` function. + /// + /// The `protocol-asset` Cargo feature must be enabled and the file must be included in the + /// [`app.security.assetProtocol`](https://v2.tauri.app/reference/config/#assetprotocolconfig) + /// scope. The protocol origin must also be allowed by the relevant + /// [`app.security.csp`](https://v2.tauri.app/reference/config/#csp-1) directive, + /// e.g. `img-src 'self' asset: http://asset.localhost`. + /// + /// On Windows and Android the URL is `http://{protocol}.localhost/{path}` + /// (or `https://` if the webview was built with [`WebviewWindowBuilder::use_https_scheme`]); + /// on macOS, Linux and iOS it is `{protocol}://localhost/{path}`. + /// + /// # Arguments + /// + /// * `path` - The file path to convert. + /// * `protocol` - The custom protocol to use. Defaults to `asset`; you only need to set this + /// when using a protocol registered with [`Builder::register_uri_scheme_protocol`](crate::Builder::register_uri_scheme_protocol). + /// + /// # Errors + /// + /// Returns [`Error::NonUtf8Path`](crate::Error::NonUtf8Path) if the path is not valid UTF-8, + /// since the asset protocol could not resolve such a URL back to the file. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview = app.get_webview_window("main").unwrap(); + /// let video_path = app.path().app_data_dir()?.join("video.mp4"); + /// let url = webview.convert_file_src(&video_path, None)?; + /// webview.eval(format!("document.querySelector('video').src = '{url}'"))?; + /// Ok(()) + /// }); + /// ``` + pub fn convert_file_src>( + &self, + path: P, + protocol: Option<&str>, + ) -> crate::Result { + self.webview.convert_file_src(path, protocol) + } + + /// Handles this window receiving an [`crate::webview::InvokeRequest`]. + pub fn on_message( + self, + request: crate::webview::InvokeRequest, + responder: Box>, + ) { + self.webview.on_message(request, responder) + } + + /// Evaluates JavaScript on this window. + pub fn eval(&self, js: impl Into) -> crate::Result<()> { + 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. + /// + /// ## Platform-specific + /// + /// - **macOS:** Only supported on macOS 10.15+. + /// This is a private API on macOS, so you cannot use this if your application will be published on the App Store. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// #[cfg(debug_assertions)] + /// app.get_webview_window("main").unwrap().open_devtools(); + /// Ok(()) + /// }); + /// ``` + #[cfg(any(debug_assertions, feature = "devtools"))] + #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))] + pub fn open_devtools(&self) { + self.webview.open_devtools(); + } + + /// Closes the developer tools window (Web Inspector). + /// The devtools is only enabled on debug builds or with the `devtools` feature flag. + /// + /// ## Platform-specific + /// + /// - **macOS:** Only supported on macOS 10.15+. + /// This is a private API on macOS, so you cannot use this if your application will be published on the App Store. + /// - **Windows:** Unsupported. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// #[cfg(debug_assertions)] + /// { + /// let webview = app.get_webview_window("main").unwrap(); + /// webview.open_devtools(); + /// std::thread::spawn(move || { + /// std::thread::sleep(std::time::Duration::from_secs(10)); + /// webview.close_devtools(); + /// }); + /// } + /// Ok(()) + /// }); + /// ``` + #[cfg(any(debug_assertions, feature = "devtools"))] + #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))] + pub fn close_devtools(&self) { + self.webview.close_devtools(); + } + + /// Checks if the developer tools window (Web Inspector) is opened. + /// The devtools is only enabled on debug builds or with the `devtools` feature flag. + /// + /// ## Platform-specific + /// + /// - **macOS:** Only supported on macOS 10.15+. + /// This is a private API on macOS, so you cannot use this if your application will be published on the App Store. + /// - **Windows:** Unsupported. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// #[cfg(debug_assertions)] + /// { + /// let webview = app.get_webview_window("main").unwrap(); + /// if !webview.is_devtools_open() { + /// webview.open_devtools(); + /// } + /// } + /// Ok(()) + /// }); + /// ``` + #[cfg(any(debug_assertions, feature = "devtools"))] + #[cfg_attr(docsrs, doc(cfg(any(debug_assertions, feature = "devtools"))))] + pub fn is_devtools_open(&self) -> bool { + self.webview.is_devtools_open() + } + + /// Set the webview zoom level + /// + /// ## Platform-specific: + /// + /// - **Android**: Not supported. + /// - **macOS**: available on macOS 11+ only. + /// - **iOS**: available on iOS 14+ only. + pub fn set_zoom(&self, scale_factor: f64) -> crate::Result<()> { + self.webview.set_zoom(scale_factor) + } + + /// Clear all browsing data for this webview window. + pub fn clear_all_browsing_data(&self) -> crate::Result<()> { + self.webview.clear_all_browsing_data() + } + + /// Returns all cookies in the runtime's cookie store including HTTP-only and secure cookies. + /// + /// Note that cookies will only be returned for URLs with an http or https scheme. + /// Cookies set through javascript for local files + /// (such as those served from the tauri://) protocol are not currently supported. + /// + /// # Stability + /// + /// See [Self::cookies]. + /// + /// # Known issues + /// + /// See [Self::cookies]. + pub fn cookies_for_url(&self, url: Url) -> crate::Result>> { + self.webview.cookies_for_url(url) + } + + /// Returns all cookies in the runtime's cookie store for all URLs including HTTP-only and secure cookies. + /// + /// Note that cookies will only be returned for URLs with an http or https scheme. + /// Cookies set through javascript for local files + /// (such as those served from the tauri://) protocol are not currently supported. + /// + /// # Stability + /// + /// The return value of this function leverages [`tauri_runtime::Cookie`] which re-exports the cookie crate. + /// This dependency might receive updates in minor Tauri releases. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when reading cookies. + /// + /// ## Platform-specific + /// + /// - **Android**: Unsupported, always returns an empty [`Vec`]. + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn cookies(&self) -> crate::Result>> { + self.webview.cookies() + } + + /// Set a cookie for the webview. + /// + /// # Stability + /// + /// See [Self::cookies]. + pub fn set_cookie(&self, cookie: Cookie<'_>) -> crate::Result<()> { + self.webview.set_cookie(cookie) + } + + /// Delete a cookie for the webview. + /// + /// # Stability + /// + /// See [Self::cookies]. + pub fn delete_cookie(&self, cookie: Cookie<'_>) -> crate::Result<()> { + self.webview.delete_cookie(cookie) + } +} + +impl Listener for WebviewWindow { + /// Listen to an event on this webview window. + /// + /// # Examples + /// + /// ``` + /// use tauri::{Manager, Listener}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview_window = app.get_webview_window("main").unwrap(); + /// webview_window.listen("component-loaded", move |event| { + /// println!("window just loaded a component"); + /// }); + /// + /// Ok(()) + /// }); + /// ``` + fn listen(&self, event: impl Into, handler: F) -> EventId + where + F: Fn(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager().listen( + event, + EventTarget::WebviewWindow { + label: self.label().to_string(), + }, + handler, + ) + } + + /// Listen to an event on this window webview only once. + /// + /// See [`Self::listen`] for more information. + fn once(&self, event: impl Into, handler: F) -> EventId + where + F: FnOnce(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager().once( + event, + EventTarget::WebviewWindow { + label: self.label().to_string(), + }, + handler, + ) + } + + /// Unlisten to an event on this webview window. + /// + /// # Examples + /// ``` + /// use tauri::{Manager, Listener}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let webview_window = app.get_webview_window("main").unwrap(); + /// let webview_window_ = webview_window.clone(); + /// let handler = webview_window.listen("component-loaded", move |event| { + /// println!("webview_window just loaded a component"); + /// + /// // we no longer need to listen to the event + /// // we also could have used `webview_window.once` instead + /// webview_window_.unlisten(event.id()); + /// }); + /// + /// // stop listening to the event when you do not need it anymore + /// webview_window.unlisten(handler); + /// + /// Ok(()) + /// }); + /// ``` + fn unlisten(&self, id: EventId) { + self.manager().unlisten(id) + } +} + +impl Emitter for WebviewWindow {} + +impl Manager for WebviewWindow { + fn resources_table(&self) -> MutexGuard<'_, ResourceTable> { + self + .webview + .resources_table + .lock() + .expect("poisoned window resources table") + } +} + +impl ManagerBase for WebviewWindow { + fn manager(&self) -> &AppManager { + self.webview.manager() + } + + fn manager_owned(&self) -> Arc> { + self.webview.manager_owned() + } + + fn runtime(&self) -> RuntimeOrDispatch<'_, R> { + 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 new file mode 100644 index 0000000..27b1451 --- /dev/null +++ b/crates/tauri/src/window/mod.rs @@ -0,0 +1,2625 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The Tauri window types and functions. + +pub(crate) mod plugin; + +use tauri_runtime::{ + dpi::{PhysicalPosition, PhysicalRect, PhysicalSize}, + webview::PendingWebview, +}; +pub use tauri_utils::{WindowEffect as Effect, WindowEffectState as EffectState, config::Color}; + +#[cfg(desktop)] +pub use crate::runtime::ProgressBarStatus; + +#[cfg(desktop)] +use crate::{ + CursorIcon, + image::Image, + menu::{ContextMenu, Menu, MenuId}, + runtime::UserAttentionType, +}; +use crate::{ + Emitter, EventLoopMessage, EventName, Listener, Manager, ResourceTable, Runtime, Theme, Webview, + WindowEvent, + app::AppHandle, + event::{Event, EventId, EventTarget}, + ipc::{CommandArg, CommandItem, InvokeError}, + manager::{AppManager, EmitPayload}, + runtime::{ + RuntimeHandle, WindowDispatch, + dpi::{Position, Size}, + monitor::Monitor as RuntimeMonitor, + window::{DetachedWindow, PendingWindow, WindowBuilder as _}, + }, + sealed::{ManagerBase, RuntimeOrDispatch}, + utils::config::{WindowConfig, WindowEffectsConfig}, + webview::WebviewBuilder, +}; + +use serde::Serialize; +#[cfg(windows)] +use windows::Win32::Foundation::HWND; + +use tauri_macros::default_runtime; + +use std::{ + fmt, + hash::{Hash, Hasher}, + sync::{Arc, Mutex, MutexGuard}, +}; + +/// Monitor descriptor. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Monitor { + pub(crate) name: Option, + pub(crate) size: PhysicalSize, + pub(crate) position: PhysicalPosition, + pub(crate) work_area: PhysicalRect, + pub(crate) scale_factor: f64, +} + +impl From for Monitor { + fn from(monitor: RuntimeMonitor) -> Self { + Self { + name: monitor.name, + size: monitor.size, + position: monitor.position, + work_area: monitor.work_area, + scale_factor: monitor.scale_factor, + } + } +} + +impl Monitor { + /// Returns a human-readable name of the monitor. + /// Returns None if the monitor doesn't exist anymore. + pub fn name(&self) -> Option<&String> { + self.name.as_ref() + } + + /// Returns the monitor's resolution. + pub fn size(&self) -> &PhysicalSize { + &self.size + } + + /// Returns the top-left corner position of the monitor relative to the larger full screen area. + pub fn position(&self) -> &PhysicalPosition { + &self.position + } + + /// Returns the monitor's work_area. + pub fn work_area(&self) -> &PhysicalRect { + &self.work_area + } + + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa. + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } +} + +macro_rules! unstable_struct { + (#[doc = $doc:expr] $($tokens:tt)*) => { + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + #[doc = $doc] + pub $($tokens)* + + #[cfg(not(feature = "unstable"))] + pub(crate) $($tokens)* + } +} + +unstable_struct!( + #[doc = "A builder for a window managed by Tauri."] + struct WindowBuilder<'a, R: Runtime, M: Manager> { + manager: &'a M, + pub(crate) label: String, + pub(crate) window_builder: + >::WindowBuilder, + #[cfg(desktop)] + pub(crate) menu: Option>, + #[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, + } +); + +impl> fmt::Debug for WindowBuilder<'_, R, M> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WindowBuilder") + .field("label", &self.label) + .field("window_builder", &self.window_builder) + .finish() + } +} + +#[cfg_attr(not(feature = "unstable"), allow(dead_code))] +impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { + /// Initializes a window builder with the given window label. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when creating windows. + /// + /// # Examples + /// + /// - Create a window in the setup hook: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +tauri::Builder::default() + .setup(|app| { + let window = tauri::window::WindowBuilder::new(app, "label") + .build()?; + Ok(()) + }); +``` + "#### + )] + /// - Create a window in a separate thread: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +tauri::Builder::default() + .setup(|app| { + let handle = app.handle().clone(); + std::thread::spawn(move || { + let window = tauri::window::WindowBuilder::new(&handle, "label") + .build() + .unwrap(); + }); + Ok(()) + }); +``` + "#### + )] + /// + /// - Create a window in a command: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +#[tauri::command] +async fn create_window(app: tauri::AppHandle) { + let window = tauri::window::WindowBuilder::new(&app, "label") + .build() + .unwrap(); +} +``` + "#### + )] + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + pub fn new>(manager: &'a M, label: L) -> Self { + Self { + manager, + label: label.into(), + window_builder: >::WindowBuilder::new( + ), + #[cfg(desktop)] + menu: None, + #[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, + } + } + + /// Initializes a window builder from a [`WindowConfig`] from tauri.conf.json. + /// Keep in mind that you can't create 2 windows with the same `label` so make sure + /// that the initial window was closed or change the label of the new [`WindowBuilder`]. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, see [the Webview2 issue]. + /// You should use `async` commands and separate threads when creating windows. + /// + /// # Examples + /// + /// - Create a window in a command: + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +#[tauri::command] +async fn reopen_window(app: tauri::AppHandle) { + let window = tauri::window::WindowBuilder::from_config(&app, &app.config().app.windows.get(0).unwrap().clone()) + .unwrap() + .build() + .unwrap(); +} +``` + "#### + )] + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + 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(), + window_builder: + >::WindowBuilder::with_config( + config, + ), + #[cfg(desktop)] + menu: None, + #[cfg(desktop)] + on_menu_event: None, + }; + + #[cfg(desktop)] + if let Some(parent) = &config.parent { + let window = manager + .manager() + .get_window(parent) + .ok_or(crate::Error::WindowNotFound)?; + builder = builder.parent(&window)?; + } + + Ok(builder) + } + + /// Registers a global menu event listener. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + /// + /// Also note that this handler will not be called if + /// the window used to register it was closed. + /// + /// # Examples + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::menu::{Menu, Submenu, MenuItem}; +tauri::Builder::default() + .setup(|app| { + let handle = app.handle(); + let save_menu_item = MenuItem::new(handle, "Save", true, None::<&str>)?; + let menu = Menu::with_items(handle, &[ + &Submenu::with_items(handle, "File", true, &[ + &save_menu_item, + ])?, + ])?; + let window = tauri::window::WindowBuilder::new(app, "editor") + .menu(menu) + .on_menu_event(move |window, event| { + if event.id == save_menu_item.id() { + // save menu item + } + }) + .build() + .unwrap(); + /// + Ok(()) + }); +```"#### + )] + #[cfg(desktop)] + pub fn on_menu_event, crate::menu::MenuEvent) + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.on_menu_event.replace(Box::new(f)); + self + } + + /// Creates this window with a webview with it. + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "webview::create", skip_all) + )] + pub(crate) fn with_webview( + self, + webview: WebviewBuilder, + ) -> crate::Result<(Window, Webview)> { + let pending_webview = webview.into_pending_webview(self.manager, &self.label)?; + let window = self.build_internal(Some(pending_webview))?; + + let webview = window.webviews().first().unwrap().clone(); + + Ok((window, webview)) + } + + /// Creates a new window. + pub fn build(self) -> crate::Result> { + self.build_internal(None) + } + + /// Creates a new window with an optional webview. + fn build_internal( + // mutable on mobile + #[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); + } + + let app_manager = self.manager.manager(); + + let pending = app_manager.window.prepare_window(pending)?; + + #[cfg(desktop)] + let window_menu = { + let is_app_wide = self.menu.is_none(); + self + .menu + .or_else(|| self.manager.app_handle().menu()) + .map(|menu| WindowMenu { is_app_wide, menu }) + }; + + #[cfg(desktop)] + let handler = app_manager + .menu + .prepare_window_menu_creation_handler(window_menu.as_ref(), theme); + #[cfg(not(desktop))] + #[allow(clippy::type_complexity)] + let handler: Option) + Send>> = None; + + let window = match &mut self.manager.runtime() { + RuntimeOrDispatch::Runtime(runtime) => runtime.create_window(pending, handler), + RuntimeOrDispatch::RuntimeHandle(handle) => handle.create_window(pending, handler), + RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_window(pending, handler), + } + .map(|detached_window| { + let window = app_manager.window.attach_window( + self.manager.app_handle().clone(), + detached_window.clone(), + #[cfg(desktop)] + window_menu, + ); + + if let Some(webview) = detached_window.webview { + app_manager.webview.attach_webview( + window.clone(), + webview.webview, + webview.use_https_scheme, + ); + } + + window + })?; + + #[cfg(desktop)] + if let Some(handler) = self.on_menu_event { + window.on_menu_event(handler); + } + + let app_manager = self.manager.manager_owned(); + let window_label = window.label().to_string(); + let window_ = window.clone(); + // run on the main thread to fix a deadlock on webview.eval if the tracing feature is enabled + let _ = window.run_on_main_thread(move || { + if let Some(effects) = self.window_effects { + _ = crate::vibrancy::set_window_effects(&window_, Some(effects)); + } + let event = crate::EventName::from_str("tauri://window-created"); + let payload = Some(crate::webview::CreatedEvent { + label: window_label, + }); + let _ = app_manager.emit(event, EmitPayload::Serialize(&payload)); + }); + + Ok(window) + } +} + +/// Desktop APIs +#[cfg(desktop)] +#[cfg_attr(not(feature = "unstable"), allow(dead_code))] +impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { + /// Sets the menu for the window. + #[must_use] + pub fn menu(mut self, menu: Menu) -> Self { + self.menu.replace(menu); + self + } + + /// Show window in the center of the screen. + #[must_use] + pub fn center(mut self) -> Self { + self.window_builder = self.window_builder.center(); + 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` + /// + /// **NOTE**: The overflow check is only performed on window creation, resizes can still overflow + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + pub fn prevent_overflow(mut self) -> Self { + self.window_builder = self.window_builder.prevent_overflow(); + self + } + + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) + /// on creation with a margin, which means the window size will be limited to `monitor size - taskbar size - margin size` + /// + /// **NOTE**: The overflow check is only performed on window creation, resizes can still overflow + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + pub fn prevent_overflow_with_margin(mut self, margin: impl Into) -> Self { + self.window_builder = self + .window_builder + .prevent_overflow_with_margin(margin.into()); + self + } + + /// Whether the window's native maximize button is enabled or not. + /// 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. + #[must_use] + pub fn maximizable(mut self, maximizable: bool) -> Self { + self.window_builder = self.window_builder.maximizable(maximizable); + self + } + + /// Whether the window's native minimize button is enabled or not. + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + #[must_use] + pub fn minimizable(mut self, minimizable: bool) -> Self { + self.window_builder = self.window_builder.minimizable(minimizable); + self + } + + /// Whether the window's native close button is enabled or not. + /// + /// ## 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. + #[must_use] + pub fn closable(mut self, closable: bool) -> Self { + self.window_builder = self.window_builder.closable(closable); + self + } + + /// Whether to start the window in fullscreen or not. + #[must_use] + pub fn fullscreen(mut self, fullscreen: bool) -> Self { + self.window_builder = self.window_builder.fullscreen(fullscreen); + self + } + + /// Whether the window should be maximized upon creation. + #[must_use] + pub fn maximized(mut self, maximized: bool) -> Self { + self.window_builder = self.window_builder.maximized(maximized); + self + } + + /// Whether the window should have borders and bars. + #[must_use] + pub fn decorations(mut self, decorations: bool) -> Self { + self.window_builder = self.window_builder.decorations(decorations); + self + } + + /// Whether the window should always be below other windows. + #[must_use] + pub fn always_on_bottom(mut self, always_on_bottom: bool) -> Self { + self.window_builder = self.window_builder.always_on_bottom(always_on_bottom); + self + } + + /// Whether the window should always be on top of other windows. + #[must_use] + pub fn always_on_top(mut self, always_on_top: bool) -> Self { + self.window_builder = self.window_builder.always_on_top(always_on_top); + self + } + + /// Whether the window will be visible on all workspaces or virtual desktops. + /// + /// ## Platform-specific + /// + /// - **Windows / iOS / Android:** Unsupported. + #[must_use] + pub fn visible_on_all_workspaces(mut self, visible_on_all_workspaces: bool) -> Self { + self.window_builder = self + .window_builder + .visible_on_all_workspaces(visible_on_all_workspaces); + self + } + + /// Sets the window icon. + pub fn icon(mut self, icon: Image<'a>) -> crate::Result { + self.window_builder = self.window_builder.icon(icon.into())?; + Ok(self) + } + + /// Sets whether or not the window icon should be hidden from the taskbar. + /// + /// ## Platform-specific + /// + /// - **macOS**: Unsupported. + #[must_use] + pub fn skip_taskbar(mut self, skip: bool) -> Self { + self.window_builder = self.window_builder.skip_taskbar(skip); + self + } + + /// Sets custom name for Windows' window class. **Windows only**. + #[must_use] + pub fn window_classname>(mut self, classname: S) -> Self { + self.window_builder = self.window_builder.window_classname(classname); + self + } + + /// This sets `WS_EX_NOREDIRECTIONBITMAP`. + /// + /// This can avoid the white flash that may appear before the webview content is rendered + /// when using a transparent window. **Windows only**. + #[must_use] + pub fn no_redirection_bitmap(mut self, enable: bool) -> Self { + self.window_builder = self.window_builder.no_redirection_bitmap(enable); + self + } + + /// Sets whether or not the window has shadow. + /// + /// ## Platform-specific + /// + /// - **Windows:** + /// - `false` has no effect on decorated window, shadows are always ON. + /// - `true` will make undecorated window have a 1px white border, + /// and on Windows 11, it will have a rounded corners. + /// - **Linux:** Unsupported. + #[must_use] + pub fn shadow(mut self, enable: bool) -> Self { + self.window_builder = self.window_builder.shadow(enable); + self + } + + /// Sets a parent to the window to be created. + /// + /// ## Platform-specific + /// + /// - **Windows**: This sets the passed parent as an owner window to the window to be created. + /// From [MSDN owned windows docs](https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#owned-windows): + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// - **Linux**: This makes the new window transient for parent, see + /// - **macOS**: This adds the window as a child of parent, see + pub fn parent(mut self, parent: &Window) -> crate::Result { + #[cfg(windows)] + { + self.window_builder = self.window_builder.owner(parent.hwnd()?); + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + self.window_builder = self.window_builder.transient_for(&parent.gtk_window()?); + } + + #[cfg(target_os = "macos")] + { + self.window_builder = self.window_builder.parent(parent.ns_window()?); + } + + Ok(self) + } + + /// Set an owner to the window to be created. + /// + /// From MSDN: + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// + /// For more information, see + #[cfg(windows)] + pub fn owner(mut self, owner: &Window) -> crate::Result { + self.window_builder = self.window_builder.owner(owner.hwnd()?); + Ok(self) + } + + /// Set an owner to the window to be created. + /// + /// From MSDN: + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// + /// For more information, see + /// + /// **Note:** This is a low level API. See [`Self::parent`] for a higher level wrapper for Tauri windows. + #[cfg(windows)] + #[must_use] + pub fn owner_raw(mut self, owner: HWND) -> Self { + self.window_builder = self.window_builder.owner(owner); + self + } + + /// Sets a parent to the window to be created. + /// + /// A child window has the WS_CHILD style and is confined to the client area of its parent window. + /// + /// For more information, see + /// + /// **Note:** This is a low level API. See [`Self::parent`] for a higher level wrapper for Tauri windows. + #[cfg(windows)] + #[must_use] + pub fn parent_raw(mut self, parent: HWND) -> Self { + self.window_builder = self.window_builder.parent(parent); + self + } + + /// Sets a parent to the window to be created. + /// + /// See + /// + /// **Note:** This is a low level API. See [`Self::parent`] for a higher level wrapper for Tauri windows. + #[cfg(target_os = "macos")] + #[must_use] + pub fn parent_raw(mut self, parent: *mut std::ffi::c_void) -> Self { + self.window_builder = self.window_builder.parent(parent); + self + } + + /// Sets the window to be created transient for parent. + /// + /// See + /// + /// **Note:** This is a low level API. See [`Self::parent`] for a higher level wrapper for Tauri windows. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn transient_for(mut self, parent: &Window) -> crate::Result { + self.window_builder = self.window_builder.transient_for(&parent.gtk_window()?); + Ok(self) + } + + /// Sets the window to be created transient for parent. + /// + /// See + /// + /// **Note:** This is a low level API. See [`Self::parent`] and [`Self::transient_for`] for higher level wrappers for Tauri windows. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + #[must_use] + pub fn transient_for_raw(mut self, parent: &impl gtk::glib::IsA) -> Self { + self.window_builder = self.window_builder.transient_for(parent); + self + } + + /// Enables or disables drag and drop support. + #[cfg(windows)] + #[must_use] + pub fn drag_and_drop(mut self, enabled: bool) -> Self { + self.window_builder = self.window_builder.drag_and_drop(enabled); + self + } + + /// Sets the [`crate::TitleBarStyle`]. + #[cfg(target_os = "macos")] + #[must_use] + pub fn title_bar_style(mut self, style: crate::TitleBarStyle) -> Self { + self.window_builder = self.window_builder.title_bar_style(style); + self + } + + /// Hide the window title. + #[cfg(target_os = "macos")] + #[must_use] + pub fn hidden_title(mut self, hidden: bool) -> Self { + self.window_builder = self.window_builder.hidden_title(hidden); + self + } + + /// Defines the window [tabbing identifier] for macOS. + /// + /// Windows with matching tabbing identifiers will be grouped together. + /// If the tabbing identifier is not set, automatic tabbing will be disabled. + /// + /// [tabbing identifier]: + #[cfg(target_os = "macos")] + #[must_use] + pub fn tabbing_identifier(mut self, identifier: &str) -> Self { + self.window_builder = self.window_builder.tabbing_identifier(identifier); + self + } + + /// Sets window effects. + /// + /// Requires the window to be transparent. + /// + /// ## Platform-specific: + /// + /// - **Windows**: If using decorations or shadows, you may want to try this workaround + /// - **Linux**: Unsupported + pub fn effects(mut self, effects: WindowEffectsConfig) -> Self { + self.window_effects.replace(effects); + self + } +} + +/// 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 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 + } + + /// 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. + /// + /// On Windows, using `no_redirection_bitmap` can help avoid a white flash when + /// creating 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 { + &self.manager + } + + fn manager_owned(&self) -> Arc> { + self.manager.clone() + } + + fn runtime(&self) -> RuntimeOrDispatch<'_, R> { + RuntimeOrDispatch::Dispatch(self.window.dispatcher.clone()) + } + + 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 { + /// Grabs the [`Window`] from the [`CommandItem`]. This will never fail. + fn from_command(command: CommandItem<'de, R>) -> Result { + Ok(command.message.webview().window()) + } +} + +/// Base window functions. +impl Window { + /// Create a new window that is attached to the manager. + pub(crate) fn new( + manager: Arc>, + window: DetachedWindow, + app_handle: AppHandle, + #[cfg(desktop)] menu: Option>, + ) -> Self { + Self { + window, + manager, + app_handle, + #[cfg(desktop)] + menu: Arc::new(std::sync::Mutex::new(menu)), + resources_table: Default::default(), + } + } + + /// Initializes a window builder with the given window label. + /// + /// This creates a window without any webview attached to it; + /// use [`Window::add_child`] to add webviews to it. + /// To create a window with a single webview filling it, use + /// [`WebviewWindowBuilder`](crate::webview::WebviewWindowBuilder) instead. + /// + /// Data URLs are only supported with the `webview-data-url` feature flag. + /// + /// This function requires the `unstable` Cargo feature. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, + /// see [the Webview2 issue]. You should use `async` commands and separate threads when creating windows. + /// + /// # Examples + /// + /// ``` + /// tauri::Builder::default() + /// .setup(|app| { + /// let window = tauri::Window::builder(app, "label") + /// .title("Tauri") + /// .inner_size(800., 600.) + /// .build()?; + /// Ok(()) + /// }); + /// ``` + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + #[cfg(feature = "unstable")] + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + pub fn builder, L: Into>(manager: &M, label: L) -> WindowBuilder<'_, R, M> { + WindowBuilder::new(manager, label.into()) + } + + /// Adds a new webview as a child of this window. + /// + /// This is the multiwebview API: a single window can host any number of webviews, + /// each of them positioned and sized by you. It requires the `unstable` Cargo feature + /// and is only available on desktop. + /// + /// The webview is not resized or moved when the parent window is resized unless + /// auto resize is enabled, either at build time with + /// [`WebviewBuilder::auto_resize`](crate::webview::WebviewBuilder::auto_resize) + /// or later with [`Webview::set_auto_resize`]. + /// + /// # Arguments + /// + /// * `webview_builder` - the [`WebviewBuilder`] defining the webview to create. + /// * `position` - position of the webview's top-left corner, relative to the top-left corner of the window's + /// client area. Any type that converts into [`Position`] is accepted, so you can use either logical + /// ([`LogicalPosition`](crate::LogicalPosition)) or physical ([`PhysicalPosition`]) units. + /// * `size` - the webview size, also either logical ([`LogicalSize`](crate::LogicalSize)) or physical + /// ([`PhysicalSize`]) - any type that converts into [`Size`]. + /// + /// # Known issues + /// + /// On Windows, this function deadlocks when used in a synchronous command or event handlers, + /// see [the Webview2 issue]. You should use `async` commands and separate threads when creating webviews. + /// + /// # Examples + /// + /// Splitting a window between two webviews that resize with it: + /// + /// ``` + /// use tauri::{LogicalPosition, LogicalSize, WebviewUrl}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let width = 800.; + /// let height = 600.; + /// + /// let window = tauri::Window::builder(app, "main") + /// .inner_size(width, height) + /// .build()?; + /// + /// let _left = window.add_child( + /// tauri::webview::WebviewBuilder::new("left", WebviewUrl::App(Default::default())) + /// .auto_resize(), + /// LogicalPosition::new(0., 0.), + /// LogicalSize::new(width / 2., height), + /// )?; + /// + /// let _right = window.add_child( + /// tauri::webview::WebviewBuilder::new( + /// "right", + /// WebviewUrl::External("https://tauri.app".parse().unwrap()), + /// ) + /// .auto_resize(), + /// LogicalPosition::new(width / 2., 0.), + /// LogicalSize::new(width / 2., height), + /// )?; + /// + /// Ok(()) + /// }); + /// ``` + /// + /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 + #[cfg(any(test, all(desktop, feature = "unstable")))] + #[cfg_attr(docsrs, doc(cfg(all(desktop, feature = "unstable"))))] + pub fn add_child, S: Into>( + &self, + webview_builder: WebviewBuilder, + position: P, + size: S, + ) -> crate::Result> { + use std::sync::mpsc::channel; + + let (tx, rx) = channel(); + let position = position.into(); + let size = size.into(); + let window_ = self.clone(); + self.run_on_main_thread(move || { + let res = webview_builder.build(window_, position, size); + tx.send(res).unwrap(); + })?; + rx.recv().unwrap() + } + + /// List of webviews associated with this window. + pub fn webviews(&self) -> Vec> { + self + .manager + .webview + .webviews_lock() + .values() + .filter(|w| w.window_label() == self.label()) + .cloned() + .collect() + } + + pub(crate) fn is_webview_window(&self) -> bool { + self.webviews().iter().all(|w| w.label() == self.label()) + } + + /// Runs the given closure on the main thread. + pub fn run_on_main_thread(&self, f: F) -> crate::Result<()> { + self + .window + .dispatcher + .run_on_main_thread(f) + .map_err(Into::into) + } + + /// The label of this window. + pub fn label(&self) -> &str { + &self.window.label + } + + /// Registers a window event listener. + pub fn on_window_event(&self, f: F) { + self + .window + .dispatcher + .on_window_event(move |event| f(&event.clone().into())); + } +} + +/// Menu APIs +#[cfg(desktop)] +impl Window { + /// Registers a global menu event listener. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + /// + /// Also note that this handler will not be called if + /// the window used to register it was closed. + /// + /// # Examples + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::menu::{Menu, Submenu, MenuItem}; +tauri::Builder::default() + .setup(|app| { + let handle = app.handle(); + let save_menu_item = MenuItem::new(handle, "Save", true, None::<&str>)?; + let menu = Menu::with_items(handle, &[ + &Submenu::with_items(handle, "File", true, &[ + &save_menu_item, + ])?, + ])?; + let window = tauri::window::WindowBuilder::new(app, "editor") + .menu(menu) + .build() + .unwrap(); + + window.on_menu_event(move |window, event| { + if event.id == save_menu_item.id() { + // save menu item + } + }); + + Ok(()) + }); +``` + "#### + )] + pub fn on_menu_event, crate::menu::MenuEvent) + Send + Sync + 'static>( + &self, + f: F, + ) { + self + .manager + .menu + .event_listeners + .lock() + .unwrap() + .insert(self.label().to_string(), Box::new(f)); + } + + pub(crate) fn menu_lock(&self) -> std::sync::MutexGuard<'_, Option>> { + self.menu.lock().expect("poisoned window") + } + + #[cfg_attr(target_os = "macos", allow(dead_code))] + pub(crate) fn has_app_wide_menu(&self) -> bool { + self + .menu_lock() + .as_ref() + .map(|m| m.is_app_wide) + .unwrap_or(false) + } + + #[cfg_attr(target_os = "macos", allow(dead_code))] + pub(crate) fn is_menu_in_use>(&self, id: &I) -> bool { + self + .menu_lock() + .as_ref() + .map(|m| id.eq(m.menu.id())) + .unwrap_or(false) + } + + /// Returns this window menu . + pub fn menu(&self) -> Option> { + self.menu_lock().as_ref().map(|m| m.menu.clone()) + } + + /// Sets the window menu and returns the previous one. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one + /// window, if you need to set it, use [`AppHandle::set_menu`] instead. + #[cfg_attr(target_os = "macos", allow(unused_variables))] + pub fn set_menu(&self, menu: Menu) -> crate::Result>> { + let prev_menu = self.remove_menu()?; + + self.manager.menu.insert_menu_into_stash(&menu); + + let window = self.clone(); + let menu_ = menu.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let theme = window + .theme() + .map(crate::menu::map_to_menu_theme) + .unwrap_or(muda::MenuTheme::Auto); + + let _ = unsafe { menu_.inner().init_for_hwnd_with_theme(hwnd.0 as _, theme) }; + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let (Ok(gtk_window), Ok(gtk_box)) = (window.gtk_window(), window.default_vbox()) { + let _ = menu_ + .inner() + .init_for_gtk_window(>k_window, Some(>k_box)); + } + })?; + + self.menu_lock().replace(WindowMenu { + is_app_wide: false, + menu, + }); + + Ok(prev_menu) + } + + /// Removes the window menu and returns it. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one + /// window, if you need to remove it, use [`AppHandle::remove_menu`] instead. + pub fn remove_menu(&self) -> crate::Result>> { + let prev_menu = self.menu_lock().take().map(|m| m.menu); + + // remove from the window + #[cfg(not(target_os = "macos"))] + if let Some(menu) = &prev_menu { + let window = self.clone(); + let menu = menu.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let _ = unsafe { menu.inner().remove_for_hwnd(hwnd.0 as _) }; + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(gtk_window) = window.gtk_window() { + let _ = menu.inner().remove_for_gtk_window(>k_window); + } + })?; + } + + self + .manager + .remove_menu_from_stash_by_id(prev_menu.as_ref().map(|m| m.id())); + + Ok(prev_menu) + } + + /// Hides the window menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + pub fn hide_menu(&self) -> crate::Result<()> { + // remove from the window + #[cfg(not(target_os = "macos"))] + if let Some(window_menu) = &*self.menu_lock() { + let window = self.clone(); + let menu_ = window_menu.menu.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let _ = unsafe { menu_.inner().hide_for_hwnd(hwnd.0 as _) }; + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(gtk_window) = window.gtk_window() { + let _ = menu_.inner().hide_for_gtk_window(>k_window); + } + })?; + } + + Ok(()) + } + + /// Shows the window menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + pub fn show_menu(&self) -> crate::Result<()> { + // remove from the window + #[cfg(not(target_os = "macos"))] + if let Some(window_menu) = &*self.menu_lock() { + let window = self.clone(); + let menu_ = window_menu.menu.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let _ = unsafe { menu_.inner().show_for_hwnd(hwnd.0 as _) }; + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(gtk_window) = window.gtk_window() { + let _ = menu_.inner().show_for_gtk_window(>k_window); + } + })?; + } + + Ok(()) + } + + /// Shows the window menu. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. + pub fn is_menu_visible(&self) -> crate::Result { + // remove from the window + #[cfg(not(target_os = "macos"))] + if let Some(window_menu) = &*self.menu_lock() { + let (tx, rx) = std::sync::mpsc::channel(); + let window = self.clone(); + let menu_ = window_menu.menu.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let _ = tx.send(unsafe { menu_.inner().is_visible_on_hwnd(hwnd.0 as _) }); + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(gtk_window) = window.gtk_window() { + let _ = tx.send(menu_.inner().is_visible_on_gtk_window(>k_window)); + } + })?; + + return Ok(rx.recv().unwrap_or(false)); + } + + Ok(false) + } + + /// Shows the specified menu as a context menu at the cursor position. + pub fn popup_menu(&self, menu: &M) -> crate::Result<()> { + menu.popup(self.clone()) + } + + /// Shows the specified menu as a context menu at the specified position. + /// + /// The position is relative to the window's top-left corner. + pub fn popup_menu_at>( + &self, + menu: &M, + position: P, + ) -> crate::Result<()> { + menu.popup_at(self.clone(), position) + } +} + +/// Window getters. +impl Window { + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa. + pub fn scale_factor(&self) -> crate::Result { + self.window.dispatcher.scale_factor().map_err(Into::into) + } + + /// Returns the position of the top-left hand corner of the window's client area relative to the top-left hand corner of the desktop. + pub fn inner_position(&self) -> crate::Result> { + self.window.dispatcher.inner_position().map_err(Into::into) + } + + /// Returns the position of the top-left hand corner of the window relative to the top-left hand corner of the desktop. + pub fn outer_position(&self) -> crate::Result> { + self.window.dispatcher.outer_position().map_err(Into::into) + } + + /// Returns the physical size of the window's client area. + /// + /// The client area is the content of the window, excluding the title bar and borders. + pub fn inner_size(&self) -> crate::Result> { + self.window.dispatcher.inner_size().map_err(Into::into) + } + + /// Returns the physical size of the entire window. + /// + /// These dimensions include the title bar and borders. If you don't want that (and you usually don't), use inner_size instead. + pub fn outer_size(&self) -> crate::Result> { + self.window.dispatcher.outer_size().map_err(Into::into) + } + + /// Gets the window's current fullscreen state. + pub fn is_fullscreen(&self) -> crate::Result { + self.window.dispatcher.is_fullscreen().map_err(Into::into) + } + + /// Gets the window's current minimized state. + pub fn is_minimized(&self) -> crate::Result { + self.window.dispatcher.is_minimized().map_err(Into::into) + } + + /// Gets the window's current maximized state. + pub fn is_maximized(&self) -> crate::Result { + self.window.dispatcher.is_maximized().map_err(Into::into) + } + + /// Gets the window's current focus state. + pub fn is_focused(&self) -> crate::Result { + self.window.dispatcher.is_focused().map_err(Into::into) + } + + /// Gets the window's current decoration state. + pub fn is_decorated(&self) -> crate::Result { + self.window.dispatcher.is_decorated().map_err(Into::into) + } + + /// Gets the window's current resizable state. + pub fn is_resizable(&self) -> crate::Result { + self.window.dispatcher.is_resizable().map_err(Into::into) + } + + /// Whether the window is enabled or disabled. + pub fn is_enabled(&self) -> crate::Result { + self.window.dispatcher.is_enabled().map_err(Into::into) + } + + /// Determines if this window should always be on top of other windows. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + pub fn is_always_on_top(&self) -> crate::Result { + self + .window + .dispatcher + .is_always_on_top() + .map_err(Into::into) + } + + /// Gets the window's native maximize button state + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn is_maximizable(&self) -> crate::Result { + self.window.dispatcher.is_maximizable().map_err(Into::into) + } + + /// Gets the window's native minimize button state + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn is_minimizable(&self) -> crate::Result { + self.window.dispatcher.is_minimizable().map_err(Into::into) + } + + /// Gets the window's native close button state + /// + /// ## Platform-specific + /// + /// - **Linux / iOS / Android:** Unsupported. + pub fn is_closable(&self) -> crate::Result { + self.window.dispatcher.is_closable().map_err(Into::into) + } + + /// Gets the window's current visibility state. + pub fn is_visible(&self) -> crate::Result { + self.window.dispatcher.is_visible().map_err(Into::into) + } + + /// Gets the window's current title. + pub fn title(&self) -> crate::Result { + self.window.dispatcher.title().map_err(Into::into) + } + + /// Returns the monitor on which the window currently resides. + /// + /// Returns None if current monitor can't be detected. + pub fn current_monitor(&self) -> crate::Result> { + self + .window + .dispatcher + .current_monitor() + .map(|m| m.map(Into::into)) + .map_err(Into::into) + } + + /// Returns the monitor that contains the given point. + pub fn monitor_from_point(&self, x: f64, y: f64) -> crate::Result> { + self + .window + .dispatcher + .monitor_from_point(x, y) + .map(|m| m.map(Into::into)) + .map_err(Into::into) + } + + /// Returns the primary monitor of the system. + /// + /// Returns None if it can't identify any monitor as a primary one. + pub fn primary_monitor(&self) -> crate::Result> { + self + .window + .dispatcher + .primary_monitor() + .map(|m| m.map(Into::into)) + .map_err(Into::into) + } + + /// Returns the list of all the monitors available on the system. + pub fn available_monitors(&self) -> crate::Result> { + self + .window + .dispatcher + .available_monitors() + .map(|m| m.into_iter().map(Into::into).collect()) + .map_err(Into::into) + } + + /// Returns the native handle that is used by this window. + #[cfg(target_os = "macos")] + pub fn ns_window(&self) -> crate::Result<*mut std::ffi::c_void> { + self + .window + .dispatcher + .window_handle() + .map_err(Into::into) + .and_then(|handle| { + if let raw_window_handle::RawWindowHandle::AppKit(h) = handle.as_raw() { + let view: &objc2_app_kit::NSView = unsafe { h.ns_view.cast().as_ref() }; + let ns_window = view.window().expect("view to be installed in window"); + Ok(objc2::rc::Retained::autorelease_ptr(ns_window).cast()) + } else { + Err(crate::Error::InvalidWindowHandle) + } + }) + } + + /// Returns the pointer to the content view of this window. + #[cfg(target_os = "macos")] + pub fn ns_view(&self) -> crate::Result<*mut std::ffi::c_void> { + self + .window + .dispatcher + .window_handle() + .map_err(Into::into) + .and_then(|handle| { + if let raw_window_handle::RawWindowHandle::AppKit(h) = handle.as_raw() { + Ok(h.ns_view.as_ptr()) + } else { + Err(crate::Error::InvalidWindowHandle) + } + }) + } + + /// Returns the native handle that is used by this window. + #[cfg(windows)] + pub fn hwnd(&self) -> crate::Result { + self + .window + .dispatcher + .window_handle() + .map_err(Into::into) + .and_then(|handle| { + if let raw_window_handle::RawWindowHandle::Win32(h) = handle.as_raw() { + Ok(HWND(h.hwnd.get() as _)) + } else { + Err(crate::Error::InvalidWindowHandle) + } + }) + } + + /// Returns the `ApplicationWindow` from gtk crate that is used by this window. + /// + /// Note that this type can only be used on the main thread. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn gtk_window(&self) -> crate::Result { + self.window.dispatcher.gtk_window().map_err(Into::into) + } + + /// Returns the vertical [`gtk::Box`] that is added by default as the sole child of this window. + /// + /// Note that this type can only be used on the main thread. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn default_vbox(&self) -> crate::Result { + 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 + /// + /// - **macOS**: Only supported on macOS 10.14+. + pub fn theme(&self) -> crate::Result { + self.window.dispatcher.theme().map_err(Into::into) + } +} + +/// Desktop window getters. +#[cfg(desktop)] +impl Window { + /// Get the cursor position relative to the top-left hand corner of the desktop. + /// + /// Note that the top-left hand corner of the desktop is not necessarily the same as the screen. + /// If the user uses a desktop with multiple monitors, + /// the top-left hand corner of the desktop is the top-left hand corner of the main monitor on Windows and macOS + /// or the top-left of the leftmost monitor on X11. + /// + /// The coordinates can be negative if the top-left hand corner of the window is outside of the visible screen region. + pub fn cursor_position(&self) -> crate::Result> { + self.app_handle.cursor_position() + } +} + +/// Window setters and actions. +impl Window { + /// 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 + .dispatcher + .set_resizable(resizable) + .map_err(Into::into) + } + + /// Set this window's title. + pub fn set_title(&self, title: &str) -> crate::Result<()> { + self + .window + .dispatcher + .set_title(title.to_string()) + .map_err(Into::into) + } + + /// Enable or disable the window. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_enabled(enabled) + .map_err(Into::into) + } + + /// Show this window. + pub fn show(&self) -> crate::Result<()> { + self.window.dispatcher.show().map_err(Into::into) + } + + /// Hide this window. + pub fn hide(&self) -> crate::Result<()> { + self.window.dispatcher.hide().map_err(Into::into) + } + + /// 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) + } + + /// 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.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 + pub fn set_decorations(&self, decorations: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_decorations(decorations) + .map_err(Into::into) + } + + /// Determines if this window should have shadow. + /// + /// ## Platform-specific + /// + /// - **Windows:** + /// - `false` has no effect on decorated window, shadow are always ON. + /// - `true` will make undecorated window have a 1px white border, + /// and on Windows 11, it will have a rounded corners. + /// - **Linux:** Unsupported. + pub fn set_shadow(&self, enable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_shadow(enable) + .map_err(Into::into) + } + + /// Sets window effects, pass [`None`] to clear any effects applied if possible. + /// + /// Requires the window to be transparent. + /// + /// See [`EffectsBuilder`] for a convenient builder for [`WindowEffectsConfig`]. + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::{Manager, window::{Color, Effect, EffectState, EffectsBuilder}}; +tauri::Builder::default() + .setup(|app| { + let window = app.get_window("main").unwrap(); + window.set_effects( + EffectsBuilder::new() + .effect(Effect::Popover) + .state(EffectState::Active) + .radius(5.) + .color(Color(0, 0, 0, 255)) + .build(), + )?; + Ok(()) + }); +``` + "#### + )] + /// + /// ## Platform-specific: + /// + /// - **Windows**: If using decorations or shadows, you may want to try this workaround + /// - **Linux**: Unsupported + pub fn set_effects>>(&self, effects: E) -> crate::Result<()> { + let effects = effects.into(); + let window = self.clone(); + self.run_on_main_thread(move || { + let _ = crate::vibrancy::set_window_effects(&window, effects); + }) + } + + /// Determines if this window should always be below other windows. + pub fn set_always_on_bottom(&self, always_on_bottom: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_always_on_bottom(always_on_bottom) + .map_err(Into::into) + } + + /// Determines if this window should always be on top of other windows. + pub fn set_always_on_top(&self, always_on_top: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_always_on_top(always_on_top) + .map_err(Into::into) + } + + /// Sets whether the window should be visible on all workspaces or virtual desktops. + /// + /// ## Platform-specific + /// + /// - **Windows / iOS / Android:** Unsupported. + pub fn set_visible_on_all_workspaces( + &self, + visible_on_all_workspaces: bool, + ) -> crate::Result<()> { + self + .window + .dispatcher + .set_visible_on_all_workspaces(visible_on_all_workspaces) + .map_err(Into::into) + } + + /// Determines if this window should be fullscreen. + pub fn set_fullscreen(&self, fullscreen: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_fullscreen(fullscreen) + .map_err(Into::into) + } + + /// Sets the window as fullscreen on the monitor that contains the given physical position, + /// such as a [`Monitor::position`](crate::Monitor::position). + /// + /// Does nothing if no monitor contains the position. + /// + /// # Examples + /// + #[cfg_attr( + feature = "unstable", + doc = r####" +```rust,no_run +use tauri::Manager; +tauri::Builder::default() + .setup(|app| { + let window = app.get_window("main").unwrap(); + if let Some(monitor) = window.available_monitors()?.into_iter().nth(1) { + let position = monitor.position(); + window.set_fullscreen_on_monitor(tauri::PhysicalPosition::new( + position.x as f64, + position.y as f64, + ))?; + } + Ok(()) + }); +``` + "#### + )] + pub fn set_fullscreen_on_monitor(&self, position: PhysicalPosition) -> crate::Result<()> { + self + .window + .dispatcher + .set_fullscreen_on_monitor(position) + .map_err(Into::into) + } + + /// Toggles a fullscreen mode that doesn't require a new macOS space. + /// Returns a boolean indicating whether the transition was successful (this won't work if the window was already in the native fullscreen). + /// + /// This is how fullscreen used to work on macOS in versions before Lion. + /// And allows the user to have a fullscreen window without using another space or taking control over the entire monitor. + /// + /// ## Platform-specific + /// + /// - **macOS:** Uses native simple fullscreen mode. + /// - **Other platforms:** Falls back to [`Self::set_fullscreen`]. + pub fn set_simple_fullscreen(&self, enable: bool) -> crate::Result<()> { + #[cfg(target_os = "macos")] + { + self + .window + .dispatcher + .set_simple_fullscreen(enable) + .map_err(Into::into) + } + #[cfg(not(target_os = "macos"))] + self.set_fullscreen(enable) + } + + /// Sets this window' icon. + pub fn set_icon(&self, icon: Image<'_>) -> crate::Result<()> { + self + .window + .dispatcher + .set_icon(icon.into()) + .map_err(Into::into) + } + + /// Whether to hide the window icon from the taskbar or not. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn set_skip_taskbar(&self, skip: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_skip_taskbar(skip) + .map_err(Into::into) + } + + /// Grabs the cursor, preventing it from leaving the window. + /// + /// There's no guarantee that the cursor will be hidden. You should + /// hide it by yourself if you want so. + /// + /// ## Platform-specific + /// + /// - **Linux:** Unsupported. + /// - **macOS:** This locks the cursor in a fixed location, which looks visually awkward. + pub fn set_cursor_grab(&self, grab: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_cursor_grab(grab) + .map_err(Into::into) + } + + /// Modifies the cursor's visibility. + /// + /// If `false`, this will hide the cursor. If `true`, this will show the cursor. + /// + /// ## Platform-specific + /// + /// - **Windows:** The cursor is only hidden within the confines of the window. + /// - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor is + /// outside of the window. + pub fn set_cursor_visible(&self, visible: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_cursor_visible(visible) + .map_err(Into::into) + } + + /// Modifies the cursor icon of the window. + pub fn set_cursor_icon(&self, icon: CursorIcon) -> crate::Result<()> { + self + .window + .dispatcher + .set_cursor_icon(icon) + .map_err(Into::into) + } + + /// Changes the position of the cursor in window coordinates. + pub fn set_cursor_position>(&self, position: Pos) -> crate::Result<()> { + self + .window + .dispatcher + .set_cursor_position(position) + .map_err(Into::into) + } + + /// Ignores the window cursor events. + pub fn set_ignore_cursor_events(&self, ignore: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_ignore_cursor_events(ignore) + .map_err(Into::into) + } + + /// Starts dragging the window. + pub fn start_dragging(&self) -> crate::Result<()> { + self.window.dispatcher.start_dragging().map_err(Into::into) + } + + /// Starts resize-dragging the window. + pub fn start_resize_dragging( + &self, + direction: tauri_runtime::ResizeDirection, + ) -> crate::Result<()> { + self + .window + .dispatcher + .start_resize_dragging(direction) + .map_err(Into::into) + } + + /// Sets the overlay icon on the taskbar **Windows only**. Using `None` to remove the overlay icon + /// + /// The overlay icon can be unique for each window. + #[cfg(target_os = "windows")] + #[cfg_attr(docsrs, doc(cfg(target_os = "windows")))] + pub fn set_overlay_icon(&self, icon: Option>) -> crate::Result<()> { + self + .window + .dispatcher + .set_overlay_icon(icon.map(|x| x.into())) + .map_err(Into::into) + } + + /// Sets the taskbar badge count. Using `0` or `None` will remove the badge + /// + /// ## Platform-specific + /// - **Windows:** Unsupported, use [`Window::set_overlay_icon`] instead. + /// - **iOS:** iOS expects i32, the value will be clamped to i32::MIN, i32::MAX. + /// - **Android:** Unsupported. + pub fn set_badge_count(&self, count: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_badge_count(count, Some(format!("{}.desktop", self.package_info().name))) + .map_err(Into::into) + } + + /// Sets the taskbar badge label **macOS only**. Using `None` will remove the badge + #[cfg(target_os = "macos")] + #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] + pub fn set_badge_label(&self, label: Option) -> crate::Result<()> { + self + .window + .dispatcher + .set_badge_label(label) + .map_err(Into::into) + } + + /// Sets the taskbar progress state. + /// + /// ## Platform-specific + /// + /// - **Linux / macOS**: Progress bar is app-wide and not specific to this window. + /// - **Linux**: Only supported desktop environments with `libunity` (e.g. GNOME). + /// - **iOS / Android:** Unsupported. + pub fn set_progress_bar(&self, progress_state: ProgressBarState) -> crate::Result<()> { + self + .window + .dispatcher + .set_progress_bar(crate::runtime::ProgressBarState { + status: progress_state.status, + progress: progress_state.progress, + desktop_filename: Some(format!("{}.desktop", self.package_info().name)), + }) + .map_err(Into::into) + } + + /// Sets the title bar style. **macOS only**. + pub fn set_title_bar_style(&self, style: tauri_utils::TitleBarStyle) -> crate::Result<()> { + self + .window + .dispatcher + .set_title_bar_style(style) + .map_err(Into::into) + } +} + +/// Progress bar state. +#[cfg(desktop)] +#[cfg_attr( + docsrs, + doc(cfg(any(target_os = "macos", target_os = "linux", windows))) +)] +#[derive(serde::Deserialize, Debug)] +pub struct ProgressBarState { + /// The progress bar status. + pub status: Option, + /// The progress bar progress. This can be a value ranging from `0` to `100` + pub progress: Option, +} + +impl Listener for Window { + /// Listen to an event on this window. + /// + /// # Examples + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::{Manager, Listener}; + +tauri::Builder::default() + .setup(|app| { + let window = app.get_window("main").unwrap(); + window.listen("component-loaded", move |event| { + println!("window just loaded a component"); + }); + + Ok(()) + }); +``` + "#### + )] + fn listen(&self, event: impl Into, handler: F) -> EventId + where + F: Fn(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager.listen( + event, + EventTarget::Window { + label: self.label().to_string(), + }, + handler, + ) + } + + /// Listen to an event on this window only once. + /// + /// See [`Self::listen`] for more information. + fn once(&self, event: impl Into, handler: F) -> EventId + where + F: FnOnce(Event) + Send + 'static, + { + let event = EventName::new(event.into()).unwrap(); + self.manager.once( + event, + EventTarget::Window { + label: self.label().to_string(), + }, + handler, + ) + } + + /// Unlisten to an event on this window. + /// + /// # Examples + #[cfg_attr( + feature = "unstable", + doc = r####" +``` +use tauri::{Manager, Listener}; + +tauri::Builder::default() + .setup(|app| { + let window = app.get_window("main").unwrap(); + let window_ = window.clone(); + let handler = window.listen("component-loaded", move |event| { + println!("window just loaded a component"); + + // we no longer need to listen to the event + // we also could have used `window.once` instead + window_.unlisten(event.id()); + }); + + // stop listening to the event when you do not need it anymore + window.unlisten(handler); + + Ok(()) + }); +``` + "#### + )] + fn unlisten(&self, id: EventId) { + self.manager.unlisten(id) + } +} + +impl Emitter for Window {} + +/// The [`WindowEffectsConfig`] object builder +#[derive(Default)] +pub struct EffectsBuilder(WindowEffectsConfig); +impl EffectsBuilder { + /// Create a new [`WindowEffectsConfig`] builder + pub fn new() -> Self { + Self(WindowEffectsConfig::default()) + } + + /// Adds effect to the [`WindowEffectsConfig`] `effects` field + pub fn effect(mut self, effect: Effect) -> Self { + self.0.effects.push(effect); + self + } + + /// Adds effects to the [`WindowEffectsConfig`] `effects` field + pub fn effects>(mut self, effects: I) -> Self { + self.0.effects.extend(effects); + self + } + + /// Clears the [`WindowEffectsConfig`] `effects` field + pub fn clear_effects(mut self) -> Self { + self.0.effects.clear(); + self + } + + /// Sets `state` field for the [`WindowEffectsConfig`] **macOS Only** + pub fn state(mut self, state: EffectState) -> Self { + self.0.state = Some(state); + self + } + /// Sets `radius` field fo the [`WindowEffectsConfig`] **macOS Only** + pub fn radius(mut self, radius: f64) -> Self { + self.0.radius = Some(radius); + self + } + /// Sets `color` field fo the [`WindowEffectsConfig`] **Windows Only** + pub fn color(mut self, color: Color) -> Self { + self.0.color = Some(color); + self + } + + /// Sets `interactive` field for the [`WindowEffectsConfig`] **macOS 27.0+ Only** + pub fn interactive(mut self, interactive: bool) -> Self { + self.0.interactive = interactive; + self + } + + /// Builds a [`WindowEffectsConfig`] + pub fn build(self) -> WindowEffectsConfig { + self.0 + } +} + +impl From for EffectsBuilder { + fn from(value: WindowEffectsConfig) -> Self { + Self(value) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn window_is_send_sync() { + crate::test_utils::assert_send::(); + crate::test_utils::assert_sync::(); + } +} diff --git a/crates/tauri/src/window/plugin.rs b/crates/tauri/src/window/plugin.rs new file mode 100644 index 0000000..2ccc2fe --- /dev/null +++ b/crates/tauri/src/window/plugin.rs @@ -0,0 +1,351 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! The tauri plugin to create and manipulate windows from JS. + +use crate::{ + Runtime, Window, + plugin::{Builder, TauriPlugin}, + sealed::ManagerBase, +}; + +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, Monitor, PhysicalPosition, PhysicalSize, Position, Size, Theme, Window, command, + sealed::ManagerBase, utils::config::WindowConfig, window::Color, window::WindowBuilder, + }; + + #[command(root = "crate")] + pub async fn get_all_windows(app: AppHandle) -> Vec { + app.manager().windows().keys().cloned().collect() + } + + #[command(root = "crate")] + pub async fn create(window: Window, options: WindowConfig) -> crate::Result<()> { + WindowBuilder::from_config(&window, &options)?.build()?; + Ok(()) + } + + getter!(scale_factor, f64); + getter!(inner_position, PhysicalPosition); + getter!(outer_position, PhysicalPosition); + getter!(inner_size, PhysicalSize); + getter!(outer_size, PhysicalSize); + getter!(is_focused, bool); + getter!(is_resizable, 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!(is_always_on_top, bool); + + setter!(center); + setter!(request_user_attention, Option); + setter!(set_maximizable, bool); + setter!(set_minimizable, bool); + setter!(set_closable, bool); + setter!(maximize); + setter!(unmaximize); + setter!(minimize); + setter!(unminimize); + 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_fullscreen, bool); + setter!(set_fullscreen_on_monitor, PhysicalPosition); + setter!(set_simple_fullscreen, bool); + setter!(set_skip_taskbar, bool); + setter!(set_cursor_grab, bool); + setter!(set_cursor_visible, bool); + setter!(set_cursor_icon, CursorIcon); + setter!(set_cursor_position, Position); + setter!(set_ignore_cursor_events, bool); + setter!(start_dragging); + setter!(start_resize_dragging, ResizeDirection); + setter!(set_progress_bar, ProgressBarState); + setter!(set_badge_count, Option); + #[cfg(target_os = "macos")] + setter!(set_badge_label, Option); + setter!(set_visible_on_all_workspaces, bool); + setter!(set_title_bar_style, TitleBarStyle); + + #[command(root = "crate")] + #[cfg(target_os = "windows")] + pub async fn set_overlay_icon( + webview: Webview, + window: Window, + label: Option, + value: Option, + ) -> crate::Result<()> { + let window = get_window(window, label)?; + let resources_table = webview.resources_table(); + + let value = match value { + Some(value) => Some(value.into_img(&resources_table)?.as_ref().clone()), + None => None, + }; + + window.set_overlay_icon(value) + } + + #[command(root = "crate")] + pub async fn set_icon( + webview: Webview, + window: Window, + label: Option, + value: crate::image::JsImage, + ) -> crate::Result<()> { + let window = get_window(window, label)?; + let resources_table = webview.resources_table(); + window.set_icon(value.into_img(&resources_table)?.as_ref().clone()) + } + + #[command(root = "crate")] + pub async fn toggle_maximize( + window: Window, + label: Option, + ) -> crate::Result<()> { + let window = get_window(window, label)?; + if window.is_maximized()? { + window.unmaximize()? + } else { + window.maximize()? + }; + Ok(()) + } + + #[command(root = "crate")] + pub async fn internal_toggle_maximize( + window: Window, + label: Option, + ) -> crate::Result<()> { + let window = get_window(window, label)?; + if window.is_resizable()? { + if window.is_maximized()? { + window.unmaximize()? + } else if window.is_maximizable()? { + window.maximize()? + }; + } + Ok(()) + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + use serialize_to_javascript::{DefaultTemplate, Template, default_template}; + + let mut init_script = String::new(); + + #[derive(Template)] + #[default_template("./scripts/drag.js")] + struct Drag<'a> { + os_name: &'a str, + } + + init_script.push_str( + &Drag { + os_name: std::env::consts::OS, + } + .render_default(&Default::default()) + .unwrap() + .into_string(), + ); + + Builder::new("window") + .js_init_script(init_script) + .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_fullscreen_on_monitor, + #[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/tauri/src/window/scripts/drag.js b/crates/tauri/src/window/scripts/drag.js new file mode 100644 index 0000000..5a0b124 --- /dev/null +++ b/crates/tauri/src/window/scripts/drag.js @@ -0,0 +1,129 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +;(function () { + //-----------------------// + // drag on mousedown and maximize on double click on Windows and Linux + // while macOS maximization should be on mouseup and if the mouse + // moves after the double click, it should be cancelled (see https://github.com/tauri-apps/tauri/issues/8306) + //-----------------------// + const TAURI_DRAG_REGION_ATTR = 'data-tauri-drag-region' + const CLICKABLE_TAGS = new Set([ + 'A', + 'BUTTON', + 'INPUT', + 'SELECT', + 'TEXTAREA', + 'LABEL', + 'SUMMARY' + ]) + const INTERACTIVE_ROLES = new Set([ + 'button', + 'link', + 'menuitem', + 'tab', + 'checkbox', + 'radio', + 'switch', + 'option' + ]) + + function isClickableElement(el) { + return ( + CLICKABLE_TAGS.has(el.tagName) + || (el.hasAttribute('contenteditable') + && el.getAttribute('contenteditable') !== 'false') + || (el.hasAttribute('tabindex') && el.getAttribute('tabindex') !== '-1') + || INTERACTIVE_ROLES.has(el.getAttribute('role')) + ) + } + + // Walk the composed path from target upward. + // + // Supported values for data-tauri-drag-region: + // (bare / no value / "true") -> self: only direct clicks on this element trigger drag + // "deep" -> deep: clicks anywhere in the subtree trigger drag + // "false" -> disabled: drag is blocked here (and for ancestors) + // + // Clickable elements (buttons, links, etc.) normally block dragging, + // but if they themselves carry data-tauri-drag-region they act as drag regions. + function isDragRegion(composedPath) { + for (const el of composedPath) { + if (!(el instanceof HTMLElement)) continue + + const attr = el.getAttribute(TAURI_DRAG_REGION_ATTR) + + // clickable without explicit drag region → blocks drag + if (isClickableElement(el) && attr === null) return false + // no attr → keep walking up + if (attr === null) continue + // explicitly disabled + if (attr === 'false') return false + // subtree drag — any descendant triggers + if (attr === 'deep') return true + // bare or "true" attr — only direct clicks on this element + if (attr === '' || attr === 'true') return el === composedPath[0] + } + + return false + } + + const osName = __TEMPLATE_os_name__ + + // initial mousedown position for macOS + let initialX = 0 + let initialY = 0 + + document.addEventListener('mousedown', (e) => { + if ( + // was left mouse button + e.button === 0 + // and was normal click to drag or double click to maximize + && (e.detail === 1 || e.detail === 2) + // and is drag region + && isDragRegion(e.composedPath()) + ) { + // macOS maximization happens on `mouseup`, + // so we save needed state and early return + if (osName === 'macos' && e.detail === 2) { + initialX = e.clientX + initialY = e.clientY + return + } + + // prevents text cursor + e.preventDefault() + + // fix #2549: double click on drag region edge causes content to maximize without window sizing change + // https://github.com/tauri-apps/tauri/issues/2549#issuecomment-1250036908 + e.stopImmediatePropagation() + + // start dragging if the element has a `tauri-drag-region` data attribute and maximize on double-clicking it + const cmd = e.detail === 2 ? 'internal_toggle_maximize' : 'start_dragging' + window.__TAURI_INTERNALS__.invoke('plugin:window|' + cmd) + } + }) + + // on macOS we maximize on mouseup instead, to match the system behavior where maximization can be canceled + // if the mouse moves outside the data-tauri-drag-region + if (osName === 'macos') { + document.addEventListener('mouseup', (e) => { + if ( + // was left mouse button + e.button === 0 + // and was double click + && e.detail === 2 + // and the cursor hasn't moved from initial mousedown + && e.clientX === initialX + && e.clientY === initialY + // and the event path contains a drag region (with no clickable element in between) + && isDragRegion(e.composedPath()) + ) { + window.__TAURI_INTERNALS__.invoke( + 'plugin:window|internal_toggle_maximize' + ) + } + }) + } +})() diff --git a/crates/tauri/test/api/test.txt b/crates/tauri/test/api/test.txt new file mode 100644 index 0000000..ec77307 --- /dev/null +++ b/crates/tauri/test/api/test.txt @@ -0,0 +1 @@ +This is a test doc! \ No newline at end of file diff --git a/crates/tauri/test/api/test_binary b/crates/tauri/test/api/test_binary new file mode 100644 index 0000000000000000000000000000000000000000..35d42e808f0a8017b8d52a06be2f8fec0b466a66 GIT binary patch literal 43 scmZ?wbhEHbWMp7uXkcLY|NlP&1B2pE7Dgb&paUX6G7L;iE{qJ;0LZEa`2YX_ literal 0 HcmV?d00001 diff --git a/crates/tauri/test/fixture/config.json b/crates/tauri/test/fixture/config.json new file mode 100644 index 0000000..775a19e --- /dev/null +++ b/crates/tauri/test/fixture/config.json @@ -0,0 +1 @@ +{ "devUrl": "http://localhost" } diff --git a/crates/tauri/test/fixture/dist/index.html b/crates/tauri/test/fixture/dist/index.html new file mode 100644 index 0000000..698a357 --- /dev/null +++ b/crates/tauri/test/fixture/dist/index.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/tauri/test/fixture/isolation/dist/index.html b/crates/tauri/test/fixture/isolation/dist/index.html new file mode 100644 index 0000000..698a357 --- /dev/null +++ b/crates/tauri/test/fixture/isolation/dist/index.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/tauri/test/fixture/isolation/isolation-dist/index.html b/crates/tauri/test/fixture/isolation/isolation-dist/index.html new file mode 100644 index 0000000..a97caa8 --- /dev/null +++ b/crates/tauri/test/fixture/isolation/isolation-dist/index.html @@ -0,0 +1,10 @@ + + + + + Isolation Secure Script + + + + + diff --git a/crates/tauri/test/fixture/isolation/isolation-dist/index.js b/crates/tauri/test/fixture/isolation/isolation-dist/index.js new file mode 100644 index 0000000..6510a21 --- /dev/null +++ b/crates/tauri/test/fixture/isolation/isolation-dist/index.js @@ -0,0 +1,8 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +window.__TAURI_ISOLATION_HOOK__ = (payload, options) => { + console.log('hook', payload, options) + return payload +} diff --git a/crates/tauri/test/fixture/isolation/src-tauri/icons/icon.ico b/crates/tauri/test/fixture/isolation/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3636e4b22ba65db9061cd60a77b02c92022dfd6 GIT binary patch literal 86642 zcmeEP2|U!>7oQpXz6;qIyGWagPzg~;i?ooGXpc%o)+~`MC6#O`?P*_Srl`>>O4^Vl zt=7su|8s`v_4?O)M!om+p5N#5ojdpUyUV%foO|y2yFUVfNMI)j3lqRqBrISj5XKP* z1VzP8|30{X1nva{bow>8iG-;V5CAR=-#C~+ST9E;Xn-Gr!ky0h;1D2Lf*4;X82+F5 z^O!~^Jf^7tRQm(w05$`n0FD500O1jY`PTJCTr&uF8&Ctd3%CcU15g0^07(D;)9Adf zstIlhAP-;y5Cn(-CIB#7-_;YEcYcq9pC`~SCax^yT;tqFlpu0SAAgb0M(%>+U?7k~|H%oqaU zG7;{Jz;i$ysD3TnZ-VD-5EkR2olyjs0?__2E-*ZQm7VF#;NSU+_7OmYx`1^UZOBN# zZ~z&=UqaKwI`Y#Ck2VnUWrsY50ipqDyIunt0QGGg8gr?2RTL#iQ3}^>n-k1l{K?P(24g%0NBOjQwp>0N6 zhjzBRS^h3uXS+k@hxlm#X1Zv9Hv0OTvCgXwwP zq#48g-{<`$)9@L955ofX03HIiAkD1kBgDb{vAtuK;{yB_#QPb z7^H|%!06@BiN3iB9Ci78{h)m}hG)EA_Y1zH`^*1Wf4llgsP9;I#3BHLhv)*3H@g5R zlV^Z+P(Cg!<3L6m(}8Vg0JP8Z6)1FRdI6mvlhg2JHsAe^X#fq({sQKWx@-!-`2=vgJA|ipM_2(ARW89@<$pz0wRD0er!Mg=)&?pq^Uuj`CRX?9*x7azbOAK z@H2G-^F}=%gkdm!Y=a>`Q^09J3jk?AHwd1ygZo_)zQ|)8q{l2D{8#x>{=D$a3qS*8 z111CAXbTwW4yLv;z_e*M;Xm3zM*5f!0C|LU zg0Iuw|9`uKynsF=_C>Le(g8pk&cc1r&p*nakv`gza{%N4>RJSp5&Mw;$GgsaI*5=q zmKXbCpZlKhA9*1IxDCMk>j5T!|4WB?1IvT?0BiuDe+(M19t1$Sg}`OV0>fk8pmV72 z*#F7{U_NW0eAu7a2&1HW%{zY}3)Up9h#SY3NF47`W8{X8O(W ze>OhDK0LaB@qi`(hS@cO+Q^{od->yi%maY-6m1cfpQ(>qnED85VcK)M(q-n4ZhYr6 z?DL`?bPNYS@*baIA02u2N7*x;b?F+k<*G9Px4US_gnGiT>6iw<41l`L%)cG}F9P5* zCd}dgCjf>?g|QY9W!Ign^11>c|FRO{UA~Ycj6Ga{hP6N!@P*9aA*6#kz6$UJfa8a) z0PLSLo}&x!1~BPEU4Uop-N_!}GWdt%ozXHBy3E`wDI75VA-wBVTOGd0>2?(2cQ9fd87SHgfKkd{y|RPf7B@l#{7Ukq=937 zOc#Ow3jj#VQ2-6_9>9Fw2LE>h7~|aU=kVuGP^Lf!^3@q|AAsdz=JPEV<>d=;gux{Y zr8fO}CVvtF`Or1iSA;ZI04@NY0crqf2Qbg8fDHgW2v5Q|Kl{S^JB<1Pbg6?E@=*d9 z00sld071yJ+cxHB)Ap;SM`vCXf0#BfB^<>kvv01CC`J_@zV+k|RO1cjR9xrCYoxrEvTxwtwwxwz<|Ttaj%K_NO@n-D#) zNr4^!2~!9r^m2kfBuuAwurYI`<2*$GG7aW4KF?FYzrJ}2WJ=%F$ALZ$^l_k%1AQFm z<3Jw=`Z&D9AVFj7Vcf(hBajw0PLk8I{=n~yu$%I0l1F|_gft6 za?!s75C&KbVeKIv>~A1Tfy;$^S>XP!%94LQ-B@QI(6mS(b1{&Y5y)*h$P4#F-2%J> z;97ngfVrOkM=plL@Ku28fHc5jNOw5wlMyMV>41&U{MYlew-@jM$UKSWi1i%z1sVeU zKu$RT+^g7KS^tq9eEF;u(!{-I7eKdsAg{ro3%svrg3zYu_I6hNtLVeJcZW6<_r{5W z9Kf!t?gQX{w06LkGW)Ckqi#J1q=PO@02+j=XySeC!(Xgr4?*rvXo^_hg@NZ&fcK|B z2DlINuaa|j(yf8~j{!Y)ppOEuSE|n*`~`aO2=*ree>s8Aroiumy+H0?>jvsU2GBPG z=;Qz${R_D8-%ApBNhqbs;@(qPsP93*<4VBSyzfo^a-b9TrmIOkfqmOJ7U{cs#sQQ) zjN@?6E7p1FcYWRy+?(Y6En4vXkrP0-VF^tK#w6-JW59nn7TQmcKkWG@&j((X0=~uP z-hQtH=${GYfcI4T+Jo+@Gt?Wj_aeZ%V30fWU4-5)>+jL`7Rs>(#)^V{I`GFD0J6ru zJp$e{Cnta(-$VKyUw@_h`2Ke!0N-K#V2j;&S(5D06(DAN%k8`()z$2V%`%#|b`*UD>8D~&L zfjyZ4X%7X+0)!wxe4mgDfbZ8~`;2`JoL7(s41@o(;6BPL5AYs<>HR28r~{iIFUbG< z@AQ6yJ^$)kD0}E5;k#wH_VT0k4(-N0KqT;ZG^8y7X~P(Twf+~h*GLnNJ^BG%;~+iM zg$IBi)lFDeAp61^B&;{GM$^Ah34q72ZljHSUI@JXk-0palP!RBya8n3E&I>nZmDB5BQO}=69e2E^yug@xMGa#CiPk&bb{6;AaJ(r}h=s>B2xhYWHEhjXL#L zT%9(7@eZyQ0^+7G~b+gU#t=Xw1ZKfZik4slKJ9O2%+pQ3AyfCw(M=Qv-4dl$%aK>pZ2JOOwN zfOhPg`f#K-+qWO7cwd|$IUdSh^PTd4DRbt393%OH+*zK({SkV9X522Fz`f}Lpc85U z2Po4f;6Xm%%Q??i@N5*^Biy1H{!9}7@wA}qI7a7yvc&_Kvh9w06?mcm_{Yoevk1Vl z0N_knRcUZx3`~Zz1sP}f!rBEn9PB^p%FoKKSEPgG0VqH@3s{gp&Z)SUG4}lad*uJ6 zK)Uz>^@6dsuoB7}0}uy%8SIz-UqsV~ecSl{6xkli)d1*Dy~i-u0J4Bzy8PWC9{V-0 z*AePHSq#dH>(bqc_Dh7pxzb{qHVNdv5z5tF+2eT6r+_v9*2sRm?(d~}!CI3X@R+fO zoD8(s0hVAMoi6GoSrhVtd3{CD)xLeZKTEk#eqiT>f!7yVkUy*kGTy)ZVKPwvpnl;T z`v^!A_m!0Za8DNM81Cyp7yIPcH{S&?g|I)oo`h#o!}+OPa3-cMoSP{J;MVKGIjld- zfPXjv;3wLCZE(u~-L3ywAUFOWt@~Z=E9f4173BS_oB6+h@arKi>__T(KMc=hA3|+~ zb5c9-T=pVBI$!}{Am{{t*O}@6uyp>~?DJ_RAbZCAIIfj;x9!KdvsGm@d9WKjxBXw( z9UNE|d{;sF z_vFHOopqlvmjeBWZs+?gx~d^9E1Z`t?!kNBAXAV(T^aBIz?A#fE}m6h0tf(IQ5`|8 zBf?qzJt=yxi-YYa)J53m!8nWITm1djy=;&_w%I)@Pp9nFFwdkPlzkU%52T?`BIXX-^U=z+^%Y8wxZC4R-LQx=SMZCZEb4{{Hq(rkziK$fgt*zYTa{eX}c zj`x1XI~!fPKn~tVTZnBLOC$}2?{jXZZo}_~g!DlEs0TF=HxwX&x`gA2U+L`|6+@o_;pr6KgrvTE#aox*ecLry)%;_6Z@) zze9vSlt-8R1%ZEO0pH{A*Y|h-$ec@8|6dRC>+XE-*ZF_#$2kC8J7Ad?(1(ZqUmMQr zYy>dBMaYzAPh9-=*ilGV9_2rrTFWv`e`kbF`7_4i`&f|wg~zbBzbE|0vZ0NJej2<_ z%J}~K*Rt$^pA2WYsQ2hy1C&wM9B_a5KMQ3Ccn9c-?3r=e!4B*Ky%IzF(wi@o1=@0u z1@xb~UH^+g_DT@GM@57AMwoNPbK=NWkVa45FZohOY9O5{xE9fq@d&d3Aa4SEn;826 zI2U9MI09gPCy^;vR@^2?%OB(q>x;ct2XOu$&%^_Ht^ir!y3Uup{oem~5ZBSp} zJ1vSD$M^;`GmqZn-i32If%hnXJ8*H${g3#~e1?2qih9H9c>Bw;ceXubDabPwz^V=a z4XOvhe#wDL$bzx|&%ChzHkA4S=JwjPpdP1!9GTy%{+_JAcmEF5e;tSq-{t)DGfDhu zX<gsXSELq@*pp%q)9^DAK#0I_4q!_Cj%`o79|^koZSIofLK5{ zz!RR01i1?r!h1Zdj`M$%fjCcWNd3SL?E-$Q8^7iJ2lf41&pN0Ow|{T!3o>me@YoT+ z%9_k2kO#~i{`cF;d$hq^ou(?_`Ave)BK9R^tr0vGp%v7!Uns5`xJ zEYR5oFven+S&%>4fCmtF5V$|3FZe6yMOR;d2(n)e!1dqm>Od{%jWzBqAJNP9jxo;c zfbXzDeO?N(WOY8~0Q4gz{#)$;?j7rp0ohYnkU!{2M?BaN4(vF4z%Mu@kbVPpa5hq-y7QiTo1TTGr@QImiNF0 z;93lf)79`S&hE1DFA0b9EHGz70zN}uy`2x{-?#=-o5BBc`(04~u`h@=Addz4*F(Gs z5FXlq#=oTeKawcQ4rGY)>a6SuVU7uL?rsk10N8^cA%o?(U{|4E*1-n6RRq@&_!|Mp z1i+eZ#~yHTkDo0-dNAzU#Wws$FRa58s1?`__&~b&o93$w4Xv0I@sVgJ>dOuKzIA%xSp2=P{uhq)S;eUC_{iCq;(R|UHLzPu&RKbX8V`M zyANkVpxmJT;(Nh&dSC<4R>0hV>LEyDa50>n0Q&S(X&yvv0l8!Q+XnA%cU)nC_e>d~ zJ-|Ji3Mhw3)Q3Hy58HsQJ*2*nPIvbT)IiuVm~U^r@Jy&^S_taE6p-VO?9(ZMG?u~m zQ0f7siR%qN0Sz_)Y+t%V1KKH9 zoCkpUn!xbLRB z{lIU9!!;u+U^%4AI5!Obvs{oae)j{nCwBj9IiUX#)PMe-%b)Qcp(Lb31AHs}Z{14( z+2eX5%jN$&BV^Mi;#w@~K!0%e1G>9U@LTd{-oteR&(1R=S?d=t&*cCcU;(_wcJy1k zW%b^3kOQ9k(IeJ&jRE+97VLv|H}8Eg{^RcL^&c66?`?IS6QK%ogN!{oKdJ*bzl`V1 zqF%AYb8Pp!*3ogS$2_;AyFCA1IA}vUrlW2#-U(ufA_AlR2i?KTaa z|4eX{70&5^i#mXI;OjkF%(~qj7v_sqodJZ$`K;N0=&Rwp83}mzGv3)@>I3SL7s|gU z^FoF&7d(nu3v>GI+gXtRIS7m6#(zejJ;=2PzNvtA0P3s^$Sx7U%6_3Q^#bMZ(kXux zmMFpcX+o{Rb~AwmUNhzVJr~DqJ_aBQ)B#p6BbY<7pjP4jutXMUIuBugDfu(`($yyv z279m;WQhARzm#ov{^R~Z_s;KXXfc!RmJ4!+z1gj}_8P_lufHdE=6yWdVMZ~(^MnwV?1SGI!}(@bF0{|cGk_bQ zyYqcaIe*W^ar<~o7xsCwLJlJ=>Lk#`1M&9*zL&?>_m4t*!Pk@ahGhc(q6nx1xQ`#& z131rxyaRLq=6$YR{Gma zzJKjv+mCC7>^~@fIf!2f_&WXX`J-`7`d6<1U+M?W7vF?&Vprb~&+f%DMX;auJw3qh zfy#p2_%fMp{Wqr8b-l0IZU+3WWP#`3lEr<9uM1$bE8QaCt3X|Ghk^SF@U1+)z6axt z4li7P#JmD9J;1YA6hO9~;9dfJYaJQiBQ@=b{E=T+Z@_+HpKBHH9M|){=5crY zZ$S<&c#c<3>mkYy`;CylGoY!PbbJK5r$ShQQ7=Cupr^Wt?*+m4UU4rGtO2V|03-m4 z0L=GHVGfDB>J?1{`;k4$2G?!j-5ep{C5{DHeP0{j=UWEy=SDg7^uo9RY&+rs-O)J= zQw2N^TIFQNqc0DH{Ik)Q`T;3mL*z8_f=#Q9SI&fVi$Pzm7A z<^&n%I70a85buZkUnoO>G=P=4|C^w9xNq#2k>k%I6lD!E$Mb_k;J-Ya+rYu<81QRa zPzS&kumMj808fJf*8r~p*e;+=hBF)KF9B4LyAOmXgWbUQyT49~CBGr{Bg6JXnl_Mj z9iY4Qe>dcf?-8+-Uti!q<^b>?>mu#}lmd4IxDLQ)C(sK!_&)?(c=w|9r}eoZJzO*9 zguD^~-IYDsAI7_YJ?(S+F&F-sr&yPuKPCYDkc0odeqHlta0%py`Zf?y3h1u<(GD2` zeg+A>CJmH7jLYF2XU3QuZ7{wc1!Hsuk9rNAKZ_77FN_;d&vEXcyZgRSN6tcAJX7Ll zkj)VzJmUG@7?dzT}BRtvs|D|2<*eNQulF> zxHp~!@o$qqo^OLZfpU!l_Z@&~4?n{H2LRY_+c6(p$nn{k$*_)4S~= zt`8bf>ygemKr<_Se$yGf0cSyf$l$`c znLqYUMtA9DH5|@2;oc*VJ=(Bhz#ot{IMgtn2fe!*(qze;$lA2271@8aaJ$RF%O z;W^skfL>QzGwK`WSYHw7Jj-I)P!}=*zwCN{cLjp|0L9KaG8@W^^DbZ4gFo`adVa?y z&>tbxquz2s8K7^2?-$Z>UST)j&*m7vF5@fE>2avnnAX4j>KY4*LRqr_U-RP6{J1s} z0k&2c+mnC#!uJEQO@nga9Pcgw_F?|43|~Lr20Y>Ejdty?;IARrfUbVPSm4!*9`FnL z1Re3vACSiOwkLaXenz=akAZefN4_)2(>e$Jgzw^VohZ1Uv!!nXZ28Iio)dbPFRN z{)-p(1-p2Ob?8wK`G~x&1szBRJ;FUU9Pt0Av(ueQCE&aq%t!G+`ePuU!+@UdD?ys` zAsu`t5Yp_OXFvaRCVnHqPCMEG`?Wi8JkY~4lo|C8>r**k69Dyq7x2UVX{_%?ARnlw zxOQa*z&RS+pYg3a-Q9cTkd7suCI4To`(LU8w4*pDfb(8H09N#9jjCVIk=Li7z41Ap*tNu5T-W=$!;5$m+rQyH! zptCQ~j&&>?c#Ly?tn&3+;V~UtTfn)MRgm^X0KUg54}f{3cHEN<=d7U1m{(E+Kc3Yx z3E&GrnPdCj1o&3^tloomioP877;vJ__g%l|0Ms|M1Gx4X1$_EhI>3|>+6A;NINrPm z$OBvioCDco{~gyHiUBVH*sk}aKhMnTTP~jSz8dQNFZ(^v-%IPS@!@$F@Xa;cvx$2I z>H**4<*#<{HI!!w*tq}99M6wvN0%MIws$GWAM4|*3#ScKo77F_p|#1U)Ix~`5(`5 z-Uf85sx!uT|E_myvx$&;OZ-kKf_Id8od%ns0LX*Sl#5_0|}^-3#>?)|}~VObmlQdn`4I zFq3-y*DF*X#eE#;<3Jw=`Z&0DllK&!ua>irA=OR!#{huigfYLykpEG3q4fw4D1dLk#*$?DE zR*-2|eh?M@!Cn8(8*QB-Kl__HQx0Gf*wo1@3e#WPNm)6QBek7>x*W{e1QYHG_SsJl z=qeDUE90iF0#TTReeJ*2NnZdwFaOL8Iz0eH6~IRCQ0RQj@Iw(gnEb$JSVU&|zz;?C zr+1PG_nH2#{J;;)F~R$c>$AU$uHXFrzkAMP5U>a0E6@YFGWgBkN%U{=J2U*v-M zci#H!FYoks$pa*&z_`)TDL)W&XFgr>{4DscijKB|A^0u_{gBz`U??$$pv!^9jH}Cn zP?&y3^+OSwbUp{aKf~g5`56*K7QtP{6@VFl8SL^xOrQ|O)^&jeG=bos{ZKXVVo-rW zx-2MzO7w%Y@cL{tATC}C_zW)~2rm4B7vI|oS7^3&4^870BpDV)RJjwhl(t9ZRT^x0Gu~~X zUyxI9Re%$v?0t%aStR**yJ?DTL7DAhf8%VnRHf9y^ZKv$4?j)S3=oN~a-Sn2RzA$9 zgpFgDM)fm_2t_1F{*eAemo1~SO$B0z#{(X|e}3IG)zYefm^veNfY~s@LGd+H3o--U zC8lnpEjg5yqYyRzO;E-**Rd7i6zUOV`%3ZcRWtZ}5 z?fMJK57(U9a>n%GbdJ_=2f~!`C+qIBZRee7d9qHup+586v+DuMLTowGsa1NL6Zaq7 z`&eD7XoQ}}xdXhJgac6voy zpi9;Tt4U(<3EFv%=8{_VCS-$Q96q}Q8Vwbw6PNKS=CLWAZJ@hJ%Ef zoD=7(_Me)6;DY3$U7aaE$!UW@_hG1(cM!gKX$To%9va(ZaThX za1H;|<*Bl}ZIi1-*4r1H2*21Kowoa$>k;ke&JwQ4hvx>wCVN3h-thM=le9~$IodM} z)t!^}DGN=nENZWOf79;txni!k1kHg^Ug2AJC>3*KuNb{`=kU|ES4&n|Kh&}E%{+q# zZW^D~9^R~~YpV<;5Z;ku6(KACLX7|8PSRnk8-q!j0<(EWO}j$Ta>+IBcV2xDdqJBG z$!IS3?S`yjXK$rQO%L{)mQb%3Svf!TjpLx2w;A&eXiOwdPJG|C-&tyAi7 zkL}||1YH_o-8@Vy>|)C*uMz!U?utEWDUozxw`)lA!!31hj&Cs;P)iRupD}O6#c<_= zqi;%#dYTh9LXJm|9g+*b-S&#TVzX!Ad%c#BZO=*T3a@jPi>2ns@a)M?BJCrvHOCXL z`h+-t;3*4US7tj>PN~#=*o}P)Jy)haF^uBdY{(%zD6h?m-Dmeg>88Duk^2VZM3Ts< z{Y%nm^UX#E+!ii+J|}Xl`6zRdGUeeyGi)bEx$)bNeZC;wz-@bm`iX6gAwDUu_ICIi zYzYo6ZjDb+mrNps$M(C`k$kk7eOqite2(ShlVuS@vB=?Gy{~> zMl@eA_gH%-wM^|ieJ_#Ei1>u}3BS(1#=T|IPn#Vy$B&aaNe|$sdIZfTtUXO>%ILSa z|0CV1ccJyZ`d7yB7;@-`jD40po&V#^lv;O+nbi$;b_&V-NWaF-sdq^Gv+pd)zr#Tr zTsZPd>Qc@DvWuo9gqC^k%)6LpH(T@YX0q;$n3zy=xuN`}t()1F5cZOFCUWZ#){~y_ z&o>U4;zGu><`@gQ7q2 z_z!fXs#_)7RXRns9oQLqYWJ%{J2vGQp(9A7NEZ>KZQ+H;hh5wnHkE^F0)kbgbu zjTq<3DYNI_1TMHJ`isspc(}GDN3Ghza>=X&Y6WxFkHBFy`ZU@#VhaN zY*EAD%C(B##BDQf3hdo@=z!caamxDR%S)xBPH6K~rbhZ*Rv>P&qNUYp(6(``)3)?D zyQpp3&APmg?sIjk4DH8&QJypMGRj^x3 zIL$fMnRl&({pzQ4oU1$=E>0~TG;wcrk#5lX2%5}3pO8Ju{#tQ<7gA@PD?XjEZC=VU zUKbOMD%;VqEjlk0_|`5bDH|!cUK(tA>nJoAYAucJ$xCh&M)q+H|hQ`qXiLU+c^ zYZGc~KMi%Cop<&e-Dd6dk1{|+tZwtvac{gr45|!-TFWLI`k2RZjlOv;;YRGIi7xTc zJJ+o)w2tEr*3+9_E?Rzrq9h@wkStJFs!=^={hKRRde>$o=3 zB)(X~x_v1?i}{N5#{WP5QmPVD$F-j$*C@kJyYS-#c^rCE@hGwCA^lYYtPg zx5_#fJm}vzA!yONXO2S*IkL7bSkF0q{JkRo(_>>jw<>cFeBfQ!bXQ)cSZK9HS*hsC zR*zhDN7F5<{M8Lc-JwYU39j7bcI&?zb;7cx=HL?zO&K=FO4=D*MUq>;G!*%{ioP4(BvZz7cP} zGot0-$HV6e7fm6N4Q#j6nPgb*3Hqq+Q}RhOZoi~+0OUk_w8lNYNWe`q$ErYDLgr%) zu~gkG)V#uq99z7>O*4LuON6olDftlXY;_KA(j?tW1SnOE{Uh@nS?|O!zmZ#;S1Irf zoJLsaJKoARM=L^hk9=rgt8UeJ7i*4CIlh^kI}UR)GNKe0nTYM`xOUYz`Em=PMohBd ztZkwXHQIBWQ$M@(5RO|P6W_Jc@8)hR`Fb>mOQ(0wv?Nm`;5bBt?U$r<6YS4$%{ zu2@1icOZoRiJzLa`OQ)GA%}%xcDu2))o8Eq;s}+^q&;4{uVG_zd|YzJ04uFs$32^F z7%SwRIWuR!-&5gT9lVWf{Uwsw*2wtqI_{^*1kX}guud*-PW<(qoW~Cfr8iHXMJ#=3 z{PtMz{fN0^3cUJP?-a~9?;YbnxbW=MDtU96{>QiIxt0}cvkzsn)jIB2utD+!%_T)Q z{$aUTqs$^tYi|KP@sx^5)>Su1CTgX{i^2#m1C91JZ{NSE#GBV;m>W-4Vm$k<6JhkR zfwMQP3gilC4ctH}3VO$RXxauVl`BM#S*9^2^5#n<-#!eQEz=P5GI%!MakW?HYP=`J zNh;p*eqlTJRMa-jmYbhA+9?A%UKh8t@C82Bt(qNaH2ZQ{MOtxoS!Sf7zY)b-sMS4P zjlA5Ra{$MYuu&N+*AzPVOW!7yaC~SSI6YXF38i>pJR_!ME+x`|xTPpUSvrRx{v5dAsj1FtTr_P(=n zO3=ws=TAjbR#N&0CP;;im#v*pcy8YR91%W45O0SZnObmY? z(HK0Nvn8A=`Se0tt?Rkr8>g>&HlN(U=OQ?8Ix$GT%+z_1=0#3JJ{R@sRaO}*#ubVV zuW%{ow@lIgPOjKo+1Kq9p`umc`24Iu&cbw=c1mPe_|&>n3yf<=x=to+yeX&H`rNf6 zH+Am^YR1b}(rwbRw+R|&p6&>E>mxK$+R&*$MR)#1uIHq^YfEz2!mbUr8M#cY)_2Dtf;-W0m8JLPVMOD(0S?rW57d+RWQq6KT$N4o zPt$o7#j8WI5|*Dk_l<%b`~wY-;Xd^b>F&|TNPd@a6(4NoQA ziIZchPOqAukTNI2-%+62$9%_Y&C}~j>e+N(<;yA1Qle6K8*I7L&!^uqqnO9nHa~V9 zxO&D-A-|wCrdp2^Jl1n=T%DXcOxR)jYV%PlA(?5}z@79tpFMB}# zLV-!!*ch=ukJQ!u8|w*r9s`NhH&Z6&RH`1_IgvPuyiC%*XjA)~C~ET3tfNyaLk&8H zHKv4_oGX?!cFZ59E5*K8g|~j=o>Lc6PjJ$jC+}6G%0q)ET=b+^e%?pE;V$)|8WGht zF%M;)>YYg*P)upx>7ikAw=n5s$%6Hg<82oQf6TTh&<^AoW0b35rgum9B>Rf;t(14r zvm0W(MwB;XAtfg)QJkPZ#9DvioLPk@o^HHA;upEKVU@VS^vhPnDjoCLTuB63O7z@Y zDIa+5Om)kvPf%UE@sg!`hc~ItVpH*vJ5q1CN>+RM+fL{5B{e=UO_WrBRvuqYrsye2 zo;bwjBT(z&bi@p*l+cdHkEXxeR1xEH!_fStQ{|?47pIBrO1@yDFXD6a+Nk(O+4J?8 zb7J?Zy=&et~&cEUfz7%$SQODsZ z;*sNtf@A9T4i>+qVg5e)-KoJ0nnMB-YRYWX+zL#GlQHBZ0zlxmP^Q%74~C?h!cw}CO>#~f1rTZ zJvHgMYa6^4`Mqh&$b7po=sgcGbqC)&&cqG%v&xrBHXAMzZ>_SJJ}*|n>b7R?6=8Xm zYWMv!BTsBo($BlH{;J9%%kxpI+yXTyyK9dthAE9!AG*N#aK8uFYRJ$`BaQKorp75H zxfUD@ugEhY$X+x_(atik&Qh{Yq+J|Q@AXh|uAi9+yXu?3D4$^Em)fHX$D4|XPoFsX z?L3-@Ax(Wzy+gfd^%26z)N=)brlHGx_ths5YW#S|lyJ`6cGP|Ha;<}6+nrUi@4co( zkou`AQ*P`RX>6y^Me|;$kCWOJanSej2THY6sFX^zqoTx0(k_lHxf8sRQs&OZS1zSR ztv-?GJ9oh_6KE$-&$S0oZf~E^I5xCuZcX-ahtWo( zZ8FE{5tkR3R<>F$ihc}3c*PTZo9{Y0+L}DHdU|iYUT&L=;ij}tQ9|4;87VQ%H6jM% z*Ug@jb#%hmfL-y#0ffU=h57;m8!cy<(7Xl;#7ao*Od!Z+5&}Fn?BS2uzuolO&M`Mr zbXE-4*V_ARt@!k9_k<`{D#Vh<`%Yildc{gHBGkP2%x(9iRga|NSNXckTr}#cpYZ(L z!Y9Si2M8~C?Da;i=@%OzsXi-cYP!{n8(grjX37bxTgt!Xo?|RH`Kv9>?cOq{hyk|LDbp zpovGD%GZSw=Lho_D_Zg@2wfO{$yTWUCzETQ``n}hZM1dvh~<~6IFzN+`iTo3d{SMg zTWuONF?IRa#Rm(oSBlP-Y|B`ezFKtNyS!r-uM6Ws2LboA`8My?KOc2&Qml}u#F>3k zyvA&9alY*G7QP*u(#lPR4m%7U$l)?@OI_=UEsJa(58jrrtXyO_0V-+!0!!{NE}vQ`@B$iI(Mrj}b|sJu6B*+8yuoy0$< zUxCm)wQT;82{Fk5H%;RVxD#~9&IM-=1!Tx2>FF=h4Ol$h>lEohT*56O`5jSfJO+mN z>3N3vlS1fg!O$^;dGW1#>xc*j!wP6_Tt!+`2MZsR#7mF5?rk1No z2bbg-?+B{sKT^rg$I+ww?75r?cKngbT)9K7+TNdhLJHkVTCilH`=+S9fq`?!+@#0I zpP+My@7Jz)$?5uLT(;NMJK20guB9*Qm!T^8fxPfagJeytJ~ib<&HHw7J5KK$&rxqZ zcZ@O%i)4=?PBD8Xp;Xm6_SGH_v%n!ir95q=t|Q{>4Xi5z7N~em`EWg>-~5rU-oGJ# zvYE6!jzE_wH8YtoJKA;T-LydEorU$+^%sd#Do2kDUA8E^Sub^n#~Mx^_Jn|r+2xyg zwZ(bj-m#?yoZ)<{n_*3CWXn-7pBCd5Z*N|kwKCU1T-=3Fl32oiX0D?~!2S*Me72k* zw`ofZH}O~#?n+Z&Td!4pE8hF*qbUXn*PP<+P-BZZX53gZ%XTuGiLM9r6ZhKHg=Y$7 zt_x4miPm;bf1tcGFPp?KFo-wOqv(!E`K$x9RGm#@WvT`1jtCB%rI{aZ5~bm;EI72kH%ycfrW_{RPI68S9x*XN@6vVG zQ5GA-)}5Z4o$6edwRC}d{rw4zM`x^QahsZKlyN^dG~|3S=~hb;r_Te875;_wj+GCL z?{zGV)v?+^f2_YXQH!j7NH_MCrdm0BsR*Pz^~QqNniKhBk1klDd1Rj1(z>jd^SDif zjI1MTEpIHh(z`QY`l7utY5u3oN7)8tzZT!FP~n#ydudYP%KBk9M~c1Otzi(EsJxOr zd4JkblWlPpi3g?-ig>N_g^Rb;joMGssFbVz7K0L+ptAvl+vhYu|Zc?F6CpNmArTHHhHU$K}%LdrTZUHPD!u-)RCTQGPER8 z{QX143FlME=M0KlZ#11-eb>}>&55XvWb-2#2DX!}16Rv59+fw%FeaXH3EoaPQ?StEC!GjCy9FbNoQ|yzyGQeAnG5Ik!fz_`^K& z^)3TzCcD|&jM=cUZAk6~ZqE1Y)=rPy`ZcH*S{$|&A0zsp|I-G_fsB{ub*JoM2tQ2L zylt4qisj^MlHR9M6?C5a9gHe_P#SkYJh(l@`3-64b*Y8kw{(f6&5~XMcO!;OHrlgn zUcjef;fBPM118+c7m6XLMprxwx*f5Q-(0>X{nA`T@*IlYJYJWT;xGNPHch0D-_h}o z)9=&f@g}Xe%pOS}S+u{y!Qa9raUECvf&1(}+FbjZS8r$ta27lD=FzsWHvt-zP5qUs zKA0abyKYxHsi?)Y(BUajGBRmmRG>Yt(2%=w#ivh`jUV>2v@k4`FPP*L60|)}{Beh7 zr0=<)<3|Yt#^leHl2oH7Pr98#SRi?G@a9_Cf^(v?E?gCp5P#S~;0c`VGNd-ke95o{ z@{PkOdtc?2B`ErnB=^_xEER6Nm>Bwsr*5`h$(q@3RIF^9IS#0a`|y2`T|Dh#p=;@c z7eoC=s(3fBxj8A2G(6TruHp2#s#4;j zZ|3yA>B49`qee$F+sNgKnG#boZdD)Q<YKP2 zs4Qv7anqe`bdD<^lZ)P8a#8-ByplDJUTtf}CQQ)LsHZfnC^*j+=fQi*p>R+1s?iEV zyzPedue{7F@Q^t3oYBY^r`1|48mkoEN2Tv9ko6CtUY*x6#(T(hg|vkyj}57#z1bGC zmXSSM^~cdSM-F){*KZg(c>SK_icJpIH_rLruCvk$R8cFwJ+lAZiKeBN;&cVRjfVz2 z?{``J^jw>EiPX(98{Ot>i)MzdCz|=kDm9t$6Yj$4$pnsfLp+tB)* z?3)H{DRQbjt#*F=ro*4e#_zVpdh#h!RB~;mRnjNBoPEhL%HguJZd~-t#TLF%MS_#Z zDZCK7+J2z%P~MY0npX6u$@iQHgZLtSh91aYMy%WF{%CxDYMIkOk9t1=e#6W%eOMRJ zcrG1tBYb$$%vfKObD42E-siO^EhLKPFB5+w#8cZb|5$>4+q-nxX-cPalLYQ z1;w>CE0en=Ix$Sfu5$AP?=TO6pz+5@wRKtU+BT7E_DvxEpaHeVfwHwm36dNAt zDPvxVQ397o@1b2L)XcVe^-4%Hn{@Gbt)YOp7bQpZM4V`&y4buTw(acJ_9L~fB=~9% zdAit5(^;!};d6Q0*fRH(MSF*c9!!3yH_3yzrB=lIfO6*5;nAslzHe=(y^%V6HAp_% z*rH)jz{JZ}pWA-OQV90RUa`?g+Ow}EU9EVBn#G9H%qZOv>tQb(YV*!!2 z`TRb=BM}`LneW242kV%-yQ$){Du1-0>nB+8`J#s?+a2P#eDTibr?g;3_+^8DMDyEyDF?+!7U z5Nr6fj#%4Z(9sfcUh|daNY}9qgLp*hxb+5=e6rhaQ@GRA!M@CQb;fw&OhdW?f3dZR zgp}L^LlU3S+mwYGUJsHIkiLlMwpXdz!iHs6)+g)>HG6W1bG@Kz(fXD#*TpHLhbPJI zNm4$x!y~A)#Qfd)W0Q|_AK4uTOHdOUgJk{A+txbgPOEMpJ64_{&YqIg5i?qWKpU%g zx@1vcCP((3i1k%xGWG}7-rhdcUvp}%Lq>k;+#5c-17;4E8_)TUaJnf(PFf&%gV(rK z`VOrZ{n=)Xj~%G~!0zI>@_pl@4rUop=&{tPc_2{-f}~l&c1lRoxV!$cV_#l>ztJ(c zb)r|A+y)t;T~5)S_fKiq2<*<-w>I5fhj?A`72D9QbqQPZvqBJzrhf0`3QU_E(j?x7;L@8t-(q(7`rp@pkrvH6>i_;#Ko(wRPsL zo#Sye)tzVUZsi9HC-18;{W#H{Pk&tOgAIu(3AIZl8{48nhd^r_pFDrjq3xe!mJB*7 zno=$s+;K8)r$V*;%`?87#kzy#9Y!K43t zypQuqTFnsNpz8uu3wLo3fq^-^`ehDo6$3Zy8GPoHy73F8Jtk$NcYk!deXOBWt@=*j zZtdZh%$HQByvh zDKkj0khiI$!IFQ~0ox`A=sUg`<_}>GSY*wdDnvbeYNlxQoiqAQ7fz(fE=vn*4^CaGN?bTK_D##a z_E{z?_j`Js9+okh=os?+;|rf#n9o`gWxSuo_@Hb2E`14&A8 zjEMgh<*?kL>_!QpNp!H;3o^<=5{0JjD}E+upSUpA)}7}-#Y$6HT=h^M`R1woGhNPX z*#(xCNvA0OEg^TBHJc{96WVV_kfbUJA}QWm2)_bsMSl5C9W6(@#{CwIchZS$-k;ZYGPdJDSzC-KM=H0HL13b*21oL3(MEQj{zmO?B8`*HZ(B`{ zS!`E%k5Kc0SarUN>(TTzlUCRU+uu)COLgZjI6!;MZY(CXwQ&T|@#bM-X}^H=IUk;7 z{`XAm39l1syt7&MkhTny=z@%Whb(T z%WnKyiPQ0(E2ZfsS&=pG(=T}j`>iss;7xTt;qAHWZqsbSM#-X`8FYU!fvDZ;2Q4R= zXEqAR<;91hH(4b)c5kn&!Bi65Iw10fm(n%-a<(QjX26N@xiuRr#w7_!C zw6Zj1iHWA^V-(ej9IxoSIIia0ni1{2hJGe~7pEL^rTa^SpFJ zx9X|!z1c73SX5SpiE9L0@g8)va8H`q^GSpu@}~#pPcDDnIDN!^0aFEQoA9TK)p7a9 zkBp4i!NcpA5z%y=y4YH}DL8MYOJlRi;Jadzz05YZlb3VU?oHj)e_phfci!N!#mdj) zP7;*kNZ9N2gzML|%*QFtjd)11bDTRcMJH~}w16DP*{7D| z8n&()SHWA}p6Qp!c1kSf?4!oDB(b>gWsfBlBEx1WW+~g7t-9I3xz2e-v#4bH61(Ni zgzFpIbaU4|SCekvr91=|8bhjf3=o}05T24hutZ?F-zDWRE~x=K=$~?{9Ix))w&O$U z8M0dLMB&EwYMjZ3CZswC!5RdAki2A(u&u^S`>XUErP4OGm!%#S0!3M+eo7L&ietjf zi_MHIVlHdTXtZp;9vg9M`Meu$$JsUN*SSn^4Z4^#Kq!0tpbylb1l1iIWlW9JlZD6R zOKwm|pj|YJJ$Pcv$fx`1D<;+PYiMvj6;?J+k9n9@MKe=(sF-&&s$|1~6~W5WRCW0R zQqSC0E$@0Igk#HfLW%G%2(Gxj4!>QldTRHtF zr4z)>hLPUPm2r)_Tv<8sTtCg{_NpfeQ=K{1#*62rmaX5g$VZXm)+F^~H4Ige1LbqQ`G9?f1|^D=;_W3V&Zdh8?@x!Q&0z6Fs1JE^Oz-|SY=+Opc;YJ*Vu zvZuMuZmX6XESz@L@MeUm?haq0j^hdYZFF_C=W*vu%{3AB=`S()Drfeo(E3c>!t9KB zPOfj3E%(tTei$PEEPq{-?M8}gxnz3$dTGo2?ai$dwZtjTRTnqz=G7)9Wot-$)~4AtqbWl%UF-ZS=7MT=BuV(PN=JZO(iz2yu~XSwZGR?vKQ^camR z;^>vd_65$oEf1Hhc$4fY{d(FNKWe(qiPgev1za$K7NVJOEbf0%KJ@((las1768+s) z%;6YY+HxVl@w@|fO9QNaUkFR`%Xo1%BeRVJ0~-AWd&71#h&QCj>IZ|^ zA8`5j-Eb&ST-kncTEj(IxA`S6Oa_-&OC)nmPp=Iyd&y>P`hcx?S7TkQ3}0#}!E6|R z%&fG5nuM652ZKD7Yi(dzCxJuvn!$xy$7UYEmZ##yqoiC*(`aOv#ixr?oyvtc+n=$Y zHoCO&*r7#MM;h*&9=t%$;X{7Z<+8vst|o2L#Z&#=d|xf|D;{32HP%xnfbS(eILJoX zqSwQLd*aVm5xj`YjwoLf{c!V9e9ggrjsvR8OqamZ z@iC{HUq97rr#GImmX^*KMohw)slZVMf-&x<{rHR)#pZGEv>Uv*e_8B+NnRY`Aw0wcjnWgm z4i!>ko_R;gav3Ey`mWBq9`9Uob{3_r>h#BE$$_Vw4)D}@ve|G7Z_e7X`$?JRN^_xw zk8M}=FFp1W#wzzFUA}VURceQb>m&ljr+k8TOQw;}qG!t`)tdw_4dd5hx1Kyrzs`~K zTCL)gX@mf)4O@LmR?nz>B=uq)$w#i>y-nq_Ylki?^A~&DuS-;xGu_sjyxK-gA2ueX z>BqjS*I=LZT5QyolQ%uox1!y&ZK@rRqbd~!?pe5W~@TCR5E!f0-JN!)8k&=zgD^6*6Av;ORUa<$9WSQj4p+>Q!rnbp*1MHbl+wcce+CCaAD8EHNrX%LdbF_AnjY~B_%9fcdBzP_Gw zrh81kyr%xjCg?Z|-{XE{cU57Jy?$}pzKNoVqU94fqU|abl@~7cU-dqKvT0shg_!Ow zD_i3a8BXSc9m~`b>Xtf$Uzj&xvsqbxmm|X#cpk4hunQKhE`^95ILGgksr)?rJmJ3B z7tFgctx z7#`}v*seB<%c-(I?+I;vH$t1NW6Jx;#pf-vNsjjncFkYIx#@qcoQprx-yg@fF|ugN zHkVv7mzev?Epo|5C>q*?&2%GCa>=FK8d(x4m)x3-klPlLYq?)izN6Usb|ch64??x( z_WS%EzklKP2b}Xb=RD5k^?tpd@8e=e>N6zGj-$7>#TqEe3sjwJ5A|xk2E@VUmR}~_CV^_|G=M2k!(iDUumE&^I{=P=X)xH}?wRWc< z2F;X7-bcjxwF#TbxgR%n#L?`ReoLK-z1PV7ombro33=4Yb-THogZ*?IcY%?6+K#(4 zK@e5r+fYyYRPw!4luvp)%goUr9c;{s8AgGO;k?z@Fvk>hmX#N^FgTC_SD2)3J*)t?D97Ua|a#gP!HZ}h`w4mox{%kWQ(42T_f^)SiQ)z@&f zXk#qycX(ywOkEWlkr7RRX3Vw|JaU1nC3Z&AwbGh>#x^*c4Ji=s(}9VsXbA=y)8pXR z((g4{1*!O1oe|W$J7*{m8EY_H8=Fv(X!hNzDAWBu{Ak3&(TK za&>GY&WBz~?Q)RLdA_%|vnR02S+n;OX96yj&o#)dhO$n}-9mHRxW0&l67`Us%M!%$ z78^2fMaeWD-B-a(iLUPNkh4hBQNms@i{(e>FK^G@iYiLnp@;%Hs??>O9}zMLLh)gX zs;js(+-pwaMQ-9G!Oy>kr=|Ot*!a|t!JcNKEced7R?4MbJnGYIFOvT4f^79U8S>P> zW_*A{0LfZHlLycROBgSVT&TM)7(jcA?62rDT zxL-xiq>`bAEudHqA|ZRliL`pc**ZWW z7a5F8uC1O9K)|a^gF1Wo-PP@BFlE-5qivGFhQVL`Ncm!x2vvLzE3J!PKovkX=<^w;$#|*{-3#-;lz7(NC%ath)OXpeYXaQ>Elip9&N7C5th2!Gy$S zbJuxNuWhVjErkCvrw3*iu}>a=!f}L%Oy)Ne+E!rZN+?)6rep3w`P>y_2pjaik#!D+ zI$%7y@HaK>use5emETNuwjH~aC*rU2j72C0H*^bO@&!m)TefkO;l65964?5mde6ff6;y@+is%x(IOQNL zt{(rXW=OY1r{~9a`86Qq^WnBbRl>d|L`@;ORJj2DP?;w^Ex>+y;XO;HA;X>8&;qUW zGNDPBB=?8g#(a-%QYWC;V$ zFKw+WDK?O!^QcU`$z@`U452q;TGXTjafgXWv@K#b^v13h(Z<9b0PJxFWEd^3OLHm; zw(XQXlT2_PF%#F}5T@+8wo-A|=&^2HmVa(axq$&%DfCB5a8=n`1!|_}tbS@E!ZJ^1 zf#WmjlYIP!jZ)N?u|#3Yi1pLW_=atSAZ*JPfj1+Ws$OG z313h8CQjD5E5DYY*531m^G~Q~8W@ZTfLo1r+wU*x6ot?&aoHDOfRuV$rTM2D$4hlV z{?HdA<8tY0lJU4~CvkF~x?ld7vA0EKn@@q|ZWfrr5)&K@avzS-D)aeii2Hxl{QR$SC}|sBR)4XPFAh@xs+mB}csE@A5$cWq0B-FI AKmY&$ literal 0 HcmV?d00001 diff --git a/crates/tauri/test/fixture/isolation/src-tauri/icons/icon.ico~dev b/crates/tauri/test/fixture/isolation/src-tauri/icons/icon.ico~dev new file mode 100644 index 0000000000000000000000000000000000000000..db7fd98204424b6b9b02fa06ad18f05c089f93b5 GIT binary patch literal 57594 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 ny0Oq8K3_V5(4c+dzp8=O zXDSf5=*|HsK$Joq)+S^d7_(hP8U)RpmoE*bdL*MnGtz}QOpVTElg07(3lt#r!!#82 zwE{sK)hRieNg9a}aGz+L@178>T7C9%V6E>$6H7)Fn4^FqG;-vH!;yakN8cJpN($s14}DfZszw!(5&k%S6M!1nV?g$} z=go?t4(+mj%u7Sc*R>UIPnzc?3E|0%_E+KyNFZ`NKl70Dt+WADYjTE-=_2?iJ!A%3B5qYZaeUyq@uwQr|iP|0!Y! zPhbFXKj3nj_M?wc1YmVsd%Qy#!z=9*Vmv8_3x^T)g$e@UC&0{gNf{`IObe?nmaQk5{k(ZSa+pc1*Z$hQi`!0F54S?B z&&fVKx69_0ik?LD0t9AE)v_ZIhv>wAc7Y?TwL74eD=s`sb@ZyHaV7C)_CPD`6YqPM zLUa{Y7eJX=k-7~@_@pd>6BhHH#X;wd<3vtsh|Ar+U?Z6(Y=S}qCqSwYg#rTzSotNx zhL*qZ1@>#&8YmVO<3+KOzXn^IlrEHh`WU(eJz0u5&`xm2&i1;a*?>9fG53<|qechpa>DyWDW6C%*9F;DFV4cM)Ue}_F#7MUVLpHvQ<*euN zW^NNFlfw|AC=5J%hXW^vH#H;)MXEM(G2SF4(v%&Or-hC_#%{FL$lTA&S`K@XJy1M& z^FF8b(jgg}UpSV_D7G+RE*z4MOC_%GDgP3q#p+fUT{8=%yLjnMuXsLLk|N}Aji2}p z?=+y?euqO)Es;So%?kGLnAo9d7Z{Y6r^j$I-+wB=gOUFBZ1iqpTsRDufrDHL_)?Jf zL!u~fx3i$6*3j1DjuifMwjz9I@;Kk6s=xJM>z7}?LEZ6omM%&Ey*+`xPr$K?LreFB z5STU3Nq1h(*`Ydhb9y*d>#a5RBMG#t(|sX*@*iuK2VHVP97(dKXNba`wuOqA?z_#} z%d4;c4%-JGt~1afHO5QukrAE}Bqb6UXVDNf_0a%Xw{M8Sls*-55l7=x=R_q-s0Vxh z6e!IiyU3mJ3fQ}!d5Md*j)BPts^3SURv#7ojY5$x$x=06;OyKq`4{Jt+4CyCy2#E& z<3JTY((e8>a@$7<7{H|j4a{BO;o{AkBBBGM-8RcNZRt3q{Qi4CE_>w2N0ZMk2~82-*tQl!W;YY3MZV2>0o} zx_8v_(rA7h#3>G)MfyZX5DUt&rcZGq-Pb&4!dzEl(Vsq3>#B(I4x_Qk`p>3|^WxUk zK1F{X>cX7NNGa>I5g5FYn~`PZ5FR!pWb!EMYDO3!$Lxmg)4GHq^m zfYNW=Z^$J-&M;Nm9z;hWGIQFjcL8J;O56Op_@j7WIY7CuK6JdKADl+N=@_HFw1uJx1np2J&$13FY84VuDsJWNfo!=k#kSB z7`%bV$ zrBuEtvAXACT7xtOT@6yhq<{8i9jPDTe>&T^#i0eI!=TY_l)S|22PW2jO)X|wS0o0s zKh;ImtH-n6ZHx~i!I9PLr_&aVZot1mD&UDZ;83w}G+}egp zo8mo)xTV6mpL}zje-ApnA3nF2!c}!A$aC}T*7Bqj=g;SScXJIDmyVvX3Y3t)*oD8X-!oy~M zj;9ik?W-m@C$_ZFQ)mf@1L%-{JUE)pKLVn~_~U9%b;oc97CtKteXbSo_Yxa$GFk|s zKefZyP@upK^1t}vc8STQD|qi;cdRPwL;MEeiG z;m|d)9&0L0Df&mC*ptNRwl0eClSx~LPJS{NSov^Jh|l7nM&5Dw;GM@8V8`$6pa_Rmpm>3VjzUS(y=_EFg`&X*rw*LJ4=676A zL2ZM(wNWFZx!}HE1<~3|XtYqB#;Rnl_a0cC7vI8RSte))Jb;@o|1tlO>B?uOmM;O` z4NEtNxK}?q@S)_hg=less~3 zZ-zy8tKvBBp%J%UT|^y@M2LW2E7O!zC1>xcnSTi$G{{v9vQlbDL97UBbXDfmFw3fd zrnn>dGUcQFsik_x?KkS%KvO*3l1e7o0|oK($qJ3WH!OtSOJ@rIl`lD?s?8Nnr|Ob$ zVNK^GE{uv6&kD{@YaWyioy%jW!(qB43=;af0*tHV>!T{oNBXmxo~Gt1md4ovGZ5rP zn#HAreUT|=DyZm`>MHtJTDP#-OWAl13-0#ZKDlv;U|Fq+QcOwxtu4P+@=AgIlgdBE z`1|yXCwXL8mKhIRw*o-Z+peAD(t5==QRPh6V_EL6h#Y`;#H4gMLBT^f3Z9-*kB^S( z&g)Ski<|rwS3GO8r1bRbGw~>d1t7TG7@3l|81?C^!{k!BnD1>c=%*4V%6nM@aW23( zR4nUA!I6hSv#0q(iUnCS8$LV#N$`e*AyHegl}7^(sh1?s4nv|F1df;pfP|-$5wVSW z%F6WQ^wWhB=M6sx^f>2T)+>VPkq8X=cUc%BC$)ZYzp~CH6L#e)UzyRJ(NY#{j1V25 zX`|LBUT^)qEt;7WoNV7VJnXWl*nmT+%K((32M9CO#vpE*HAX%(@yT<^{kga`jFk|p zou~nnWgvs&aU=|?i@g?Zr`o%pY^}0Oey_kb`s|y+cWw(+#C|u$$@}d{+}>t~ebX%x ztIPC8#Yo$$`pKSuFMgf;iO|D{O?Nl+-2ATOL{IAZh5r0b&s&EEr`bf}o?*dhD1wl8 zTOio*hi9{*OF>(U1gf+)*Fw?Um?VPo6ab!wKl@?*c4*0F@1zZVNg|{XQ*fOp9FV@l zLn!eJ`aF6_Lcdip(PH)^KmW$dw&$&42>N*msIa}6Pi#0^ zO=JVmM`Z-2azMY-4E`P* zh(x40Qa?SPX-W~$tF0(a|JB>s?QZ8j`-0Zk^?&<;0bl+jq zc>RF2LmGfbH5sU8C=vWeFP0PA?N3?92W+LHTNrNj@i1%eUrPYBd>lh9RLHw6+#%a~ z>dql{-(^vkIet5x0UeMgKt+WkFzX68y>=o9Z4*j~uweAM8608y@u>5~-pHVb1o;Vo zq6(m5vfUdOwF7R#I38sVE{sXPh*LAs3XdT#NReOF}o#v za6{uH^W&#cG2yxhR|Nq+KSs8#gKo`T4qu4aQ}5JdmOjb8&-~rMTk_!8eE$BMEce%c z^YqZg%*yTKZ1AD9$uB|_#7HBDBk?Er4S!#LSDyanfJ*a3Ulw0-^{8Pr36^IyCzzI^ z-su$Pd@c9Bx~#I(CD>h>U*kQaB|YYuD{O{4jqpd9*FR}>0oz&DE;e&l!rd@8I7_qA zltx3iN&B>M8M0IdAWOu&ZYf&cGwuBHTV&r#fxOKn=zc?rg*uK6as<&3idw?N8tOXt z*GHdyW63sOUmx6kgqN8hk?avxzd1}lV1Cav-&5aCu$-#1oFp1)OeVGJAU4559n*zl zCwuK%7?))o`YW(Qu|Wr=bRqywfDcLKon+$Z=d1@Q@2+OVyjOPlc7hE6q4a$Z&q}F$ zlSsjL;U4Mm*=y-z!7Ebb$8uh+=?+)gYCXABsZ)8L`~{qH{VeCVdDF= z>yd-TdNU<<)|$tCF<0aq8DX(_$l)^)nHTQ9x4qrh)1=aOt*uu>}!Rs}wnJ;VXZ?*IQft58LmmTrC6vkLV{r z#bx3L%zt$h`>V~0oU59eSa)YR~b5 zmH|>A4ker^L;zr8_%4VJqaL2y>*{i7Ieo?Hg2l3~Y!ycJH>X%CG8kd{hgE!0##z7b z+lS+upH|W|%MIMu-`9uP41JH}MALQIP{~4t&p$vHUXV-O|1NLUTaQ|=ZU5j3bbOci zVNq#ffVn;%q_Uvc-FS|l6RB_9Z7RA|v~-&7jMg3Fr#+ptEu^=(Ss=z_kfJ7;9L$3Lg!e)Tdfkm`(8hSkm}shQNx#lRi)qv z{G@^}5~NsNf$wD?uP^3m>u+Ww&&S(_4EA`{Do!3TjB$wnx9Tzwi>+nv3^Ka!-FM6= z7b8lpPkIsbr#pQ9B_ODUlr1_QY46xXAv`d6CibOud`!~qfyjgMd*Q8hHB64jsN%@* zK_sOPK*50cz>sn8cxj~LQm&)mZ656bzLU#LtmjHGRz`n@@pz9*#YZiAp6wsx# zwJ;5)SBwQ){IRc=n|mF`j|#v=+qeC9qmYI7MZT#oryHiFBy^>K6_HE;hWy!(I%Y%^ zsb!~UO*jW=UF1HwSlIq*`SCRK9m=2me?TFfAq)6ffgw!Z`Q4~yz0T#WU^ReK`RHwu zA6X3oOhz6Lh3LWH1wMiWC8TrLp|n0ON%+HmH%IjRr3dxAFDPnA@-SgQG}5y5$S9n( zdo?T6!y>r*FEtp^Ig^jbFxLVUaRP*xkO=tit*bpN3VTR za^J!kK9;up2yp{2n!=i9lQCO>fAp($QLG7hm~8~J-$new#} z)O2F5rrd2LzUix!1{b2gDnSa5 zcLA0U5^7b+K?BzgOuP*|C>sAeI9+kYQmE zV1P7$a0hrCF^=N)ddAvhX^lzUSP&KFG!BL$6Hum0tOSA_Gen@9Z8h9J`v7s@CmgFf z=&aPHl3)E*g`NHe8B26S?BRMq3Z_md-sn-#?1k4yLZ_@vS1(Agp#UF4)AZuN@{Hzf zRol`YM3WXRXdJU|~@_3iNEnM(Cek%y=eqQz6QkEz?%85RjTdfOV zeNZSv8jAF)e^jiG!Sylp@z7cSUw@1R3riAN5UB(=aEgE+9yU-w@x7I{vtlQ5{`0bG z4!@?;|M1t4kd9&kCwd@1&cwv#Z)$#j`S?jr*Zjw1_zeh6$I+^tr-K(MSGw=?n<(7Q&6(2zJUsUK z=0{npNWc@TCWEfdd+HeX`xD=5~zWyq-K;q_7?8S}=J`xik z4x_4($p;xWe?!w%n(zG?vqo){%{!x6i)o!2AuYI3R6P-51yO)39)B1}+_O*Lsm*rX zmgy__{^WvIi|xo%yX;1SF>8q{0k|hjgqx@=|68M zl;kAO-)|Oqh>5R%_gCKLDkJ3DnMWO&)nYsEo0EmnyPIC|8&bnBZkzq#!BY%jTM$Dn zF~e;EH$)%QhNd=0GH<@19km!c+(04*hrjhPs zEGy+y+yO=bC?E>(M6n_DjPh~*bjI9dpQv*CNc#NuB-+re4|;M=6IiN704K24QWu=) zDL>foT6OuQQ12>BI4qn{TC_a4^Ww~p{7U9vaUn3vJvL zFZJ#_h(XIa)Jp7K1s(xqhtJ|SaSlQ z-hqsN`*zLEGG(sF{YKL5)W;#2;}eBkI)?>hHYWHWZvRJ*ki1SgLn08>I%aS?3uxP0 zQRbM|`*n!wMh`;u7qVJS6 zsvWeN{Tfa(=Od^UzV18hZ*{+)r`6Bzzw;IDd%2~H2U{8^!opxb=#UyrA*$8)^Y0X! z`Q@4vuzqU6h<^qQiR^V49ElDcVMXPJ-(0xym*D-b(8DMpPi&DDq>66OvpNH)w|9x#N2m`!Ooyg=OU^_9snIgH>5|FRx z?Tf)hGK-zaK*Ip^mWNP{M*459G%G;Y>2uZrm$@@9Ib_S3)X4x1CNK!}2m!?ADqVlY zou_{FS$idz6%!wW<$3m;UYmBBC~J%fdxe8!@u*h~>_o&Dvp=lnher5GIo8*A{-jF# zYpo5xnzND)z53h5&EEe2N-{+Cix8s;wfy#a-T;Z>5Heb1fXDJ^vmtv8p&-zp5-w*G zO+GGmVDQNalX>ShH^-Y2qw~Di9o=*Yq>;xB1<( zC_q#^Bp4HahyWux20lEO(s|prIoRHBz)w)(guae z@@olS4bT+0?Gt3xa=b6rUhBCmCq-B*eyW<2z2KF~>>%rgbmvg@8D|hfI z+cbM()sI>w9tv)eKtkqplI|j6q0NVXWmarc(rZ1|dg#`Y<%5Ab+4v_R5u7K)okikK zxdBuH!f!?r=*VEI&c1;OkxXI4*+RuZ{#I`h?ObePDRN)?QdXgF$RpxX3HO-;yeO0f zKrTGI;`ak=yUvH09kV~P69!f#N*9Xta3~7xy@ONudj~8Qp}FhW2?A*+UI|mD#u{XB zKWzfrzRv<^Te>v^EPHm^OKn zfInsXEU|^B;!GZ{Y0_)k-{GzT zD?E<#WcJ;xNBh2n(^e!hkt-QG-tT8Qq6o`s{mgvup1oH{8`&;N+ zR`hWAv1-l?Q`^Vv*E$4k*Xu$vo6q+(a^;o9vJC-%n85RgfQ%c5{UqS-Jo3DcCh>ZK z*YHTVpO1F+bc~+6R=V)Mh8abPd><@L3_~OUG$@NfrhUWcqk-FJkSG#7rm;m&$(TOX z{AB7)Yo-t>NbH$NLVLE%$9;)-6pKepT_mKVM)ir&9LX1Fm}o*YLgD#E?}RO@k2$Mz z!rk)oyR-2U4ttpzN95VU{33?H2O&Am{Cp`D$TBe?i2( zP^`<7g6xbO1p%&xB+PkZp+`le0kQqCs(u?0)aa%2Af+wxd7{FS#Q5HogU59^B^4lz zd!fL^;}Ru7-`39k1|A=C8~`|Qu-~zQ>e0LRM784~DgcsQYlZEyZu2lT1Q&>0j^gLu z{m)xMNTAOdnMYtle=6D+0$|(XtQ*~3tBT=?;ySb~<%-6ZJG4)nJxCjXq~eoe)H1MA zhoIzonnX)ijlk2I?{Dk^>8hhmC^svm?Q!{N8PWBra*-mg^^v_OAJ*kU+XNxi1>V!P z=IoF7Pzl$W9K;(;?r`<|M*{wf@B%0#nir(|{}}t~sHna#-aEqpBRL?QLr5ctlz=dR zlt_tEf}nJFNDeJXD=8_Bv~&)j2qMzm(v5WUF5lmJf4zU6Ycb2U81KEFbM8KS?@t^= zQ~WzDG}T`~2mYkbmIpt*x$kW6^nUYLyK~RHce^1$nJl=xkw6Ss(mnOv!l4Vk0(Dw5 zAY)S~$4}x=N7^9ix9Aa+c#nLMb6I2FVKOTH9uueQm**{P11 z6GR?moS4b41!%^1fB1$vCk5^@qF=D7?`tB|K{9yE=Sy7vdy!ax9;JVo1n`~~+$=qO zxUmv|ioI7-cCTeE*HUfUj)mzvsa@gg8Or)^93n3;&WJA3I>;Xq{$>bONWM&T&*Pm@ z^|?R|<#WR>tOiPFkl%Lt3;|cvMPDFqHyRHD_^G9A^)=e+3L^^c#bu0WWD?m5-=^4y znDz!59<7$I^{#YT%^Sx(nlFe`Q$K+H`ZySLhI2U zE`u2K0c@NttBvI5>+ND3-lSCc*g}c7d7jpXb;T!x&<{Y?ff%MV^=0X=#u%@9WhUS z0=Jolkr5bKU9mtE8aBXr0|*x`lrP-rp`+9PtCr7?x+RA#0k5Ar;wcfO&REVx#~65; z-_2jJ7lQ#Xu&?@_1yhPX#I{)5Z5*@Ih8|0%QxC7&NUnL^Cq4|c%an4#g2T=cu-i=e*Kx=b^GOXt^BaTiovF=0fZSH{wBz)7ecK0poDHmj1OXU*|IO@R~mh#+UH z5*>gpO+%XcFI8pLNrBD;;2iMcJ7n`0bzQC+rf;b9yayV07pOxcMbEm!e$s)PDGZ6u z!U4TI@BjkH9fZkq1>kNPs1pTs>ba0eISl8r@@;dG<7^?`8SH6e_>tXefJ0`y??`xz z3vuZBxuq1YJ2^)xcI$@4*8MT)wS=&NlRqV zViqGNj5u@&3WurxHAX1bK_Kv?O4KjH{tP*UXxcu0^7_1)cz@NZAQ-|tkTI0`1>8v# zs~29}S4HGZN8)68h3=e}Q+B${!O1;8p=jkDw`09Qb$=a+jsg|v(3&`4*^5fwv z9g;Ois0Ets%Oxz(Z=+^O47m&t0qm3eXT0rBy6EgBK z>fozdu~1K?Hg=(^I2#cf%%HHaK+b&vHW>`u^&22iEIm@B2qqDSGzyIk<-~W(v*`d3 zDD+kano{;30?g{kgh|rVd^aCWocQZLZT$LQGTO zQ9gcHO!{p_5nR8{U;zw1_Y9CYI3`*)lU82aaU;zw2 znZUULGcXXVDDWmd!dQz#h<2!7)4ZT*mh84_3*Q?)Dk;)%zc^kq} zpdM=j=iJ_HV|etLvwc*;Xj`ax13eF9GW3K09}PIjJE2>hM)9rTc(RCq5kf+0k`Xx+ z0=~ubRZLdWzchc{@9Rp;%92a_EbLGIdY(EfV^VEk8K;* zV0fOnnwJO%TBI(7=X!Fno#-`P!rL23(kw*$slk%o1KvRY1vx*Bx%=Npfvj|k$$s9F z$$h)?eRJxJ-7C4#z3+?i4A9huctoG7S{)kr#qPETy(azS;%#!h>~U2NNRdZE+6u5_r^C+3YV z$_Tkbd^or0tOl;Gu;awTf$>*1W%Z0QAZ`PLO6#Igx78Q4$rrszJii*6PT5+V^sW`0 zT|4tnDUD|Ly+*Tq_G;qIlt%H}GRfG)!bA5?oe7{xYc}>^Z4Z7V;HU`R`D{xjz@Ldi z*&*F^WnCxZ`uB(L&9P(mGWsnyZJZuyB=+%Ub@yQbxDzap7IIDiAox6viAgvm=kkm= zPgAUDW7NBBTSlQvFKd(QcuTUsqZ}U2o$bh=euYfvfAH zDM$(whP%k(b$FI(QXXZ3Jd#a9OfTDipOYk3ur-CI0fV8@S@UG&0*0=O+c>~nL^Z`)&M+Z6_C6O-s9ly(n{=%-F5jfy; zp4L+$hgidvs^S4}48H3%tZN5Hv86{^qoqyj{e;tF)7p3*h)nt0h%DXzLKpa4V2Y#t z+;4Dhd-CmW1^dtYHzc2$zk@yj=sSfB{zf$@7*H7eq)L!Oe?mREf~T3sc76Hpr(DB4 zxryEeH}79}o2x~SOE-0!d_18iEm&aEz#DCd2CaDjqvuI|+Pp^*WRK;2`piO)#MQRA zLx88=U=v*mFvnI@>UVEm6bJW@-lO{Fyw2m_ zP4JE-Z}oVYb93oGwrwtXG=AP&F_JU63-7mZFq@o za^L`jdZaNR;rUs1`5aN1>Y3L3-A!{TmR{R+zmdt<3QcRn7VFv>>Iaiv0)vga%4*w= z{6l4{3laFuQ8_*{7pSen^o$IfBYI!&rY+AU^HROk_@>LD;+$ov#O1O{^@Lh0>o@1& zf&8j%q8dpxm+*){{$mL)-#5lu?Ss4DNE4cF_`QA@zU|oB8eB{yYIXE1x*tJ7JGM8( zj+5;}+@D}^h$IIq9uU6^KslLQQ6h3(=pte`@4wN-fNwaT zX}&w{1C{kbE-S5Uy6w3nD&<;b_RpF9+{7RVp>xrZL`R=0u@OE9wRm{+C`BOB`ZWni zCIl@R29ZlN!S$IZA`0T$)c)am^4T*ps##Y=qVNJRdt;OGYoZB(Xj423hfCZ8^_8DI zccVvkoW*K`TJV&mF=CK&GrCt4hq}9m760j8b5919C+`{?@ACr|2)H^ssC;&UMbIW8 z)~?b){i)N2GuJ8(*bzm;!5yj}B;R`{cd1zSQ`Lu-7Nl7P1$MYp4cwuO_5M?BzW0XJbQZs|U6Ouh~Ve*%f1UY7ARNus7{o%gx_eLwK1Jn* ztAsf%xB!zbxfGCF#Cvx0GSWVM(KV9)$>_f zPf@-vW$;~ZD*fLjL~UR1YFZeG>vqc{G2)PE#>^;izz!#tZvy|D2$S>m)#f+*l#T!r zY>c~L7qx&Bu^7NO9pklSp!e9lt4*&9i zz+&$nB;rBKwVDqy{EOD}=)iTuV~H%!>X&$+&KjK(42L$K1gDMgZ&uObaC=7JY)eUa zKlAY3rtI$4~aS1CI3z-=Uk6~)}X=*}f0OF-C(go{*5KF27 z@2wRmbd(@HD~{yJ%g{ZwiUq%K@;c(g)4SSCJiI+JHWMeWOsjAo<33nkl~sg8<7+d2IRVSg*_Ql-NHj4kbGZcru=JEaG!-L9#6h z4hqd)SmS!qt9%$b+XM~XT5#n`zA_!#*=K>+85Ur;D5?OcNt!XCr4ck^7Ct>Qs^D~Z zMz|0yw*=I-gbZL|74Uk zoG=t82+*d0o8ymHX8zP_&A1)xgpX&li&!CN_m=c7?mMiha*mK zpaSD4)<7UY8>6wjr&!iyF+R6OFXo?)TT-!3B_y_BMt={YfMcu_OEPBop=fIC@YUxb zod3*VhBB2)Gl&k9pdH=_fRuq4;&Kl`wZo__v9v*NdUqm2WEk7o>Qs41XKTI8zjJd&a zN3sQj)|wz$a1G~0ROSUmE@MB5kkN(4hc@?Y=a6ZTpbP+k&LA}jh6D1^e+H)($r!5&TwlOv82$eUVRQ`3q90N<7YQ8|jm~KSspn z3~J(UFY-F?6_M4edbt@YZ$9dNBakTqrxHPzB7nd&sBi4;1vX;%s8ukB6P!dS| z=C9Ou#C&#GB_M={jUPs2`hC;eZ$kgp3o35*LJG4)eBs+aLuM2Vl?4nQA@-tBuz;*t zIEZ`h{N&ERbaS(`4bLEis1FfJqhVlxy1?2xhnPJWX!}hyeplTs*b;XTG{BAYS6z@I z+E;|Y0gxmEI%lw`V?m~9>k=I%=M6J@e8j^iW%L@IrG~}!5Jv2n&?2dR7hSMA44hXt z3;$EA9n#~&XQ10~bN1SKzGTlea9Xns8U4bjTNd1bsbKv7O4QX6{3`8j>HpqqLxRi#oR50KI8l)BtqJ{6&Gl1CAizuMM-}pzh7;TeOzy4`nWtkCokHtZu z^veCPEDCp{{l+*8954Wa;$#H@5UKN*R}P)O>GF{ZH=%HWWDA`nQdly9Y-UhV=?@Tv zO+Z1DJKb`YzZSJpenZMTUK_;%k5(GK|5ddD8^a#KkTMwblnVeYgW%HuC6;}N{ioaE zyK7lWm{C$+TSxZ@5)X0^=^?y?No-k5%wGbN+a?WDHHheKq>uDY;}X^%wc7XYnhYm5 z=3wE|fr@Yt#<=Yi0`s7qDis`fZE23TOqmkJtZ3egh>chp`U zZplyDMa}p2R8$&kKgG>IGOb$-&s~q?5P7&NXyhxNh7SjA*itT8EtHTr{un}!JVW}W zwCv(TP1dX{Q$88^Fa`~91mA<;V&MhYJHSnAsPQ!uU&pDHx~3S{I@RhJqjH+*Ur@5ZPGWw;57I7%W?6QsUQ0O zJc%20oDfbAA%^&p#2%hQuB#V2&gv&yr_a8wNaRYN<_L_txyE#lSX@AA2it&rg8Qi& z7^#P#RviQ~^F%NxAXGNz<3NgR^Ky%b9KE!-Q-b9VXKB9?5=6DeSaTv}N(B*rDBdSf z3>{&WD`Yl{|1+z-*?JkpY$xw?^DiuFfuv9E8QsvYXZEH6kXoa`yYQra1lpf80}VUZFmXm(@Z9Z$le+7x>Mj`mWMNil`@)@olb9)lmJ{1}oe|3%u zM4=E>x?PFbpuc2_$9B>#@s@Jm@b!-?tdf^E8_yNuER%@_`)3^9<+T;Yyy`sG1%hL- zn*L_qf}Nm~F=Po|&TT~RpLtt;&Y-O|e);-_>nYwi?`BtgT?6Io^V4WJ&hsdA2Cva; zaXYKKq{AHgn@2vi?(Qxd)Pa&2Hd}d(6W8|wKV~t6h*bJ75GI}Sg9d=y*pO%56||s& z{4Wj+gz4IPmqN|lE46xj%EHAy&ps+FtoXKP9ocN^N(B==OCCg(NFlwu6{~$SlY^i9 ze9Glg6T~ojz28pI>xeJjTP(eMGWL0c;qYR5{_Wdn1pUAXXr?m-Nm zd@p4*@`4@O${y2WN&BIyZ%~1MY83g>814LWu9nZTXgskdb078#3XZM)+xI&^npr!{ zzYdk@TQ}_SD{#Muy9O$;q9K4^h<_;^{E74}^(`1QV2i4fLLoC#cnbtw|6FJGeZH|y z?32?lA6X&bjZDMNu-f7*Sph3lf3|bbjU_HphEoAKSwgj~QYXcdchQL{5t|!to!(ZP zqq1I0`L*9RCdfau_HV4r^YHq7>37R3iI!bIp7YY3)$6?eCtyqHC-jSK)eo_GZ7JK^JNff+mY%{{htot@?<4Sl&9_CdBGS>qHD9er09fK_Sj6z>VDis8lT0QyCmbDBZTep zOkyWIAYh&p4+m@v^eae9g9t+d=tGgCp$MSN@w+0V(F4Icrj%@Q+v;8nL<8fxOK_%s zjtNGvLp*~XzQC3MSYHcT?dUwDfIgdN>FcgoaR_; zHc4sF*M37NZ%B3ZCl(E?^`JB7g7t}DUD$h3NXqL7WK1>=w1qW>2ft1}t4*ky?M>lA z{3o@D?Qhk~*FxOShns4a?+4l;pBqOPv8umP{GNba%pK!^rPAycVUEAXmV0>?LK>R- z9Fh7&8x-haK>->n=mv7lSsok23XJw{nJwZF(qTdeg!+G}`E6H|jt2%(7Mk0Zhh-4} zul5kf^Wlxj*!+@;?0GDB0G+fhvLa4>oWta`_=-6`mG<5fg_J_^fb=Q}5OQL2!m>i5 z!lDDw^E@ut1n_fG5fs+F763FbGxEafCjmXE9bh0AP2)zd=4)GIQ<&2Y7dfY=+cwu0 zmU18;5eD+01j+%Lc2ct-(@ zcvaGhfzDWK=6Ql(N|$sEJbaajXZ8yrixj~YhWiaR>t%wepQ~d@q;QZ*N3@U|2xwlT z7Voy&a)|aSULo!-Fy()_M%~p6Rry1~AB7UK?dnCq&#|^5Aey8x*C^9gP0Rd?j6Q?m zn@(BYdd5a(xox#qgQ3#ednKLeaq4g=9P6mC<0mhxm7NgNB%c30N<0#6dV+oD1z>_?MkExAHYaQ~H493lEhG^#NNCIY;GH^{l`@3jv$_s_s(H>+&xa zE?sA$mu`qVrM&364Ti5Qb~sK)v*OnN|4-t$J#<3rhM3Vjc%*Fe{0ad7D9NclDU~q_ z_q$!PW=&Q~ zKZxu8{){cTDVg;k^*oQtSb#W3 zE#~7UE8p0^-0W5^k}CIpk*!Pl(#yXV53>cWI{0OC@Ekf%XJ<}6u>u-0P)|!NK|~{; z*#)>hEsc36bIg2@Gi@&)-Sca}CsO&Z#^@W}fEFu2zr9z1 z9Q~mSj&Oj({O390ut$LEg4^?`J8u-J#2b!>qB2a#)?1E6^s>hUUv~0YUt=1+OaP@8 zfCF`k)a8Ec*#ZKA+>=4!K%_Ace;q{&wQtfOMH6ta_{E8Sp#RHsCL?m_}0iU7T|%Ri5Peqx?d`T*s?$AeOcpE;~sgcf9AB!Vi~;8xJ-PQ z`r^q2?qYif^eoJuo`Nl^gwXCxTHdVG``xw>g(d3)ink6F(?Y;JQ5C|ch6FLqR)Qmq zKqC#z5;NR9)~Q`7qXYNf9&8)VXPA7NisGcBku2%Q#B(sigO+eV{xt2mxNaUbY4Le= zSk%&_>d8Dj&sk>@MSrg6wLLGF`J&e*ax{%*fY|K(I)Wu?o-)53Gc zlpUkT4`^}KeghsdD1PkrCE3NcP6W%VhO1P3xl38C%04U^v@P!iW)4^%HKiVXY!8=K%Qd+^z=i`v?Q2SBQUlxU z_h=#}*Ra!*u@Hd~1du5!yD~ps`zPlo`*djh<}i2OsepIZK9aYsf;xU? zlip%OOQXT}5h!74|Gr9GYQ65}Jp1V)yIo!q=~s!CrI!IPiHBDsO5 zy?_ixm=xZjVTP^kJDl)ks_#sz_rZ^7!xsrSeN_7E$BwTB_iTo|kZn*j7?lFh07M4k zZ6>Kj^R@|DPc8}lEP_=Ua6rhv6&VRh z9q?P+*riqDyeTWM6kK_f8xYcgSJr2|pc7|`qgV|?V$=NZdFt$s!70d=rM#}wvN~@D4?1q^20MxSxSuzX7EfcfI8R6;ymL+0dn!~WFSv_Nv*8-$L3g2e|Gmy242%PKy0wUX4)9k*Sm;Gh-eP)jLms#VvL)p+V&3>E~5 zMEa{=IvjX5T)pD3lFC0;RJ_}>l$p5kmjJ8JTjdE`rTcK;G(E6{WI>G+oyQgL&((PO z)Owp2?SCxto8L5R_o7v3e(9%B>sOX@Zm#8toTEWYfLjJBT&0gEF~KkH@|w8}E}p`c zpQ4|SGJ5^IQReomZN>DA0Rd^i5;!eOD$?qg{crWIoImfTBPvuITRK~o(rcU#K1Qk@ zMHi=TFxt7>c~~q=QrQ00p((0(_;(}1$IJJLM$KwOob0M&@Z2H8$Ei6_9XFrvZ6j57 zO)j3+OL$h+#^*(e{p(}g)}m9BcgfnpR~RrPOsW@`Z;qzf+rHFY4K8)HiF9|}cy>8j z&%U0ZZ0OXY?B~>1I^dEpWkKUWtAf2_+}|0Si=Mg5X+(T?Qpc%5^IhGni#H9tzlOT8 z_zZCjS2ao&o^+8`s4U=E%+cTOs;Ca#@QcgJ9c$bvZpe?B?d+aCuo`qZ737Xw-i^SP z69)MQFd6ra8k@tp-hjt)+n+!3&`rh>i@W8UiXNHsr{M~q@hjV0=EZ`h9B9Uc+J#|I z90*an9Ubf>>${9`A78K?M|BV_FYTQHDSYv89UX4!4W=hOyy4YwSQYk%D3do~6bCsr zKlbiSM1Gp#&^OH=kwlCxiT(Xrk!Iy*R#E}|%%*OGk9rUUK8NZb@#t+mO~0z$-vw@K z-LRn}t`@+VPF(EnI>@LVGeE6g{cP%g5koIXqe>UZ0crd%u?}vEtt_n+9 z8m@hk6D+*g+U$9mH79pO?*wy6tvqp6nmR%Pz^CzmfNqf^`gxSjXeb0}1loU~iDAMp6U1vW3jGUBJvwfNSNQwck4OJL+?s~aOZ{9NQ7AY@ z#AjsBVh#I{g`d!Ui86Nb1v=X6YM-XvKsJh8r+livVH36)y&l5L{S~Tft*jAn+E^VO zEiPyVZ01QA&7|TNlYj{s0!jq_*vMf};Bh6$@!I^_iP0YlQzPsi6I${&JvD#V?Ms|H zj_Zz>Cm-?uwV|^zrnA}aIG&)Gai2{_@CaHnD$Y@aZA*R`QgnfaSs~QPNsDBkyzDAq z!p6sMI)g9-zVXHEsSD+8%(|XhD>zTi-a#N?zkzM4BJZU5;-YV3wx&yYw$@#^Q?v&d zA`?Uay$S+M5^aRQVR%iLS#5eWKAexU!xMec{PL3CVA-&~ue;2SW2^y6hWV*h1qrUn z98h8{l0%IsRyYWueiVQ=-H>5h#@(K!+5H{NH>8mreH>V5$sr2sPa1QLE7)I%H+}i> zWL8Vwcekd&ySyabcIq7pRL+Ey;PgJq9<4595`X&lgz6nba-wG2HFV1JQ>c*|lLaHx zRw28CgNFx{*elT(`l4&|vo8T(l9HCmPqsgVUk~0fCvKZh4{2}8`usGW_+i4m#rm~` z{ZzkKBNUazRM^18Xaf{n!O|!+S}X88wRHPsq13B9iIfxBs0F8=`s(X&RPy5sOW5P^ zFZ4<-Y|>~9B}Lsq;Hb6d{ex}xtmCf@#ND#|?=3lge{T5q;*6h#TL0u@6-OU&Rq=gT zf{;xhvYZKt!f0b%oIi6YJf^Dqyf6N%v*0K}+yLr&9~c&es4oEJ?02j9{DpJ-YXsS) zFdtr$z;ntm4Xx?AE=p?YTz*rfYd=&}WRp`OzY`34DaI2ICCI5vg!h`BMe}fdz$9>i z!TdgAZv>dsjsW_#u7rV?QJE5sKP8L2ZM=8>;-$<077p0x1p<_iJ|O~r4*_%1K_oFI z`Nt}L%ztqf4ywCKb^3MaR!Uq&3J(`^1?s$BS!w<_$p75)p=~AnEgD&l0+X6~z0Y`m zt!_fE@SkXH!!`umd}ZP@B-3f-LO;9Z2Wq(kDQ*yTV(EflP`o4khyY*57tN>@+W{4q zGIEtq5mR6Onw1i?rubeuF>^xkZtd7R_2r++rdm44mWqP;^mh}^<#(WH0%;6>ds~WE z=C>XC)1E+O{)Y7~t5j*@gzYusxonoVdy%icKU>biBxBH;o zf%~Vg7uUgTyrXC-;Z(4XD?i?4WZk&ubl#); z(0p5Ed350Rq@~Q*EOSVC?OM;-3s0VD;jB&IqS#?sgaN2O#6~(sFGUFm$j0fcdaLuA zUNo;1T^m077JFL_WfIPe8Y)yvc?)oQ*jehm5W^!(l7WjNQE)JV(H=O7W^066>9_n$ zcx6I|82>8$cG%5Uth8;aN2V&i!d+XfgW)D?Txj zxahE-iEoJ}+?U+hUcVL|YpKSe#@Q$A4(Mz!AD{Bk%$S|M({Xwz)cG!#oDF-ghc74u zd^f@L#weMb-0|q>WUOurM`e#PB#w_DfD^Idv?cXr{rPKMI|HTNI^ugHZ-O}%ISqE2 zE1W43w|lk2i9*ZlQ)%LYWdU1(-yEK$*MGRJeYF0`EnW(sb)~8Kr4cbG{$9Sz^pm+4 zW)nT-DKl*4rU_`^hXGt#Em@PFF!A3nCytiZA3lPv0S52e@5@N9j8clQHwLX!jwKsU z4R{X?js{kE$=`@j`YpzretKZs;UICFSHXCOVxf#zd!`^6Nqj03>{WHCX z`I)YQO*L)f@3ZOD%!EhK2BkZ#a)jSE9`C@D`;^!s%x$(P>ZM3=x<raK5gIW2j-LadTbwx_YNm zX(Lxls95*Cj^=mfye6XII^`L+vwb zvB@B1b3#as3$;K5ls+Ak8#=EN5>p?j=gW?6)uYDpUmqtcc{tPei)h-YD||{YO693+ zel#mWXTXeQW`XVLfd?mFg6i9sU+kQOKwD^OBU$vbi$8ZSl6_x`ci28&n-mG#{&9fD zSk3kF^f*-}h4v&-PCR(4wijQr_y*!8cqN$Dw|Awaki-4!60%z*2o369i#!aLFJb?& z=|Tz;C6t6f(me!HiqlXLoNPVq_wQ+j_-KU)+@-_k^u7rz7$-y;%!6UJ*}IPBIF;c< zFWk4fsqSfWJEPXrN~Ti0w{yod$Xosy9LMmSE+0hpK4hf#YS8!)piIJ3QdhKFb=S&Y zRfZ6a1Pn;wdT*4%=%>XU9{UB>$i>@Pam;t&6)nRC82*d?UdoAM?(Y= z)M{V(^QSBFusKVgrAXi4VFGyluh>Zo$jlMcfv*0LB^mzycRWpxX>0B%U&psMIUxi&pPVLoX>+$ZuyQ10^s=_|Ar2Zu zLQF(qUBdx?U-O6tQ1^zXpVXVavN%a*8J2gRfOYTu0quBb(70lFYjnCwx(^w`hUt(OMbk1tw{ zjTgZOCl|t zs_oT|n6F#&mP*8vbf>%qY*_!q@a%ULA_|o}Yyl(4D(g%iC{m^;; z--;W1r3oHS-N|YxI=#E6wx6Nq60w$dK)+eN3XguUDg}{P5 z!F1TB66Z82IIdA&Ox=dX3#O<`o>9T5;9ChN;e&?Gz4CuiKxBMlb`$cuelnL-{=FaY z&Ih?&@|lM*Fi53B1cRfi!7m0im%qQ71}3n?-UT>!)QyxtRbS7K+s6YtQ;t@%WlmQt~_D>NM&Z2>Zp z!9@VY7bw`5zK@)Pl*n+vm=J<*#~WkC!qx;yM_Dq#VZ!WVQBnLh(&1kX!cJn$EbrIn zFZy?uu_8e%x>s7H_&W6{q?9={doex=bQ85H_(iBAMCL%;hRM$gddc{<_H+EWN)-8o zH{-H>O}SwN6D1x5ZjZB1(u?wk1$Y}V36IC(DP2(N^~}mj3clfv`DJ{Z%`q7<7!Fc5 zwhjSXI9PTwsDf{ z&6OS5!BnQxhj`pRfFE{Jt{b7OT#c&pLQj*jaMc6U{bK5_+w27i+hJ5XV~SW0))#l zBLiOakAy$0=Jr>*(=?mzX3_SwoKeC0mja%yKF*7)7RbysIx2BP(huT|hR7IZ5-*i! z5_Lcm7%|3H&17!LG~!Yy1La(d=)+g@jy4*3ACDD#(s`Vi!ZWQ*i=C6!4CfDp|9->n zLZCp-EBYbKnAVa858775vuwb%J5^jQoU@)^y_d-T!A^`i=zYQir3*v#V;kP}slD1| zNSe`dFH!lq7A+2RUrCZKm=y|R=D}P7A)r}RjKKp-szkH)i{wzrF-mH?iO=nvPZqwa zkpa;B&tU}6#_C9bDbz8>o(AuiECQBrulvL1_NqWF?G3Xt<@H8p7Hw1s4i&uD=x9uP zxL{xQQYUIy;N}l$_g4d>A4jU?T;=q|^)omoJE&IJGVa=U(q-HN7v72|^G|x?E)8f> zv&~y71*MVj4YPJ>q)@%QKuZeEhrm90<`5IzCH6H<*S?h}gVKaMYiumC2&0fdAin=to zFqpqO!VRLeSlXr$qKwJ-U?1ez`FH+XtPyEAGHD#)Y3taG`>vO;Q_RvM&{Sr;Rrt24 zK|Pn+s=3quV@Kr+rI*!@D^K+IcsqV_3h@7OG+^~0QbxJ(`dq;_b|K1#GnJWa-wIU> zUn)l;rKs$ktf>p(n_Qq?SCA=@IpP7tC z#>j)&Eef8)M>+T~_t#h3$gbc&9d9LP=zEaxIPC_E6BPwNLR|-LdH-YM*lFz6SyG~; z@z|$&Yqi6jKJ2Te5k-kNS=M~kUu@M@6~B4yD}KDly>2k@3wj)t_hyWz?z)qP)|%wt zORCsg9-?=8DRR21<4rRk&xSWLN52czPAG4a!4YVTUPxUQxzF-bsXXQPo};^Dt^ND# zcXrSDaxQ6ZtaJS$*Z2KiXT+)%hN-^gjhHb@BlJr3OgY(FFP^(nHBY;^Zb^8&qY~d( z{JF@D+VtLdNm$+4{(Zh|FZZf?GZqrr%1qn)-3y-+%Q>nx%zExreP`}Kp<4x*d5X^; z4aeBsP{ciZ9w*tk!g7Zy&NMxvLR8q#qk6;h+eHzwuBko98V^e4%mn+&w{rdQzb}9L z)}eS1Tb`z9WAj>f;JuQ}Vf)O1U#!;Y%QC%p-+WA7BE>h^0?W^>!VF#od^LB(%L|8O!FOb1k?lu=WfQIT{G$Dc*O2)rAPrjp}(Ws^@YoY2zx-^#H#^{t$o1c z_B0zCIk<&Y{CVgNZ9U$aoJ2*?OQq8;iNPvCLGcHzITQR`W{$QuEsAl8Qy+8Z{RzPL z0aC&2A$5~%cT+Grc_Kw7q{tb;9%7QUFDL+~t^Dv>Owd|GX|UBtL@bsC8Pl$Xb$n+g z99_dy$fCB+X!I;=R4%`@?=f%WWARu*8QtjX?C~F(mk;QbQh5-ud%KG9LAVc|YsbcF zVSQpe9hHbbJ)Jvr`}Ul*j#Y4Az4FA2KmWoGCf8oFGFmXWsh7c~8@{c4@;M~v!( z_`~t9RfOW$Nh!^~b@1dmvd?OX6goxNn=n9R^xjo%IF^k`5riAsh@<~3c7v}d63%)> zqj~T8%0%lRkRnB}U6at2v`FkUFPFsl;5qk&E3@X&+VMw$unU)2ij!bq4~+`KhPN4q z?e083Pft6GllKUzanpZEFU)v{u)7^hjhXtDISll74>L->t7>(u$25-dJlY;h6dHC#c&`gNeSw{Dm#7l^C1Om6Y_T- zWTZs36JrN80^}11#@_GHoE9E1LPGC2NM*yx@ z@$x8z*ol)n*065hMgqApB*M8egfEJW(ZX`GSfh*C#e9O5=K`(!Qkg*@e)<32=x zRM#2zW-VpX1!q#MigYuP>#gYix)g_Jg*|Bi(1mnZ#-PWp6~(zNSg+s(tpe z_kKFDuQ^#>ojJ6fh~Jj<+`lBmt2^$ImI@B6_dUxoWw$1n5dw{A5NokZgno@GUM163 z{h(LdpE04c%WeM7PXa=By!cA%rv@00Xb*5dBo_5?p2Q<9j%-r0Q4y#u<@+>u{)9~l z>SHE7LncvUZZnnTnwZXKtgItGa_D3YBD%gE#5ym&+G~JuNW`1nD2WU}?dnz)gn;Q-tz{5kNNw`ViqN5?R@(b>hzymoMJg zTd+Hl7R0KH-=U^W^V3SCpR8)dH?sNJDK6JMG1PW{cVE66er}+nd}=15B)5-lH^gX+ z{LHMCQ)agk*F57ObFAE(I{>*r90OBJX0iUC66wF9q&FAz*e(9&>_bEa=`)c+-+ z()k|N`LBd(H^uo60D$%K|4FD^72=g&|E(Yk7$EtDQ4&IjS*y9}q$k zI`hVT(*C`l^y%G_W?dG?YJ%!=#6FxQ$E37MDYdaKtfdDfmSd!-#IYAAYwG$6aXkMCYY~qzdaAocD=^=x`x-}W$Rv<>g{b3 z>)>4{cN5k0A$TWT$)TkIjSzNF5Dn8092^$j&xe5pI|~eyzsHv-gAV`=)CbDoGNRq% zv4mK18(eoI=ur;2ogFKl{SMKB6ws&zZHeFm<%9Hd5rEH9A2>l|sy^kilse2HJ$T3< zMtA%$QJUiB$Hs)ZiI^eeB#y%IyG|kU-v5@kKDmQ=8fu5DDt^w8v3)2fLG?ZyfVMQ@ z5)NT+Q^SIS!h-ri2W;=kU7GC2rT2vlzMX-MvkH0W6iCl1yB{81YUNj^J=f+MUWwLg z`VrP!3xi2A2KpAgV@Qa*2rw~zc1~M6Rr|%wd-e}G-4{*Bx0rtSm4xVV{Ju^!Xx;m| zom|TDl+EixuZH-fwCY){H(S)b9r+@Hs05IJq$nKNHzXWz1J8n|t@cd3h{iRl zi2Abt6?MYhAAW=oe$I}r+^l-4Jet=i_O8tzt;JDd>OP_FUoi2(15JrO43K&rOxMv^ z7n>z~Ah-|v9|1xkeam#nsuh<>r|e}4$p)8V zNhBg*etZ!batQS!FWB75W%4?6Ov9*fN<$b_C?Wuc@{$Nz56EBZ^pwhK5t*>k9koZk z`94>K7rz8WvYpL9UH0)v;o*YQ(mty7+*6Bl$qq_d2`VDsOO%5ds*dGwaFt}Q*IWKF z=>oyVJk$X-jC9}t? z&*+u2GH0AgAmeqSZun$9ge{?k2OGTv;7~)jwt@FUg(t6i{Y7SArTPBL5i*q)8-}Ue zdx+P{VY}t#$M^hIy#cn@9#mtZf@EM`KLNJAG-Dq^U=(cFZ?6#x(sPL?1*2Co)8U#;?wOq+p|!ar!DENx1#Q8l{IZ zyc5s-Cwcg(Fd*&x0{&YAFl!q^2%YsO`ZsFEdcsC?s>wbLl?2Ph1QOE){m1Gs-->6t z-2C!FTYhR4&9EKBumBO{%$v=GsNs`kQQ3R%ybp%OWWq#KUV>0g-5X1x+nfe>P{spb zv$-dIiybW(^vz?F{`aM|``JaViu>GPuku?|(o!_?s zN@&1%^m1*C`aqlfv?E4vA?_6{$VZPfI!af4??Gd^4mjJ9bkNzi?Yj#}2Kn-nF%>IHtZ z36PLSNN5q1h1*OLX7z-JVY%SEXl6>;mO1rAH;oCSJxeB^{X@XpfP>Xb_q~qxmrNIW z4ty{)IK`S}^1b=`&nqm~rlQv?gJSQ4!I6;L!-8U0dqzqw&65eP%u;IBleiW)U2OUsr)TjJ?C6dxyS&{*Xxm_zbiMyw)R(t$0WC z>8Nz~pJkm`q1s)IDAem9S;PQLffpsBzh0fSP{HTBy%TS}^^Da9#@1au<@5v{ZzJ1` zK6(sD9=7lYzFRhQ=h(cxV-Vh6h|frdjcvwSX$j7z?MCL~-KOzU^HH`t_}b7M6nhs$ zV(8G)*mp9bH`QU!QFuqm9<*nRCR~QO@3c<05ERV32Ti;=7GNa~!JlIEEm}|XS?=Oa z2T_xJ*T>X=sY}r1(0mGl4mVi|umJj>mXQwZC%H{({!dB=s1SG>vjdI>-;4>xb%T#H z3J(bJI6wF%0a-}b?&7>OSSbHW`Ig2F;Oj$F(cO#NU&KICvWu>Mv zAG&F88?6`d`{qN)JNo+_nMes)*Y1qAl_e93E$RI7Gj_SZ)KX);HGDL!S(X#4ow_Cy z;_LSfXlPSNzVZ0$$k*>UbIE3Eo6!0SOCez6eHF)=3l;OBwYFpRC*a&_BA~JY#l{%j zpS>NksSzWTC*=F7r3)J3!83;v#IU^}eCXzA==cI37u?x$qJT66MzG=p?uug;iUuWh zoqJ9#tKGA}tf4)032R~AivcB#WRg7V$@5y_3 zm?XEnt_G`~LRM2)DF6=o{a(z=ud#P$|7#@-^)iNsTSuUVlhjTgXjX0(#`5cVt2Rc_ zJP0RLW&H*VEW6G&3#Pc;wh}~n6&h%w)<>uZguaIpob%W4&dhz2JR%}VJ-&m1rmUj* znQV;1IP0c1L%)t$*ol)qs-tg@Wzh#L;ISAaOcCT%S0C0@$6U$6fxYUwcZ$`lSpbo? zA0UoyZhg)PzLWg3mljq;rs3EgT}0NwrPB_lMzDO8Jo>QoGN=V6+0x1+`h$=Q?#2fb(s zF@-7Sme~|-;35R>KIA@>Z4}>HaKAO6pI}*a(YQ9I+P zqER*R3eFm!*0c&4+Wf*3)c`-BK) z0(0F#q7PSW4-z~opBl+U>bvq3)PT(sF`R*7ok{*jzY)i4_5!6 z+=Wd{8CoV@U?fjTIgZ!X9JezkP^-ubf*hHF>%t3I8dlqzW^rfld?d7dS9f)`ul>?8 zD(#+J<-|Phh1$15{3kmB7oPwLc+f@)ErNgq%MTL4^sRX3c9jhCrY#YRL`=}IA-FMa zIRSOleHFFZrP4gLmbN-rC4FeLFlg|?JbOpvRLX`s9M8Pa(p)TFp#O2ny^A>V$H?$% z?(DzKc5|P`qeYuFt5=?U0^@)kc{YJHtZC&&{ge95`UBBtj;DwcIiJjy@$sQ4kFd!ZOc&dX4&g@_qCL|7dNmk!7paC1 zr=TiY72-@;>8ohSyse^zKnzp_4L)CIE!6&La%UgJ(!jGz#S5=j(%b&I(L9l;)^wLc z*2SEq%_qv`6*XPX)oWgqE${su0W9Jz@}lmpuY5?AGqzVJN$)n+l>+-nreM=U1X){F zaEr6D^tlIzvye;9O%4J=AEO6EUs+?JY>20iQFea?5%m(y*uOiQD!8l=pUoOMiXdgn zihpd{J&tJ1cEH;&68tx1x_T&Qb^&XyYbZ+_;afvrHR?oJ;YSF;3Tbbv= zWK-spC9gtJO!Tzd(#Qqo3c6xZzNNF+FgIMWhRD_zT+aUQqmmeSpORRPzG!;}wU1+Yhm3RnAkC zBE$G|al2QYb%O{{-t7%%8o1|tKdT&7j|bn^u=O^#^bWQfA`)om06b9LborZg6#e1U z#xfISzF3}~-rHc=L%PCOD(7i=_zt%b+?58_vf<7e8+qF`+4!gB)+h$7J=7-50h_ov zuRzCTw$4}MG5b>xULSODa5UF>sk4D`j74tuv=45;Bt8JqkH=>jfv?FMDgLx@Uft8l z>Bv)b9{+WCJCbPocp{e_^7E}#%5u`5**nYRH~IRe=J{GVn(^-@vBJ&f$toyZeZnMT z1-y(&_i{0(NmQGs22c!so^LF9Z1^llUC}=`T{Nr0so=dGc7`N?4DG)xo6K?0+4~py zv8aP788NmS9fXkQ55KeD+!O3siJ%1b2z9=1H%&cAk|A>lp~2Ekh5Tr-=i0aBWeRFK zP^#>?fen#QMRU7VQ5P>eXdOo`%5~QaH;52&XC}eUv1|#cxLqrr8S74d$6i zKKx3#qnjxcQ3>l05FDPklf{cXTUK_z@Yif@Gici;02Evq+;45WSRpw>eMk~Y`lOE1 zuO>cyuJdv6M3iEG=lr2NRp<8)|7|XZPl4Rbh7N7KX3|6u@www~Cu7_7XIg(sF5ew~a|HX&UAD!!Zc7n} z++i{9@{dp7*bLLp7(=yA{EXaM<5%3#P(n(CZ6+_co(bWEqHY!mUr}$UQ&Kc`@2xEf zjTEmh4^5Xem0RLeMd|%QvC=b_eSb$Nua?hfVz!m>0l9mV@q&x9QNrtkq0srMXy$cz zLNSZ{90x@!bIU&IyZ5?8vth@3;!mzKQ>VZV>zRJ?8kczio*vy; zl|taw*lHQd6vF*)@+u;0LAtODTqatFa3_jChTBtPtviC^G46ThnuoNU1}K<`PUaR@ z!`5b!&h|ornMS78XDK5s^yd8sTkjS>F8?ufs=Z6!?Hj2itb^+l{uosDqP%iaGQw!7 zE|Lkd!CSswQhNfO`(IVJ+i|zmt)N{SK_jfgGJH=LLR&~ z!e<(^=ogvz{t6^j6Y;=rh>NP}9~cC;HdUsE$cZBMdwf%?*i5twxu&5+b21KYicp&5$K2sdzlSTbFuLrp{bu$g8?*U29h=YLTm;hgKTOuzNfV_qT5Kmrs(9Sg@_=)3V{;uabSFoq4GYTA zTl;5zX7k;GLcaEtB`Em-JM-;bPv8fdE^)lm9cRI1wdLqW-nua_=BCjmW0?83ux*G3 zQ)@W!j8lv4h|+tueGw7Px;DgVy3KzPd!`8^yDJ&_3=EV=m4H%Hs#O&=3u{T_Mr%&; zX<~c+mEjH3%MrB6+mdynJ$C7Qi9|CpT?eIS5~fytkAV|h(iU~Y!)iel|MoqLp3Gl; zT*QCP@|(-l2_VGOqDwaa$J8zPd%kPBGGc<>{>A3UIORbpzPP!P} zmqt*y<-LV!&;Eg8#;T~Fv!mFa4dO$gK^&&SL zUG}$uAt{g!77l)p5S~u?Q)~001d8=TmN5_YP;pJ9bCUS&vs;lWSAwoCa9>Fs}2xLWmu6@|yWn3Vxb(_7UVDv0z8~w=) z1Qvn%hVjf23aNjny-ak)=1X?DBMx{L?8&`3$mo5aXK^Xfk#d)PQl;&m+|rII$QSX= zAR&)Nt|4_rFJV!+8N*wU5N5IP88L=K-e11X<3ntJ97$~~&FW0H-Qe6eOb;H>@*5#-> zmguPwnPK4}YCRdTuU*xO4tXktY|6lZpLi(1JNjnIF2Y-Yk^nMdT!{Ii@3A`npC>CV2qDL9vRG3j5*Md#t=TS+eJwKJH!HZ;W!YT8j* z9|ddS1?a04)tdtLyGct8fPKs-LwLBg?r3M$?bBi|d}RxkFD_oNBR| z42XVUcX=ww|Dk4v*-|YG`HLP=K9#-!IXE`5{TEdfcJF=nUIs#tk8~M0G!ynTcCGX` z`w^8?s9;htTxb~bUb#oE-~NtWr3s=~8g*eR`Y~rh8(9+t8+lEa0w4~X%l8cWef0G@ zT{mBb8ZR*fJrI_V_msHNY%0zf(8;^UhanBl3oGy^yvnoGa&j_P$+Urcj~JRQE5)b=C@2W35WkCi;Nl1@qv4aZ((uF+e%W8Rno7A<1B*;S zef475!}Dk(u~IJwSxMR!S}o2BocUh?FoZ1&X%u1=i5tQ+j&MKsEh}Fz7f`?hn$ZIj zm$$-rt}EosjXtIdh9%2<5$+VHoYBhyiJ|qs)4xFlT-ep`H2men-3n50X~W+d>}Q+A zOyh56=i9O;JGcuGlhVqvu5wGVu7Z96<7R-HBTF6ApOHo;V-?v+qRu6c;c+a_lZ)&bIeIpMrv{j74(rEK;!sXtOSt0>mCIKLgXh>4;2x7 z2S0NT5fbe=E#qbgL4Mgb7hv_`O^0ohnh8316rnHHiSBjoxl!L0cb&-1J#jK`kVGe) zXj`u}Ukj;k_oa_xn>G)nSOzh~iu$+6dr$rO?L282iC#WO!=K2QnF~Eoa^G+t@xXh^ zG=Xjw+}JS5z9sXAKH^h`qJV{$A|tB=M`$dCU`D1AV*(ebyf;AZ8)}7M3JeIP>1tfq zx4NSjiC!7)!WL5i&Iyu*WCrl`Fcy=cp-Root|+Cxg2{r%l}VzKgP_3;S|RlP_9q%> za=_Wl^EP&QDb}uy&qhqDl7}(SRD?Of>LV;DE`&J)3t}Kzqn2`yM)xfk%ll1uZ{6_g zo`f3dINPnlgMT4oK{YCN6l*fLtalx4A?7lcYAN`{uVqy4WUwKSE=1RjzGaoE`0K~Y zx;((p_w?jYv&?uEu`JVYQy$JpcA>w8U%m<{mgce}B#AHcx`;9}Way&}!(?*R`|s0h z-k75KQ%Naa2(torM!Ukt*?uCnHh0a!pA@~^n5{oJId%4D}%6eaz)(DqNM=NMmCsX)AfRC604Hs(R4!Lkt$L@ zBOY68BD+$%ezmq5Q_T-S6m|gQ}^BZGaN}3nyYWVx!#)M;SX7?0c4YG^^I3NO+pdkWI zmLxPWXe`ob#4xDn?=*P~qQjGBpcym&Y5r$+*nC<|-ra50Xf5jjFKDsS+ij?USgQ`u zi`iI)x50~L`uIcIr;?p)jd$_%GhkO+DMzB=~6qSLZo{tlu2A#ii&&x2LDf$-&k#!N1gU8byMcXpAnQy>C-}9=I05o zs|TI=h5~1b@)N21mFO`Y=!7Gn+jPGL(6~1%Oo@jE^jn8kDyV)kI%=&sKl(M<2!+zH zUs6SOlrmz|Zy{>gPe>$Yw^f45HG!+e|@&_c-p=*hn zSxaaBqzxL5Iu#RBqB&lnD)I8k|;p3DrH7uQ_-?0|iuu?XX6h%;7HL-86090JaAPnA>OeT}su2KjA~!0)DQet141yY=|Xf5kXs zjCD@qx`Itse&ZCZv3ZCIx7C~c-nx;bT!E2^s2E0Wab?#l#O{G(K<^-mEV=s5$RHr6 zKgEwT127g!rOI@{<;=qXW&Ntq+;K`FuR$ZUR%?V+py3w$N(n#Q<#0MKa`UTlMqPhU zJAsj#7A;e0oB7itH_W~MeuwU73kF+`R=^6`fB4z zC*9dPsFR*#ZZtiqtUYDwJ-_Yu_?_Ef1gaW-#U6Iii!kYuYa2cGD$E0g019y!s+kL~ zUa>ex_hcgcLlvO0s;A1~q?S=R$<}g|!6E;k<7$MmWrtCA^_X38^hQ6qk4uW`*dC?_pTEC}6=>Z*Bf-b8G08I4A297MTy;x3Ma7>|g`zBu);>%!E%{(%+^pjm`w zD6yn`2}q0-x|>T6@kY0df7eQ{p%W=vh6iB*i_tYLLCKJ-tTA=V^Y$9T=Xv%lM&hJI z^T=k0M!fr54oXd<-p$OP3#=zdy3*WO8c@U6qcY6rhJ>g@qn0%Rr(6oR`Tt~gT&0C} zFgEuLy`S)@p2yQuy$9-_cKS$ge{LQ?qEjmJxS~xC><|%CBT#{;i1!iWB$>{9{_p;+uRXZA9Ak8N{O>C5S9uh2eZNDY{T;%I5F5Zot8d*(hsvGwB3Q@b`0#X*me$h+fmQguQqo!Mi_iBYzI}!eGJzjs*oAU44Caxme`ekEY_D5nt#%v9xZL+GGft z0a6{Vfux^VK8lJTODqqxI&;(TO_CEjOX~woWXdnQQ%0s<&LKFtBylEPeiiLf;YP#a zg8YfqknwF-l9-!cCzOL{(F-edW_j7}PaF()M`#{L|AsyIpSEY^NW>CaXEL~?3V`eF zKXl`UM0+^O2W-{9bykcI(YfB)HmZbqE*o4wsVCdz<tc!@kL`|j(x?r=u4F7PdkC!7eA z?zF0+uw^LvyWjr<-BL$(1u}%PrXp`Xg|y(kf|G-BTble4ULYaIo1nUM;KLZ=%&yP4 z;ty|w#Z5Y;$8IiXWGtFFlpTnVE_s>&e2Xy)GS0B8`-gua+1-ofeCvT(p5{| z&h~C`jutP4qB>crfA@zT)D~37ezY%^jr6aRF4jJ$G-op05qfbkzmR$is=vY}iKBOB z|HwA>bdro|-P=p~?s@b7qV_s!qkZQHMik3v+^_$4t^4m&^6!RDf;dDz2erj$$=8uw z7we|?HUcI$ID->3c!mESiKdtIKK&R1tI&$MiqcI+yyCiB9e4KExovEGd(M2Q_`H3e zRk#kr#-_I_B+KwIhi=LO7*2gV2&!Ry=-{{G5o1UanQ!N4^? z@`0XA`>FkCJ^6#uicVKs6( zn;N7v)ieHJvZ};MA6XK-0y;-8S-q{?j$z97MXP95{37p^JEu1&n2EuKhcyv#a*Ajyr5G zJMKvD+SAzPcUEG+IlD+u(PDpqKwq_U<;k!U$`!=$5${8}d79eOD=#}lY(S`3p*Gr5 z@{nX$vy>Tqtpe+TOfTkv;-8<*r1H>)0CREn_EQ~aq!;6h>_M5A9#w?W=&w=BCo!Od zn{9llwhv>c6YpcC`Iqy&?g|d0%t;=x)%w(6TmCQVw2_aT7m{y;?^w;MV4)qGd1!Xy z@AI*<62j`0!IpyN45+%XxiJrCE_yv(I_#d~zlrshxbrsmYr2BYM!w!OXxf&-%8=VvwGMIDhoKldrJT)VNO=u;- zSgLH1Xmb&&m)bZ3UpGn%Ss{%HUg_@X^Tgu8W;H21hM4&42YLeX-ZDF!@X8-f!j$15 z*}U%uMQ91cT^(#LH&HBIf?Lq?do5R#=<14!P8*E?2hy4KT-!V<6z|?8z=ZTsNqkqF zu3<}c2ix530)f1uofG_Ym_n=T6^kju7>4CY=9AQoGuSpKXn06)^ao#1raat_5I)k0 zS&#JShJVf>$9o=Y83f9okxC2c_wXG|*Ye8J$JtuWzxP_Ld%B?u!W}SH7?k|=_Ms{N z!TX!715HRDW*p*V>B{-O^K_gGH1-3AS(k-VlmpbgVb;Lcpt_f=}Hb4d*B$Sw@jPZ zRwc?}!||Cb{|1PVKG<7kD(9!W@CnEy$;KOv=>!+IJJI^S+Q92Y!jLM|x(E;H--dZo zQQRZB92CawX2c7eGb=`(he=?GP_sA1e^9N(8IIP!R&*=UZPb~jx_$40-`A^qtzx6B zkQ545i2gApxh#o49qIptFB8{PS+dJN092@6rF09HGDg&tq&3UWnoCGIxc-@Xv{M+^v8?iff7Y{Xd z8)Pqls#~?Dr#W|Bs{kg`vMi;B`d6cy+zlK$Z`wjG4F5Xb3>e-+6K&DaSeMv2ZArvh zFR>d}@Kuf<_;Y>U95z0H#;wG1qBRcP>+JZ2VX}%SHBsg_4RweR=#1rx+UD~? z890e^T|xoymt295h@1fF&PoHhoyhsD{$HJpwfkRI#N*EDx~feVNGQ7)Z2P;A7FEM2JK5eEGmhRCXM0!ZNQlkPV=#KQPx zGynN<51KbCp4S$T#ead40gnhb2R~QelHrvx#eVmW|B)O1Gpv^}SZs+6cwQ52vfpU~# zhHNNaThryCPSfdyFA+^S&9SsfLApF+BlJphHUH13TRDU4-JNveI0>CA91|gC*ibh+ zZ#QCZ6e{%q!Rrn+!URR=519CXc?r_DNAq>7FNF`ck7|oWnjn2<(o}JH@6?=L0l8sH zoGWSRLI`l#77JDXu^$TH+>!~W7rCkn%UQUM>J7~cdTZV!0~~PL$`vG<_SWqBG1sr0^U> zIeY#WI1NsF%{p1>+UhpTZbt?HQ2Wxz|`qxwDsm6mb91bs#VHyDZKVa+5pd*_9f-aeG^bHU8GKgNp z_adH;Jz@wFW&<((MGOzzX^q0y^R{boAfq)v!Zdk3wpv}EgZyW4uqIVWq*Xg^0y-@8 zjzstDjS-wF*{|$VD4muPId61&QIUL5oMX`1Yv1hf7A=7x0V3ylwF+7+h8QBwi$7UG zL{pDgWiDZ4nxJYZ@PA)&F7cnS0Rlqd$hA=;(PkvE{rCN32ii!p+)fQ?%I~v$Vd;I0 zvk0kgL^P8JxcLsUMONWpiZ}&?L)%0fh)-3$RfB_MH7@K?%sc>PoxEy0WT&kMW_j-w z^#7J!=bS^D(xx%U38Ps5mdpoOzTlhHv278R+(DFVz|9$I-H1s(Ly~$FRI?--EJ_}S z@8c1QGfgO%^`)wHdrt2oO>aZVT)~YCXVHC6{IRXg^tS8n2_j-C6+(9@lcfb@y(h$`6ljT2gq^cmRNcn9QDR$ zQqV)AE{T9muV6#wQp7?fDw7D!C>4oJD-Q`zqu%=3`!koC9|aFZ>z_$V!Pwdx zIoi+gka$y=;eb-rxzI7&HVUh$WPa5e?*<6O2xfjjF&1JJD-m(pz5orly}3#6dRZUP zOH!$%hK(fQ>D&yn`Mrc=NMK=;vcqlr11zD1=);Ezyo&HH#@lBcHNt{x3rD|A58|&> z(>rZ#Ud)$|q3Z0C!|opsD5`%SDV6b`6-vwhtO7qsLclXZVvB0Q`WI7*-LF8>7g1}lI5U+NgTbu z<`310_GFYj#$`x%EQM>lJRgF1(61iqz~k@Q%}uFSOI{q~2C5zIDqkHy98@4^&)hvs%0lr!b+vTazcZvt+ z9LECWb{h-$6wL2E0n(f}WFW);U`P@h&wvIwjRQYz3k<1K3vPlit0arw$fo|Sq?I&r zGypr=3~}Bj8BhF6%ZI&^b}_#k?D4+ApQ015!);APVJMifhsU1)9)gbf`U)+Soz?07 z4~v%o?70Xr;ypNs);Cu3zPx9&h7#11(x`85ITl*kvzk9m%!O;*({Fy1vbj12JWV2A zALlu`H8iJ*2lwIlq9Z4&Hl<4 zli}Y~x2B(w45n@gFN3W+FlavSR&vk)$Hg&`noOutO>UyIDRlHzcmL;u6Defk3G7x`b!Vq8Ww*9t3`@~m zZYJ+&aa!tNb^=noXk}C66snUikYQGN~6v=M~6O0APCP8 z{^$qY6j7uXkVsaEppyQb5fvrCK(XIgSG)7YOnNSa~j zQWm!L5)(yD&HYVP80Xk-y*@M>XhO@y{Ys+`9;3}xyNGi+UdW(vXEX?0}%1X$O4WA|= za38@xW#NaAVM0Qvg!VCDDLf%O#O;{)0~*0x)bWB=Pisz2&AGcM9`3CYS5Ttg1Q-Kf zp)2My$tZ1PHDJ|~p0w9A{{oa0KXeo~S8{M3PN^vl2z~;3e*rdSV{D@(5ajqL(4^jB z!E1kV^*;z6=XIsP8F9Nxxx^nY%{}INk2|acN5~Do-({X#gPU|kaEpN31j#q1|ps4ws?%i%lD!r);mwCDfV5puT zEHhR+ZF)K*1jeUw{RSWtF_LY$=TA}m5X$x$$rdg2JLWP{4v^Cd!o@B1 zM58eY;O~QkK3vio@@84tE~cn_Djc@_TR=q$ccArIE@q%K{yf`Z06i`~qd3p+zIKFp zPq}eR(R+I9{pP@md1=g?3AaD*7zA6E5}Q}%R}zYW5ZgQmBDBywwqOLT?($c^s;0Mm ztk$#|6E%DMY|gt$)_Ju#9*xo?PvJD`i|V5bj1Vk#3u_fte#@BxpuY#J6ZxzOJcA#T zCJRX^e#ph9d}@$O28WFz`LU^9rjlS;~|tr)xb zBjyO6bHAW~z5pY6ql=~?ZaS$?S)StK!&UTO?^M{FQpJEa(uexFkv}_$jthpS>xQ?= z$sL>RgIe3tP|<^RG-y}cI6jgu1a;|RNvWO5DTszjtE}3ms8@FkvZIa}CAF*9zRf!a zJGHG9C9$&?Wu98!f41`EsG1^9(g^s1KSRV)7(QuW-;p?fSx8dQ{d;_m{rB1|1VX{f z-7HM>w={7wa?8UJw@LE|Xd6LL1E{8P(biQLH(pC#K+z&-{3W42#LeQ{CYl#Z%qBLk zM1KmmkG#zVBA5OLIU;v20GY%&zr!@qoURMnqMa+PESVc}%{uRAGq-jurWq9n-h+Z} z>Y^dn@qPpt{?s)cjzoA0XajAMm@X6=GwG~+aa z`(#5HjG!S<#xd2l%~5d28LApWt;>5_(7V-=b%^I`70AD#)l08IXE(1H??PZw5T(85 z5GY^bZn@vkOM3{ng)kl=3>5+~WAN)rr7FdlNqGEkoL?39RBiqyDx%QxX6vrt`jM0= zTjR0NOi#F!sm27ZHe6&+@A(C79sZy3FCE_?;$PDL?kghY&ebtI2(>Rl7te_36)r5p4YF zPrr`&T`Ta6Q*~+D`UCJu#YFQHCPL?VEt!sz#IA|l06v;$r#8vAK^aRvig9r1|*r)~HfqaytN3NmT~{0QA1 zX^3-AbbqGlpM1CDEQfVt)g9Z}G|;T>X{xjI?B+Zybd+f_^ z|46$BCo5ch!rEoWELw}(Vbv4VA2%V(MEg+7s8BKgvt^oYnijztS$b~j^=PbpnA)0Q zYTLN`YnNXnY0G+w91oN}B#SUMV#P_~DqV3Qh2B(_%*qfH0Pqg%Hl@sn=w;2lH|d@P z9mCn9xo_aexoJm(_u?lOgdz%nmk<%p z%Tt6{=Ee>sn-$fFH=saqFR-nh=i0uPV`yMc=y%I6gu3eGrq4WNX-#e)T)g!2Y0%-I zMF3z*2L(xtTqgIq?gDgo_ZHsV#rj{6EfWVqxywl z!{dt+C*y~-Sp1x9%ECpqp1JA4<&ua*@wO|G=@bDl@;!9q=nUwubUOMme@y*C{^ZAT zjCgyg4~IT?g{mzo?S1yxHHI2AOVE4YgZ$R@V|8N-+XHov7t6cGTi6e!5g4e!Ui~qJ zOmszVOKcOH$jKExe>hj!n&HjQ<$EB6v09$8uCn^Q zKkqz~#b3A`2LAo3hmW$((#L&sN6p8`440C@t-n*iZ~Rb6HHfd4XxKGJsT@MKh8g(9 zGzxa;IqG>RaiI0;HMnGC9{iMo-F58IsB_s=h*==xH}=ZYp@Vpf?I$PEC~<@f#47Z_h3N=cXti$ zu7eE{oMC3peCM3?U;gK2ueEpe>aJSd@6&a$yQ@N$R^na5+mBPnuzWON+uS8^(`AP; zi~x68_xpi7Q&-^G2)EB#aCyrX-%v|{9FWE^o$uvbVnd4#zA1Z z*3DFqfJT7x`indHi#7YlPq#l_2K5-5o97LRU*(T{>;kSq&7|DA9JoVC|XtzEBd%kW8<{vcOLuddM)=UGqkTuPdNXU7}M;ppYix$YYThJ z&@QVmhQW`v@4G48yp2ox5EHi_@Nk^mA@u8tI+hQ1m=;0 zEi_oQ{$_lW_bhJNp-Q)qAs+@8(7JgkIN4GyHzZ$)#*DVkXr~mPG-{#kTH$vo-bz>o zV<~s)NG9LI#2oGsE2VDDB+I43kw1HqNar0I-^8`;JqCV}9zv5U|l}GJ({bC@6p43Z~ZT1Ot4V%9k*qm=mH=GC3 z^#5XTZjfp#UQGG-ab-ELTUI;dalKG7mL!!jj}i#L=v6&u{0n!lFd!UKd!QF87Fo{z zJ#O)y{B4z7MBxGn(bsu)*bk5LG2w5jw5aq;#Hx`y5QSnxxfeb~M>la@TfRM_6-jf} zzQOpG$m=*FD{x%9%F$mB`!8KUrr%2%(DU&=h*A}HlHGItfQ6J>GR{*wVuO^PL{&*D zgg*qZpCoip2|e@loq3i)q7!^SFoM;%Xu=1n=%AkK>&5A@s36@?t}cz}(lGb6ZurIQ z5?kN9U~{5^#x)VCKu@Aq^JcI>40~^`7oUM~l4SVfkW(wkn^f>wK(t4xg-q>*7PzvD z<6&Te2PDVW7W6DWwrJhgKh`Q7LRW^kSoXhd%7LY%=VYA4hjq9Y@hpgRAyq#L>G%z1;29SALaqOk^-9 zHd`?Iy^=C8m+{rv@bIJ^CRrmg&)#`&u3a$p5Rs07aL*I^O^PDUlY9reTSNptbo50$E>-lrLp+~KS9 z9)Sa7pj2P>k9^M0yzITVKenNHG-WVcN^^}o_8rD!vYr?TS@su{k=7U+ClX7U%VTe6 z*UV1@Tt{7v*MAc+s<9#|W7<`IMYlt%yS_SfsS=do^Y(@Ls5C3z;&OaDeOy!_LrD#Qr%-PNY3$Wk=7-SYKjA*I_dji|`pG1K z7h5JvSx>*9gO9eaLfpavwMb#}hB(!c&+;qC--uLERWOrw2&KM!;m8+|{=RLunGV}G zK<@RU4#m0^H`S1*)H3wm}%V zbP{EQ3nDHs!Pw3>SXANr8D7MQhx8$FS3!x3PjxeS`J)RX$Vxg+O5ya3kAb?}g#6xc zB-z^hETB&agk}8ZJk+c8al1GJ?{?7?*CL(HL@mZ#{~A^Lq~VKm%CF<-+~^6lzxiiQ zvQOfbUBvSRO%L`?$6*63qGPEtzJFN{DS|e|5RDWuV;ut0pFTJ02!dN;7cf3a0RIYr z3g~itc`sa@m8`K&YFsLJRL6~czg6mADj<|Oe0=P|LD;QtzVCPNh=_W&vw?8GD3N@K zaBkcZt@8{Q=;8=@8yKsTvn~PKsF4IKRWstexQizd>{%;c*&5ktSF~g}i^X9pPVqM< zWiz@}8D(M(uNEh6ra+kilKOp)EV6Lp*#oZ z+1K}p#`IVYIzIj?%>lQE(R8Y_RCtp!zU*dR!+9J`xdu?P3xVH+e^ClG^yvk1h{zCq zlU_->LTkP;nHe#D{Aro7(nT68i*WdJ%2N~gCxGbSoww|qq=2Qht-n&ceW1%^&a&fK zhsvZ&5XVa)oBEMbR)^I{fycVn+m8^Ij@QAo$8mUP+u<%`8>xM-+L|j-;E$`G3Xd7wG3QW*QtKQeZ7b5zYo0l89i zrTGS~ufN=0hqHHlfqU^CjI*p&#mwQ~N%O{Dm7Crmc)JB!5T6Tw%1_N;3h-)s2dso* zcD(Gb`8B(8!E~$Cl1H~>4)+nK*P0SH^DTZBWSdj3u6V+6|Oa@2S3SPfYrETa#(65cbufk#&!j|}0Mz=%JkEhS&$Q2q>rUlhec$I0@H4oFSwMVYs zjqwDE1t$mabtMY?sBTxFR_^R&?>borCqEGo@ju{;sxN5DXjBX;nH8zC&Bop3=6_@!c=&E=DVN>hld=yCCYT!S_arVe zd3vkHn-oHjdw^hNy&Z)qOky1?SvWkyF-j2JPU$mwBa)uI>nxL_jTrjX*AMsC!!Bc>vOO9nDX4v05^hBde7Af$`{a1l@Y?etx|s5d0Dz`$z&0F(MZdGfq*fL5|2 zEuSi9j`_Dy8%fN4!oe~DNy9T?8^OT=`(=@6GodD)XsDualloc&Gh?a1nd>mqBB@@C zUJDoL-f!O2BIUgLqWZOOz;kG&sGS!s18J7#?bG7sUC;*j2-?{NhQ3FbsR?Ucsb_+C(@pU1|=g#bTl&f`S!c(7{H8-|JDIq1% zR}^ANUk@^13gj6D3aJ@ui-E>M6FhtBD(C!G^L_0Q_f&Os)-gAt2?5?WZ$Fi87}&*8 z)h7uNk4bVB3&R{&ncgF-1{CQseMn~~psZpWKF)f||5}R|@J`RIMhW=NDZXJtBTf>3 zf{mceZHqGX*6Yw;*#9aw6qFbCS!oIQ!Eh;%T}~_dd78_$@$Im5$7udC{#Fl4h`wtA zIAOPX!HloELN7ec-@qob);4QZZ&Nfr@m1u3m>ec_lrDZ@#g|0;&oixWp)7!GOMtGq zoF(Kvz3Y04?FrOrM7mcSUqUb~H4B=)B{f`{P7|AzSOY7s2`qZTE;N$A-!;|sqFE0xG>WRfbfd%pl&xZ;v}^I(%3%7_P436s?u z2w^D4WRTE3App)4hp}o0Egt_6=>YiZge=fw_p_RbbY3*N*@PL9a642s4&$!I>Zocl zeZ7#^@eI*ud#|oi3pe*&U$ILgTwo(*f5Iu5N8YqdcHvBjLIm}xWA zE}zKIAPY^dG~E%dEq6$EqPCjUO!zYS@Um7keDa5+d?|N98I!eUHeOgU zZ(LmXd_*a6$o}g^-arlE)u_36mT);}(YG|dsJ59{8aKSek4&eh%KU7Emk5CpWLi?W z>qGv?cNMS$j0~?Bky!~SNT&;UatzOTuN#vTEQKOi>Ot+PMmF;KCZC=+Z?|1cr#dgf$_Lq@? ztb>XhPQxlQ$&Hm`gs2c}qyx3AiZHNqTYEvX)R1M}E?2OOCR;pcJSE}8%HPfGiGPha zc~0N4^w?QAFa6cTK#BI3@|2fOZ^bsHMX&c;iQb!+b}2S-%-~5gZ~5IIX4QJacak z`5#*fCDKlbjfQ+KmQ9QT0H}$2T87HV*HkNfpauR&B+X+k{Zl6GQz{iAg#w;P_7qF< z6ixgPNd&EAgO{F+aWu9B&46Db}O$e!Xz5wXOuXrhw{!t*Llcr7Qqiv6;H znSmJfK9&>_PYR78ZuTZvujYZZ@xhyUpbcCPiR6eRGH5(0d_V-zEA-IL4+BynhkHz- zfRBm8hDC2usNosZuvDs!JU-a;%ly^P@{mEL%YK#XUe)WKN6?GM9C~OX`ArN7JdYlh zO{coiW3yW$b2Kh{I;wC!qoeh7>9y;= zuq`FTf(&9nFHDxfjm=nsIG81 ztavpH>lH$D2|z$xG_6~79hmk+4&Q~X+a*)x-Q3=Gb*j={}dHoJ$ z(+)$|3fI)4*!C3CYQ0_c@^Ip2R^e$vlDKN0v}T{6bdS7#k9lB?XLkPnNaN{PqYIw=^c^R0X9E_F*#>_q@{Jlrhy8dvg^?34+OOJc1iEDAfYcqV^wK#=G zxW&h~`MW$b^Lz_)SUHE-d8f|`PFRPQI3||4CKu2%Pcd@Nke$%?Y%}$(vkb1V56z=^ z{X$KHpk6p`>1*reC0EoML1iVP);(=@5);2zEXM=>UES7}lgsg zPyi}r2k^(aAB<#8cezHQ($2m2ekkP2eT)Dd2oL~h>BSbfveCGqb31$nO2s-c20$Wy z)HJej)g)pTr-&o&8zKTpn-K`B@EW~4giIa+p&C^oaOb{df#<44Z`Sx7FqD&4F9PKw zyL%=6NW}vcugL^XpoF$tFsnG>zTJCwFIEyShd>mHAs*gTM42HFokSl)K{p85#NU`d z8~IsU8xtb_q8LE16B@9=9+ZFet*-rN`B*rg}L z2}m0S4GT~`S$YEiplJOkt+SIuiP0+kBw+1eV{ZrE+S^ef0dMc`QV^07lM|7VsFJ59 z{7k|We)n#FkBo5VhsG$EK;!QWDFp>Z1KghJh3Ri~H`j?Vq5>jc1!J|fCdPSrDl6RG zp+3H=CV>zt-8Gh*A1LNV1S$ium6G&mO+g`(kOup9z%Q*N&u~3gO@PF^5~&y)Q?HzNZKL4!+#LnHu1t6 zxS$p6vq|)~v81qgQdk@byjKX?&VQ3Y4*&TN%wS^@_2afGSAA-?y$@NmkMU&CbQ&bs z)Hb@mZl%dWMwPCR@aTtk@FV#I&7ytCXM`6p@XpQ&uFTeM`iTrQA}Ix*l)7o)dnjjv zm$TAWbiEi~=ANF1AE;h!C?mn6dN=UY$PKUKr0d*Z>{*3Dw4P2h<-mOk7sJp&QFxEw zSv5CB;|^8h24@el+nHC#V`Vu4a^T^IDH+(X7*W*$amDuY(gV`keYTMm=!qumNFBDT zV!qYTzhsYFbc9y~#>hNIHh6$&BjNFNHi;hziEJ=d?kQH*9@{^VSVGGidAKZtoxU$M2)0onvI3VdtOX6(8BSPB|cF zBIw^jCssG)1w2b0{T@^{B#P1Igu*f^{r(HxAM`O|7JO{#pURZCQx&?cP+kjfhRGQ~ z2AI*-1JEJSPZ?lRC?-_;<`emVNH@U6fqpj=Fg3;OY0VAo)GC|2rBwjZ;^4CrO`!du z5Ac;5Wd_)GVu}rw_=gXt;iB-r(I#`92&YB5(fwZ`hM=mjJCGYweh&?e%??YB&iqa> z2u2DKU0

Sr6s4AT}=_6S5Ap`q=R32pSd659`jP+8@6&S|ixe!hm9tpu+N!$!=?F zr~YnhH`Kk%jC3=TudEdnRBmpe_1Q9EVJvFhBg+Hx_-buI0s{Vl0m1_J_m2-@tf;6| zlwZFpD)jZTF!y{^QbvnPTWY=!SBhjwN-|)KnG-3Mo)s1feP@i6Ye~|0>}wMPLT8jm@Y!H33Sz}5aFI6 zM9Lzqz|;A*0sGs=2A1uU!1nk2dGF7knQwr99SAFen)x(eCO;F8y2C~0FD1YxRTPcy zPWVxkUYmeuz}Tv?7&Fe-!UE{)ZW)Mb;H)^#eHDv$`dkZGguJz@^MA!ZNGAUqt{|0H zpZ7Ch9S`q5!>R%}>}62!+(T^evyO+ImSo2wpu)su4^3nw5(%)KD%gbSev^*HZZ&3( z#&c@Z0gH|}Ck)w6fh0&NBJ62ib%R}(3@$VFl*_#l2W$wQ-~4RmZZAt5O*^2Q5}Xr8Hy@c`#pM?kc?hFWxRXr*mUfUCXf4ka5DD~ zat6d85COB05l#(P9*cQZ3EC8fVdS~?&vN#rce(aF9@xp80O2{{FBvU+{X>Hoh;xI` z{$e^Nw1y*VbO8wv`8|-m?NwNaKGTGaF{P^JLB^DbOYWIbn%eT`*!^C1H36=O8Z-M> zkD~88ry`eSo`tEBN4>w7OWZwUzlh{WM1m8R6zepqGcGMaV7vWY9b?K4b6~|HVG)ec wi>I@ws#sZo7or4_*4M>7;p5{nr2pZ?Uu4>Krr0kU)&Kwi07*qoM6N<$f)&@lf&c&j literal 0 HcmV?d00001 diff --git a/crates/tauri/test/fixture/isolation/src-tauri/tauri.conf.json b/crates/tauri/test/fixture/isolation/src-tauri/tauri.conf.json new file mode 100644 index 0000000..c4e9c6d --- /dev/null +++ b/crates/tauri/test/fixture/isolation/src-tauri/tauri.conf.json @@ -0,0 +1,27 @@ +{ + "$schema": "../../../../../../crates/tauri-schema-generator/schemas/config.schema.json", + "identifier": "isolation.tauri.example", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:4000" + }, + "app": { + "windows": [ + { + "title": "Isolation Tauri App" + } + ], + "security": { + "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: http://ipc.localhost", + "pattern": { + "use": "isolation", + "options": { + "dir": "../isolation-dist" + } + } + } + }, + "bundle": { + "active": true + } +} diff --git a/crates/tauri/test/fixture/src-tauri/icons/icon.ico b/crates/tauri/test/fixture/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3636e4b22ba65db9061cd60a77b02c92022dfd6 GIT binary patch literal 86642 zcmeEP2|U!>7oQpXz6;qIyGWagPzg~;i?ooGXpc%o)+~`MC6#O`?P*_Srl`>>O4^Vl zt=7su|8s`v_4?O)M!om+p5N#5ojdpUyUV%foO|y2yFUVfNMI)j3lqRqBrISj5XKP* z1VzP8|30{X1nva{bow>8iG-;V5CAR=-#C~+ST9E;Xn-Gr!ky0h;1D2Lf*4;X82+F5 z^O!~^Jf^7tRQm(w05$`n0FD500O1jY`PTJCTr&uF8&Ctd3%CcU15g0^07(D;)9Adf zstIlhAP-;y5Cn(-CIB#7-_;YEcYcq9pC`~SCax^yT;tqFlpu0SAAgb0M(%>+U?7k~|H%oqaU zG7;{Jz;i$ysD3TnZ-VD-5EkR2olyjs0?__2E-*ZQm7VF#;NSU+_7OmYx`1^UZOBN# zZ~z&=UqaKwI`Y#Ck2VnUWrsY50ipqDyIunt0QGGg8gr?2RTL#iQ3}^>n-k1l{K?P(24g%0NBOjQwp>0N6 zhjzBRS^h3uXS+k@hxlm#X1Zv9Hv0OTvCgXwwP zq#48g-{<`$)9@L955ofX03HIiAkD1kBgDb{vAtuK;{yB_#QPb z7^H|%!06@BiN3iB9Ci78{h)m}hG)EA_Y1zH`^*1Wf4llgsP9;I#3BHLhv)*3H@g5R zlV^Z+P(Cg!<3L6m(}8Vg0JP8Z6)1FRdI6mvlhg2JHsAe^X#fq({sQKWx@-!-`2=vgJA|ipM_2(ARW89@<$pz0wRD0er!Mg=)&?pq^Uuj`CRX?9*x7azbOAK z@H2G-^F}=%gkdm!Y=a>`Q^09J3jk?AHwd1ygZo_)zQ|)8q{l2D{8#x>{=D$a3qS*8 z111CAXbTwW4yLv;z_e*M;Xm3zM*5f!0C|LU zg0Iuw|9`uKynsF=_C>Le(g8pk&cc1r&p*nakv`gza{%N4>RJSp5&Mw;$GgsaI*5=q zmKXbCpZlKhA9*1IxDCMk>j5T!|4WB?1IvT?0BiuDe+(M19t1$Sg}`OV0>fk8pmV72 z*#F7{U_NW0eAu7a2&1HW%{zY}3)Up9h#SY3NF47`W8{X8O(W ze>OhDK0LaB@qi`(hS@cO+Q^{od->yi%maY-6m1cfpQ(>qnED85VcK)M(q-n4ZhYr6 z?DL`?bPNYS@*baIA02u2N7*x;b?F+k<*G9Px4US_gnGiT>6iw<41l`L%)cG}F9P5* zCd}dgCjf>?g|QY9W!Ign^11>c|FRO{UA~Ycj6Ga{hP6N!@P*9aA*6#kz6$UJfa8a) z0PLSLo}&x!1~BPEU4Uop-N_!}GWdt%ozXHBy3E`wDI75VA-wBVTOGd0>2?(2cQ9fd87SHgfKkd{y|RPf7B@l#{7Ukq=937 zOc#Ow3jj#VQ2-6_9>9Fw2LE>h7~|aU=kVuGP^Lf!^3@q|AAsdz=JPEV<>d=;gux{Y zr8fO}CVvtF`Or1iSA;ZI04@NY0crqf2Qbg8fDHgW2v5Q|Kl{S^JB<1Pbg6?E@=*d9 z00sld071yJ+cxHB)Ap;SM`vCXf0#BfB^<>kvv01CC`J_@zV+k|RO1cjR9xrCYoxrEvTxwtwwxwz<|Ttaj%K_NO@n-D#) zNr4^!2~!9r^m2kfBuuAwurYI`<2*$GG7aW4KF?FYzrJ}2WJ=%F$ALZ$^l_k%1AQFm z<3Jw=`Z&D9AVFj7Vcf(hBajw0PLk8I{=n~yu$%I0l1F|_gft6 za?!s75C&KbVeKIv>~A1Tfy;$^S>XP!%94LQ-B@QI(6mS(b1{&Y5y)*h$P4#F-2%J> z;97ngfVrOkM=plL@Ku28fHc5jNOw5wlMyMV>41&U{MYlew-@jM$UKSWi1i%z1sVeU zKu$RT+^g7KS^tq9eEF;u(!{-I7eKdsAg{ro3%svrg3zYu_I6hNtLVeJcZW6<_r{5W z9Kf!t?gQX{w06LkGW)Ckqi#J1q=PO@02+j=XySeC!(Xgr4?*rvXo^_hg@NZ&fcK|B z2DlINuaa|j(yf8~j{!Y)ppOEuSE|n*`~`aO2=*ree>s8Aroiumy+H0?>jvsU2GBPG z=;Qz${R_D8-%ApBNhqbs;@(qPsP93*<4VBSyzfo^a-b9TrmIOkfqmOJ7U{cs#sQQ) zjN@?6E7p1FcYWRy+?(Y6En4vXkrP0-VF^tK#w6-JW59nn7TQmcKkWG@&j((X0=~uP z-hQtH=${GYfcI4T+Jo+@Gt?Wj_aeZ%V30fWU4-5)>+jL`7Rs>(#)^V{I`GFD0J6ru zJp$e{Cnta(-$VKyUw@_h`2Ke!0N-K#V2j;&S(5D06(DAN%k8`()z$2V%`%#|b`*UD>8D~&L zfjyZ4X%7X+0)!wxe4mgDfbZ8~`;2`JoL7(s41@o(;6BPL5AYs<>HR28r~{iIFUbG< z@AQ6yJ^$)kD0}E5;k#wH_VT0k4(-N0KqT;ZG^8y7X~P(Twf+~h*GLnNJ^BG%;~+iM zg$IBi)lFDeAp61^B&;{GM$^Ah34q72ZljHSUI@JXk-0palP!RBya8n3E&I>nZmDB5BQO}=69e2E^yug@xMGa#CiPk&bb{6;AaJ(r}h=s>B2xhYWHEhjXL#L zT%9(7@eZyQ0^+7G~b+gU#t=Xw1ZKfZik4slKJ9O2%+pQ3AyfCw(M=Qv-4dl$%aK>pZ2JOOwN zfOhPg`f#K-+qWO7cwd|$IUdSh^PTd4DRbt393%OH+*zK({SkV9X522Fz`f}Lpc85U z2Po4f;6Xm%%Q??i@N5*^Biy1H{!9}7@wA}qI7a7yvc&_Kvh9w06?mcm_{Yoevk1Vl z0N_knRcUZx3`~Zz1sP}f!rBEn9PB^p%FoKKSEPgG0VqH@3s{gp&Z)SUG4}lad*uJ6 zK)Uz>^@6dsuoB7}0}uy%8SIz-UqsV~ecSl{6xkli)d1*Dy~i-u0J4Bzy8PWC9{V-0 z*AePHSq#dH>(bqc_Dh7pxzb{qHVNdv5z5tF+2eT6r+_v9*2sRm?(d~}!CI3X@R+fO zoD8(s0hVAMoi6GoSrhVtd3{CD)xLeZKTEk#eqiT>f!7yVkUy*kGTy)ZVKPwvpnl;T z`v^!A_m!0Za8DNM81Cyp7yIPcH{S&?g|I)oo`h#o!}+OPa3-cMoSP{J;MVKGIjld- zfPXjv;3wLCZE(u~-L3ywAUFOWt@~Z=E9f4173BS_oB6+h@arKi>__T(KMc=hA3|+~ zb5c9-T=pVBI$!}{Am{{t*O}@6uyp>~?DJ_RAbZCAIIfj;x9!KdvsGm@d9WKjxBXw( z9UNE|d{;sF z_vFHOopqlvmjeBWZs+?gx~d^9E1Z`t?!kNBAXAV(T^aBIz?A#fE}m6h0tf(IQ5`|8 zBf?qzJt=yxi-YYa)J53m!8nWITm1djy=;&_w%I)@Pp9nFFwdkPlzkU%52T?`BIXX-^U=z+^%Y8wxZC4R-LQx=SMZCZEb4{{Hq(rkziK$fgt*zYTa{eX}c zj`x1XI~!fPKn~tVTZnBLOC$}2?{jXZZo}_~g!DlEs0TF=HxwX&x`gA2U+L`|6+@o_;pr6KgrvTE#aox*ecLry)%;_6Z@) zze9vSlt-8R1%ZEO0pH{A*Y|h-$ec@8|6dRC>+XE-*ZF_#$2kC8J7Ad?(1(ZqUmMQr zYy>dBMaYzAPh9-=*ilGV9_2rrTFWv`e`kbF`7_4i`&f|wg~zbBzbE|0vZ0NJej2<_ z%J}~K*Rt$^pA2WYsQ2hy1C&wM9B_a5KMQ3Ccn9c-?3r=e!4B*Ky%IzF(wi@o1=@0u z1@xb~UH^+g_DT@GM@57AMwoNPbK=NWkVa45FZohOY9O5{xE9fq@d&d3Aa4SEn;826 zI2U9MI09gPCy^;vR@^2?%OB(q>x;ct2XOu$&%^_Ht^ir!y3Uup{oem~5ZBSp} zJ1vSD$M^;`GmqZn-i32If%hnXJ8*H${g3#~e1?2qih9H9c>Bw;ceXubDabPwz^V=a z4XOvhe#wDL$bzx|&%ChzHkA4S=JwjPpdP1!9GTy%{+_JAcmEF5e;tSq-{t)DGfDhu zX<gsXSELq@*pp%q)9^DAK#0I_4q!_Cj%`o79|^koZSIofLK5{ zz!RR01i1?r!h1Zdj`M$%fjCcWNd3SL?E-$Q8^7iJ2lf41&pN0Ow|{T!3o>me@YoT+ z%9_k2kO#~i{`cF;d$hq^ou(?_`Ave)BK9R^tr0vGp%v7!Uns5`xJ zEYR5oFven+S&%>4fCmtF5V$|3FZe6yMOR;d2(n)e!1dqm>Od{%jWzBqAJNP9jxo;c zfbXzDeO?N(WOY8~0Q4gz{#)$;?j7rp0ohYnkU!{2M?BaN4(vF4z%Mu@kbVPpa5hq-y7QiTo1TTGr@QImiNF0 z;93lf)79`S&hE1DFA0b9EHGz70zN}uy`2x{-?#=-o5BBc`(04~u`h@=Addz4*F(Gs z5FXlq#=oTeKawcQ4rGY)>a6SuVU7uL?rsk10N8^cA%o?(U{|4E*1-n6RRq@&_!|Mp z1i+eZ#~yHTkDo0-dNAzU#Wws$FRa58s1?`__&~b&o93$w4Xv0I@sVgJ>dOuKzIA%xSp2=P{uhq)S;eUC_{iCq;(R|UHLzPu&RKbX8V`M zyANkVpxmJT;(Nh&dSC<4R>0hV>LEyDa50>n0Q&S(X&yvv0l8!Q+XnA%cU)nC_e>d~ zJ-|Ji3Mhw3)Q3Hy58HsQJ*2*nPIvbT)IiuVm~U^r@Jy&^S_taE6p-VO?9(ZMG?u~m zQ0f7siR%qN0Sz_)Y+t%V1KKH9 zoCkpUn!xbLRB z{lIU9!!;u+U^%4AI5!Obvs{oae)j{nCwBj9IiUX#)PMe-%b)Qcp(Lb31AHs}Z{14( z+2eX5%jN$&BV^Mi;#w@~K!0%e1G>9U@LTd{-oteR&(1R=S?d=t&*cCcU;(_wcJy1k zW%b^3kOQ9k(IeJ&jRE+97VLv|H}8Eg{^RcL^&c66?`?IS6QK%ogN!{oKdJ*bzl`V1 zqF%AYb8Pp!*3ogS$2_;AyFCA1IA}vUrlW2#-U(ufA_AlR2i?KTaa z|4eX{70&5^i#mXI;OjkF%(~qj7v_sqodJZ$`K;N0=&Rwp83}mzGv3)@>I3SL7s|gU z^FoF&7d(nu3v>GI+gXtRIS7m6#(zejJ;=2PzNvtA0P3s^$Sx7U%6_3Q^#bMZ(kXux zmMFpcX+o{Rb~AwmUNhzVJr~DqJ_aBQ)B#p6BbY<7pjP4jutXMUIuBugDfu(`($yyv z279m;WQhARzm#ov{^R~Z_s;KXXfc!RmJ4!+z1gj}_8P_lufHdE=6yWdVMZ~(^MnwV?1SGI!}(@bF0{|cGk_bQ zyYqcaIe*W^ar<~o7xsCwLJlJ=>Lk#`1M&9*zL&?>_m4t*!Pk@ahGhc(q6nx1xQ`#& z131rxyaRLq=6$YR{Gma zzJKjv+mCC7>^~@fIf!2f_&WXX`J-`7`d6<1U+M?W7vF?&Vprb~&+f%DMX;auJw3qh zfy#p2_%fMp{Wqr8b-l0IZU+3WWP#`3lEr<9uM1$bE8QaCt3X|Ghk^SF@U1+)z6axt z4li7P#JmD9J;1YA6hO9~;9dfJYaJQiBQ@=b{E=T+Z@_+HpKBHH9M|){=5crY zZ$S<&c#c<3>mkYy`;CylGoY!PbbJK5r$ShQQ7=Cupr^Wt?*+m4UU4rGtO2V|03-m4 z0L=GHVGfDB>J?1{`;k4$2G?!j-5ep{C5{DHeP0{j=UWEy=SDg7^uo9RY&+rs-O)J= zQw2N^TIFQNqc0DH{Ik)Q`T;3mL*z8_f=#Q9SI&fVi$Pzm7A z<^&n%I70a85buZkUnoO>G=P=4|C^w9xNq#2k>k%I6lD!E$Mb_k;J-Ya+rYu<81QRa zPzS&kumMj808fJf*8r~p*e;+=hBF)KF9B4LyAOmXgWbUQyT49~CBGr{Bg6JXnl_Mj z9iY4Qe>dcf?-8+-Uti!q<^b>?>mu#}lmd4IxDLQ)C(sK!_&)?(c=w|9r}eoZJzO*9 zguD^~-IYDsAI7_YJ?(S+F&F-sr&yPuKPCYDkc0odeqHlta0%py`Zf?y3h1u<(GD2` zeg+A>CJmH7jLYF2XU3QuZ7{wc1!Hsuk9rNAKZ_77FN_;d&vEXcyZgRSN6tcAJX7Ll zkj)VzJmUG@7?dzT}BRtvs|D|2<*eNQulF> zxHp~!@o$qqo^OLZfpU!l_Z@&~4?n{H2LRY_+c6(p$nn{k$*_)4S~= zt`8bf>ygemKr<_Se$yGf0cSyf$l$`c znLqYUMtA9DH5|@2;oc*VJ=(Bhz#ot{IMgtn2fe!*(qze;$lA2271@8aaJ$RF%O z;W^skfL>QzGwK`WSYHw7Jj-I)P!}=*zwCN{cLjp|0L9KaG8@W^^DbZ4gFo`adVa?y z&>tbxquz2s8K7^2?-$Z>UST)j&*m7vF5@fE>2avnnAX4j>KY4*LRqr_U-RP6{J1s} z0k&2c+mnC#!uJEQO@nga9Pcgw_F?|43|~Lr20Y>Ejdty?;IARrfUbVPSm4!*9`FnL z1Re3vACSiOwkLaXenz=akAZefN4_)2(>e$Jgzw^VohZ1Uv!!nXZ28Iio)dbPFRN z{)-p(1-p2Ob?8wK`G~x&1szBRJ;FUU9Pt0Av(ueQCE&aq%t!G+`ePuU!+@UdD?ys` zAsu`t5Yp_OXFvaRCVnHqPCMEG`?Wi8JkY~4lo|C8>r**k69Dyq7x2UVX{_%?ARnlw zxOQa*z&RS+pYg3a-Q9cTkd7suCI4To`(LU8w4*pDfb(8H09N#9jjCVIk=Li7z41Ap*tNu5T-W=$!;5$m+rQyH! zptCQ~j&&>?c#Ly?tn&3+;V~UtTfn)MRgm^X0KUg54}f{3cHEN<=d7U1m{(E+Kc3Yx z3E&GrnPdCj1o&3^tloomioP877;vJ__g%l|0Ms|M1Gx4X1$_EhI>3|>+6A;NINrPm z$OBvioCDco{~gyHiUBVH*sk}aKhMnTTP~jSz8dQNFZ(^v-%IPS@!@$F@Xa;cvx$2I z>H**4<*#<{HI!!w*tq}99M6wvN0%MIws$GWAM4|*3#ScKo77F_p|#1U)Ix~`5(`5 z-Uf85sx!uT|E_myvx$&;OZ-kKf_Id8od%ns0LX*Sl#5_0|}^-3#>?)|}~VObmlQdn`4I zFq3-y*DF*X#eE#;<3Jw=`Z&0DllK&!ua>irA=OR!#{huigfYLykpEG3q4fw4D1dLk#*$?DE zR*-2|eh?M@!Cn8(8*QB-Kl__HQx0Gf*wo1@3e#WPNm)6QBek7>x*W{e1QYHG_SsJl z=qeDUE90iF0#TTReeJ*2NnZdwFaOL8Iz0eH6~IRCQ0RQj@Iw(gnEb$JSVU&|zz;?C zr+1PG_nH2#{J;;)F~R$c>$AU$uHXFrzkAMP5U>a0E6@YFGWgBkN%U{=J2U*v-M zci#H!FYoks$pa*&z_`)TDL)W&XFgr>{4DscijKB|A^0u_{gBz`U??$$pv!^9jH}Cn zP?&y3^+OSwbUp{aKf~g5`56*K7QtP{6@VFl8SL^xOrQ|O)^&jeG=bos{ZKXVVo-rW zx-2MzO7w%Y@cL{tATC}C_zW)~2rm4B7vI|oS7^3&4^870BpDV)RJjwhl(t9ZRT^x0Gu~~X zUyxI9Re%$v?0t%aStR**yJ?DTL7DAhf8%VnRHf9y^ZKv$4?j)S3=oN~a-Sn2RzA$9 zgpFgDM)fm_2t_1F{*eAemo1~SO$B0z#{(X|e}3IG)zYefm^veNfY~s@LGd+H3o--U zC8lnpEjg5yqYyRzO;E-**Rd7i6zUOV`%3ZcRWtZ}5 z?fMJK57(U9a>n%GbdJ_=2f~!`C+qIBZRee7d9qHup+586v+DuMLTowGsa1NL6Zaq7 z`&eD7XoQ}}xdXhJgac6voy zpi9;Tt4U(<3EFv%=8{_VCS-$Q96q}Q8Vwbw6PNKS=CLWAZJ@hJ%Ef zoD=7(_Me)6;DY3$U7aaE$!UW@_hG1(cM!gKX$To%9va(ZaThX za1H;|<*Bl}ZIi1-*4r1H2*21Kowoa$>k;ke&JwQ4hvx>wCVN3h-thM=le9~$IodM} z)t!^}DGN=nENZWOf79;txni!k1kHg^Ug2AJC>3*KuNb{`=kU|ES4&n|Kh&}E%{+q# zZW^D~9^R~~YpV<;5Z;ku6(KACLX7|8PSRnk8-q!j0<(EWO}j$Ta>+IBcV2xDdqJBG z$!IS3?S`yjXK$rQO%L{)mQb%3Svf!TjpLx2w;A&eXiOwdPJG|C-&tyAi7 zkL}||1YH_o-8@Vy>|)C*uMz!U?utEWDUozxw`)lA!!31hj&Cs;P)iRupD}O6#c<_= zqi;%#dYTh9LXJm|9g+*b-S&#TVzX!Ad%c#BZO=*T3a@jPi>2ns@a)M?BJCrvHOCXL z`h+-t;3*4US7tj>PN~#=*o}P)Jy)haF^uBdY{(%zD6h?m-Dmeg>88Duk^2VZM3Ts< z{Y%nm^UX#E+!ii+J|}Xl`6zRdGUeeyGi)bEx$)bNeZC;wz-@bm`iX6gAwDUu_ICIi zYzYo6ZjDb+mrNps$M(C`k$kk7eOqite2(ShlVuS@vB=?Gy{~> zMl@eA_gH%-wM^|ieJ_#Ei1>u}3BS(1#=T|IPn#Vy$B&aaNe|$sdIZfTtUXO>%ILSa z|0CV1ccJyZ`d7yB7;@-`jD40po&V#^lv;O+nbi$;b_&V-NWaF-sdq^Gv+pd)zr#Tr zTsZPd>Qc@DvWuo9gqC^k%)6LpH(T@YX0q;$n3zy=xuN`}t()1F5cZOFCUWZ#){~y_ z&o>U4;zGu><`@gQ7q2 z_z!fXs#_)7RXRns9oQLqYWJ%{J2vGQp(9A7NEZ>KZQ+H;hh5wnHkE^F0)kbgbu zjTq<3DYNI_1TMHJ`isspc(}GDN3Ghza>=X&Y6WxFkHBFy`ZU@#VhaN zY*EAD%C(B##BDQf3hdo@=z!caamxDR%S)xBPH6K~rbhZ*Rv>P&qNUYp(6(``)3)?D zyQpp3&APmg?sIjk4DH8&QJypMGRj^x3 zIL$fMnRl&({pzQ4oU1$=E>0~TG;wcrk#5lX2%5}3pO8Ju{#tQ<7gA@PD?XjEZC=VU zUKbOMD%;VqEjlk0_|`5bDH|!cUK(tA>nJoAYAucJ$xCh&M)q+H|hQ`qXiLU+c^ zYZGc~KMi%Cop<&e-Dd6dk1{|+tZwtvac{gr45|!-TFWLI`k2RZjlOv;;YRGIi7xTc zJJ+o)w2tEr*3+9_E?Rzrq9h@wkStJFs!=^={hKRRde>$o=3 zB)(X~x_v1?i}{N5#{WP5QmPVD$F-j$*C@kJyYS-#c^rCE@hGwCA^lYYtPg zx5_#fJm}vzA!yONXO2S*IkL7bSkF0q{JkRo(_>>jw<>cFeBfQ!bXQ)cSZK9HS*hsC zR*zhDN7F5<{M8Lc-JwYU39j7bcI&?zb;7cx=HL?zO&K=FO4=D*MUq>;G!*%{ioP4(BvZz7cP} zGot0-$HV6e7fm6N4Q#j6nPgb*3Hqq+Q}RhOZoi~+0OUk_w8lNYNWe`q$ErYDLgr%) zu~gkG)V#uq99z7>O*4LuON6olDftlXY;_KA(j?tW1SnOE{Uh@nS?|O!zmZ#;S1Irf zoJLsaJKoARM=L^hk9=rgt8UeJ7i*4CIlh^kI}UR)GNKe0nTYM`xOUYz`Em=PMohBd ztZkwXHQIBWQ$M@(5RO|P6W_Jc@8)hR`Fb>mOQ(0wv?Nm`;5bBt?U$r<6YS4$%{ zu2@1icOZoRiJzLa`OQ)GA%}%xcDu2))o8Eq;s}+^q&;4{uVG_zd|YzJ04uFs$32^F z7%SwRIWuR!-&5gT9lVWf{Uwsw*2wtqI_{^*1kX}guud*-PW<(qoW~Cfr8iHXMJ#=3 z{PtMz{fN0^3cUJP?-a~9?;YbnxbW=MDtU96{>QiIxt0}cvkzsn)jIB2utD+!%_T)Q z{$aUTqs$^tYi|KP@sx^5)>Su1CTgX{i^2#m1C91JZ{NSE#GBV;m>W-4Vm$k<6JhkR zfwMQP3gilC4ctH}3VO$RXxauVl`BM#S*9^2^5#n<-#!eQEz=P5GI%!MakW?HYP=`J zNh;p*eqlTJRMa-jmYbhA+9?A%UKh8t@C82Bt(qNaH2ZQ{MOtxoS!Sf7zY)b-sMS4P zjlA5Ra{$MYuu&N+*AzPVOW!7yaC~SSI6YXF38i>pJR_!ME+x`|xTPpUSvrRx{v5dAsj1FtTr_P(=n zO3=ws=TAjbR#N&0CP;;im#v*pcy8YR91%W45O0SZnObmY? z(HK0Nvn8A=`Se0tt?Rkr8>g>&HlN(U=OQ?8Ix$GT%+z_1=0#3JJ{R@sRaO}*#ubVV zuW%{ow@lIgPOjKo+1Kq9p`umc`24Iu&cbw=c1mPe_|&>n3yf<=x=to+yeX&H`rNf6 zH+Am^YR1b}(rwbRw+R|&p6&>E>mxK$+R&*$MR)#1uIHq^YfEz2!mbUr8M#cY)_2Dtf;-W0m8JLPVMOD(0S?rW57d+RWQq6KT$N4o zPt$o7#j8WI5|*Dk_l<%b`~wY-;Xd^b>F&|TNPd@a6(4NoQA ziIZchPOqAukTNI2-%+62$9%_Y&C}~j>e+N(<;yA1Qle6K8*I7L&!^uqqnO9nHa~V9 zxO&D-A-|wCrdp2^Jl1n=T%DXcOxR)jYV%PlA(?5}z@79tpFMB}# zLV-!!*ch=ukJQ!u8|w*r9s`NhH&Z6&RH`1_IgvPuyiC%*XjA)~C~ET3tfNyaLk&8H zHKv4_oGX?!cFZ59E5*K8g|~j=o>Lc6PjJ$jC+}6G%0q)ET=b+^e%?pE;V$)|8WGht zF%M;)>YYg*P)upx>7ikAw=n5s$%6Hg<82oQf6TTh&<^AoW0b35rgum9B>Rf;t(14r zvm0W(MwB;XAtfg)QJkPZ#9DvioLPk@o^HHA;upEKVU@VS^vhPnDjoCLTuB63O7z@Y zDIa+5Om)kvPf%UE@sg!`hc~ItVpH*vJ5q1CN>+RM+fL{5B{e=UO_WrBRvuqYrsye2 zo;bwjBT(z&bi@p*l+cdHkEXxeR1xEH!_fStQ{|?47pIBrO1@yDFXD6a+Nk(O+4J?8 zb7J?Zy=&et~&cEUfz7%$SQODsZ z;*sNtf@A9T4i>+qVg5e)-KoJ0nnMB-YRYWX+zL#GlQHBZ0zlxmP^Q%74~C?h!cw}CO>#~f1rTZ zJvHgMYa6^4`Mqh&$b7po=sgcGbqC)&&cqG%v&xrBHXAMzZ>_SJJ}*|n>b7R?6=8Xm zYWMv!BTsBo($BlH{;J9%%kxpI+yXTyyK9dthAE9!AG*N#aK8uFYRJ$`BaQKorp75H zxfUD@ugEhY$X+x_(atik&Qh{Yq+J|Q@AXh|uAi9+yXu?3D4$^Em)fHX$D4|XPoFsX z?L3-@Ax(Wzy+gfd^%26z)N=)brlHGx_ths5YW#S|lyJ`6cGP|Ha;<}6+nrUi@4co( zkou`AQ*P`RX>6y^Me|;$kCWOJanSej2THY6sFX^zqoTx0(k_lHxf8sRQs&OZS1zSR ztv-?GJ9oh_6KE$-&$S0oZf~E^I5xCuZcX-ahtWo( zZ8FE{5tkR3R<>F$ihc}3c*PTZo9{Y0+L}DHdU|iYUT&L=;ij}tQ9|4;87VQ%H6jM% z*Ug@jb#%hmfL-y#0ffU=h57;m8!cy<(7Xl;#7ao*Od!Z+5&}Fn?BS2uzuolO&M`Mr zbXE-4*V_ARt@!k9_k<`{D#Vh<`%Yildc{gHBGkP2%x(9iRga|NSNXckTr}#cpYZ(L z!Y9Si2M8~C?Da;i=@%OzsXi-cYP!{n8(grjX37bxTgt!Xo?|RH`Kv9>?cOq{hyk|LDbp zpovGD%GZSw=Lho_D_Zg@2wfO{$yTWUCzETQ``n}hZM1dvh~<~6IFzN+`iTo3d{SMg zTWuONF?IRa#Rm(oSBlP-Y|B`ezFKtNyS!r-uM6Ws2LboA`8My?KOc2&Qml}u#F>3k zyvA&9alY*G7QP*u(#lPR4m%7U$l)?@OI_=UEsJa(58jrrtXyO_0V-+!0!!{NE}vQ`@B$iI(Mrj}b|sJu6B*+8yuoy0$< zUxCm)wQT;82{Fk5H%;RVxD#~9&IM-=1!Tx2>FF=h4Ol$h>lEohT*56O`5jSfJO+mN z>3N3vlS1fg!O$^;dGW1#>xc*j!wP6_Tt!+`2MZsR#7mF5?rk1No z2bbg-?+B{sKT^rg$I+ww?75r?cKngbT)9K7+TNdhLJHkVTCilH`=+S9fq`?!+@#0I zpP+My@7Jz)$?5uLT(;NMJK20guB9*Qm!T^8fxPfagJeytJ~ib<&HHw7J5KK$&rxqZ zcZ@O%i)4=?PBD8Xp;Xm6_SGH_v%n!ir95q=t|Q{>4Xi5z7N~em`EWg>-~5rU-oGJ# zvYE6!jzE_wH8YtoJKA;T-LydEorU$+^%sd#Do2kDUA8E^Sub^n#~Mx^_Jn|r+2xyg zwZ(bj-m#?yoZ)<{n_*3CWXn-7pBCd5Z*N|kwKCU1T-=3Fl32oiX0D?~!2S*Me72k* zw`ofZH}O~#?n+Z&Td!4pE8hF*qbUXn*PP<+P-BZZX53gZ%XTuGiLM9r6ZhKHg=Y$7 zt_x4miPm;bf1tcGFPp?KFo-wOqv(!E`K$x9RGm#@WvT`1jtCB%rI{aZ5~bm;EI72kH%ycfrW_{RPI68S9x*XN@6vVG zQ5GA-)}5Z4o$6edwRC}d{rw4zM`x^QahsZKlyN^dG~|3S=~hb;r_Te875;_wj+GCL z?{zGV)v?+^f2_YXQH!j7NH_MCrdm0BsR*Pz^~QqNniKhBk1klDd1Rj1(z>jd^SDif zjI1MTEpIHh(z`QY`l7utY5u3oN7)8tzZT!FP~n#ydudYP%KBk9M~c1Otzi(EsJxOr zd4JkblWlPpi3g?-ig>N_g^Rb;joMGssFbVz7K0L+ptAvl+vhYu|Zc?F6CpNmArTHHhHU$K}%LdrTZUHPD!u-)RCTQGPER8 z{QX143FlME=M0KlZ#11-eb>}>&55XvWb-2#2DX!}16Rv59+fw%FeaXH3EoaPQ?StEC!GjCy9FbNoQ|yzyGQeAnG5Ik!fz_`^K& z^)3TzCcD|&jM=cUZAk6~ZqE1Y)=rPy`ZcH*S{$|&A0zsp|I-G_fsB{ub*JoM2tQ2L zylt4qisj^MlHR9M6?C5a9gHe_P#SkYJh(l@`3-64b*Y8kw{(f6&5~XMcO!;OHrlgn zUcjef;fBPM118+c7m6XLMprxwx*f5Q-(0>X{nA`T@*IlYJYJWT;xGNPHch0D-_h}o z)9=&f@g}Xe%pOS}S+u{y!Qa9raUECvf&1(}+FbjZS8r$ta27lD=FzsWHvt-zP5qUs zKA0abyKYxHsi?)Y(BUajGBRmmRG>Yt(2%=w#ivh`jUV>2v@k4`FPP*L60|)}{Beh7 zr0=<)<3|Yt#^leHl2oH7Pr98#SRi?G@a9_Cf^(v?E?gCp5P#S~;0c`VGNd-ke95o{ z@{PkOdtc?2B`ErnB=^_xEER6Nm>Bwsr*5`h$(q@3RIF^9IS#0a`|y2`T|Dh#p=;@c z7eoC=s(3fBxj8A2G(6TruHp2#s#4;j zZ|3yA>B49`qee$F+sNgKnG#boZdD)Q<YKP2 zs4Qv7anqe`bdD<^lZ)P8a#8-ByplDJUTtf}CQQ)LsHZfnC^*j+=fQi*p>R+1s?iEV zyzPedue{7F@Q^t3oYBY^r`1|48mkoEN2Tv9ko6CtUY*x6#(T(hg|vkyj}57#z1bGC zmXSSM^~cdSM-F){*KZg(c>SK_icJpIH_rLruCvk$R8cFwJ+lAZiKeBN;&cVRjfVz2 z?{``J^jw>EiPX(98{Ot>i)MzdCz|=kDm9t$6Yj$4$pnsfLp+tB)* z?3)H{DRQbjt#*F=ro*4e#_zVpdh#h!RB~;mRnjNBoPEhL%HguJZd~-t#TLF%MS_#Z zDZCK7+J2z%P~MY0npX6u$@iQHgZLtSh91aYMy%WF{%CxDYMIkOk9t1=e#6W%eOMRJ zcrG1tBYb$$%vfKObD42E-siO^EhLKPFB5+w#8cZb|5$>4+q-nxX-cPalLYQ z1;w>CE0en=Ix$Sfu5$AP?=TO6pz+5@wRKtU+BT7E_DvxEpaHeVfwHwm36dNAt zDPvxVQ397o@1b2L)XcVe^-4%Hn{@Gbt)YOp7bQpZM4V`&y4buTw(acJ_9L~fB=~9% zdAit5(^;!};d6Q0*fRH(MSF*c9!!3yH_3yzrB=lIfO6*5;nAslzHe=(y^%V6HAp_% z*rH)jz{JZ}pWA-OQV90RUa`?g+Ow}EU9EVBn#G9H%qZOv>tQb(YV*!!2 z`TRb=BM}`LneW242kV%-yQ$){Du1-0>nB+8`J#s?+a2P#eDTibr?g;3_+^8DMDyEyDF?+!7U z5Nr6fj#%4Z(9sfcUh|daNY}9qgLp*hxb+5=e6rhaQ@GRA!M@CQb;fw&OhdW?f3dZR zgp}L^LlU3S+mwYGUJsHIkiLlMwpXdz!iHs6)+g)>HG6W1bG@Kz(fXD#*TpHLhbPJI zNm4$x!y~A)#Qfd)W0Q|_AK4uTOHdOUgJk{A+txbgPOEMpJ64_{&YqIg5i?qWKpU%g zx@1vcCP((3i1k%xGWG}7-rhdcUvp}%Lq>k;+#5c-17;4E8_)TUaJnf(PFf&%gV(rK z`VOrZ{n=)Xj~%G~!0zI>@_pl@4rUop=&{tPc_2{-f}~l&c1lRoxV!$cV_#l>ztJ(c zb)r|A+y)t;T~5)S_fKiq2<*<-w>I5fhj?A`72D9QbqQPZvqBJzrhf0`3QU_E(j?x7;L@8t-(q(7`rp@pkrvH6>i_;#Ko(wRPsL zo#Sye)tzVUZsi9HC-18;{W#H{Pk&tOgAIu(3AIZl8{48nhd^r_pFDrjq3xe!mJB*7 zno=$s+;K8)r$V*;%`?87#kzy#9Y!K43t zypQuqTFnsNpz8uu3wLo3fq^-^`ehDo6$3Zy8GPoHy73F8Jtk$NcYk!deXOBWt@=*j zZtdZh%$HQByvh zDKkj0khiI$!IFQ~0ox`A=sUg`<_}>GSY*wdDnvbeYNlxQoiqAQ7fz(fE=vn*4^CaGN?bTK_D##a z_E{z?_j`Js9+okh=os?+;|rf#n9o`gWxSuo_@Hb2E`14&A8 zjEMgh<*?kL>_!QpNp!H;3o^<=5{0JjD}E+upSUpA)}7}-#Y$6HT=h^M`R1woGhNPX z*#(xCNvA0OEg^TBHJc{96WVV_kfbUJA}QWm2)_bsMSl5C9W6(@#{CwIchZS$-k;ZYGPdJDSzC-KM=H0HL13b*21oL3(MEQj{zmO?B8`*HZ(B`{ zS!`E%k5Kc0SarUN>(TTzlUCRU+uu)COLgZjI6!;MZY(CXwQ&T|@#bM-X}^H=IUk;7 z{`XAm39l1syt7&MkhTny=z@%Whb(T z%WnKyiPQ0(E2ZfsS&=pG(=T}j`>iss;7xTt;qAHWZqsbSM#-X`8FYU!fvDZ;2Q4R= zXEqAR<;91hH(4b)c5kn&!Bi65Iw10fm(n%-a<(QjX26N@xiuRr#w7_!C zw6Zj1iHWA^V-(ej9IxoSIIia0ni1{2hJGe~7pEL^rTa^SpFJ zx9X|!z1c73SX5SpiE9L0@g8)va8H`q^GSpu@}~#pPcDDnIDN!^0aFEQoA9TK)p7a9 zkBp4i!NcpA5z%y=y4YH}DL8MYOJlRi;Jadzz05YZlb3VU?oHj)e_phfci!N!#mdj) zP7;*kNZ9N2gzML|%*QFtjd)11bDTRcMJH~}w16DP*{7D| z8n&()SHWA}p6Qp!c1kSf?4!oDB(b>gWsfBlBEx1WW+~g7t-9I3xz2e-v#4bH61(Ni zgzFpIbaU4|SCekvr91=|8bhjf3=o}05T24hutZ?F-zDWRE~x=K=$~?{9Ix))w&O$U z8M0dLMB&EwYMjZ3CZswC!5RdAki2A(u&u^S`>XUErP4OGm!%#S0!3M+eo7L&ietjf zi_MHIVlHdTXtZp;9vg9M`Meu$$JsUN*SSn^4Z4^#Kq!0tpbylb1l1iIWlW9JlZD6R zOKwm|pj|YJJ$Pcv$fx`1D<;+PYiMvj6;?J+k9n9@MKe=(sF-&&s$|1~6~W5WRCW0R zQqSC0E$@0Igk#HfLW%G%2(Gxj4!>QldTRHtF zr4z)>hLPUPm2r)_Tv<8sTtCg{_NpfeQ=K{1#*62rmaX5g$VZXm)+F^~H4Ige1LbqQ`G9?f1|^D=;_W3V&Zdh8?@x!Q&0z6Fs1JE^Oz-|SY=+Opc;YJ*Vu zvZuMuZmX6XESz@L@MeUm?haq0j^hdYZFF_C=W*vu%{3AB=`S()Drfeo(E3c>!t9KB zPOfj3E%(tTei$PEEPq{-?M8}gxnz3$dTGo2?ai$dwZtjTRTnqz=G7)9Wot-$)~4AtqbWl%UF-ZS=7MT=BuV(PN=JZO(iz2yu~XSwZGR?vKQ^camR z;^>vd_65$oEf1Hhc$4fY{d(FNKWe(qiPgev1za$K7NVJOEbf0%KJ@((las1768+s) z%;6YY+HxVl@w@|fO9QNaUkFR`%Xo1%BeRVJ0~-AWd&71#h&QCj>IZ|^ zA8`5j-Eb&ST-kncTEj(IxA`S6Oa_-&OC)nmPp=Iyd&y>P`hcx?S7TkQ3}0#}!E6|R z%&fG5nuM652ZKD7Yi(dzCxJuvn!$xy$7UYEmZ##yqoiC*(`aOv#ixr?oyvtc+n=$Y zHoCO&*r7#MM;h*&9=t%$;X{7Z<+8vst|o2L#Z&#=d|xf|D;{32HP%xnfbS(eILJoX zqSwQLd*aVm5xj`YjwoLf{c!V9e9ggrjsvR8OqamZ z@iC{HUq97rr#GImmX^*KMohw)slZVMf-&x<{rHR)#pZGEv>Uv*e_8B+NnRY`Aw0wcjnWgm z4i!>ko_R;gav3Ey`mWBq9`9Uob{3_r>h#BE$$_Vw4)D}@ve|G7Z_e7X`$?JRN^_xw zk8M}=FFp1W#wzzFUA}VURceQb>m&ljr+k8TOQw;}qG!t`)tdw_4dd5hx1Kyrzs`~K zTCL)gX@mf)4O@LmR?nz>B=uq)$w#i>y-nq_Ylki?^A~&DuS-;xGu_sjyxK-gA2ueX z>BqjS*I=LZT5QyolQ%uox1!y&ZK@rRqbd~!?pe5W~@TCR5E!f0-JN!)8k&=zgD^6*6Av;ORUa<$9WSQj4p+>Q!rnbp*1MHbl+wcce+CCaAD8EHNrX%LdbF_AnjY~B_%9fcdBzP_Gw zrh81kyr%xjCg?Z|-{XE{cU57Jy?$}pzKNoVqU94fqU|abl@~7cU-dqKvT0shg_!Ow zD_i3a8BXSc9m~`b>Xtf$Uzj&xvsqbxmm|X#cpk4hunQKhE`^95ILGgksr)?rJmJ3B z7tFgctx z7#`}v*seB<%c-(I?+I;vH$t1NW6Jx;#pf-vNsjjncFkYIx#@qcoQprx-yg@fF|ugN zHkVv7mzev?Epo|5C>q*?&2%GCa>=FK8d(x4m)x3-klPlLYq?)izN6Usb|ch64??x( z_WS%EzklKP2b}Xb=RD5k^?tpd@8e=e>N6zGj-$7>#TqEe3sjwJ5A|xk2E@VUmR}~_CV^_|G=M2k!(iDUumE&^I{=P=X)xH}?wRWc< z2F;X7-bcjxwF#TbxgR%n#L?`ReoLK-z1PV7ombro33=4Yb-THogZ*?IcY%?6+K#(4 zK@e5r+fYyYRPw!4luvp)%goUr9c;{s8AgGO;k?z@Fvk>hmX#N^FgTC_SD2)3J*)t?D97Ua|a#gP!HZ}h`w4mox{%kWQ(42T_f^)SiQ)z@&f zXk#qycX(ywOkEWlkr7RRX3Vw|JaU1nC3Z&AwbGh>#x^*c4Ji=s(}9VsXbA=y)8pXR z((g4{1*!O1oe|W$J7*{m8EY_H8=Fv(X!hNzDAWBu{Ak3&(TK za&>GY&WBz~?Q)RLdA_%|vnR02S+n;OX96yj&o#)dhO$n}-9mHRxW0&l67`Us%M!%$ z78^2fMaeWD-B-a(iLUPNkh4hBQNms@i{(e>FK^G@iYiLnp@;%Hs??>O9}zMLLh)gX zs;js(+-pwaMQ-9G!Oy>kr=|Ot*!a|t!JcNKEced7R?4MbJnGYIFOvT4f^79U8S>P> zW_*A{0LfZHlLycROBgSVT&TM)7(jcA?62rDT zxL-xiq>`bAEudHqA|ZRliL`pc**ZWW z7a5F8uC1O9K)|a^gF1Wo-PP@BFlE-5qivGFhQVL`Ncm!x2vvLzE3J!PKovkX=<^w;$#|*{-3#-;lz7(NC%ath)OXpeYXaQ>Elip9&N7C5th2!Gy$S zbJuxNuWhVjErkCvrw3*iu}>a=!f}L%Oy)Ne+E!rZN+?)6rep3w`P>y_2pjaik#!D+ zI$%7y@HaK>use5emETNuwjH~aC*rU2j72C0H*^bO@&!m)TefkO;l65964?5mde6ff6;y@+is%x(IOQNL zt{(rXW=OY1r{~9a`86Qq^WnBbRl>d|L`@;ORJj2DP?;w^Ex>+y;XO;HA;X>8&;qUW zGNDPBB=?8g#(a-%QYWC;V$ zFKw+WDK?O!^QcU`$z@`U452q;TGXTjafgXWv@K#b^v13h(Z<9b0PJxFWEd^3OLHm; zw(XQXlT2_PF%#F}5T@+8wo-A|=&^2HmVa(axq$&%DfCB5a8=n`1!|_}tbS@E!ZJ^1 zf#WmjlYIP!jZ)N?u|#3Yi1pLW_=atSAZ*JPfj1+Ws$OG z313h8CQjD5E5DYY*531m^G~Q~8W@ZTfLo1r+wU*x6ot?&aoHDOfRuV$rTM2D$4hlV z{?HdA<8tY0lJU4~CvkF~x?ld7vA0EKn@@q|ZWfrr5)&K@avzS-D)aeii2Hxl{QR$SC}|sBR)4XPFAh@xs+mB}csE@A5$cWq0B-FI AKmY&$ literal 0 HcmV?d00001 diff --git a/crates/tauri/test/fixture/src-tauri/icons/icon.ico~dev b/crates/tauri/test/fixture/src-tauri/icons/icon.ico~dev new file mode 100644 index 0000000000000000000000000000000000000000..db7fd98204424b6b9b02fa06ad18f05c089f93b5 GIT binary patch literal 57594 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 ny0Oq8K3_V5(4c+dzp8=O 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 literal 0 HcmV?d00001 diff --git a/crates/tauri/test/fixture/src-tauri/tauri.conf.json b/crates/tauri/test/fixture/src-tauri/tauri.conf.json new file mode 100644 index 0000000..f5b75e3 --- /dev/null +++ b/crates/tauri/test/fixture/src-tauri/tauri.conf.json @@ -0,0 +1,22 @@ +{ + "$schema": "../../../../../crates/tauri-schema-generator/schemas/config.schema.json", + "identifier": "studio.tauri.example", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:4000" + }, + "app": { + "windows": [ + { + "title": "Tauri App" + } + ], + "security": { + "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: http://ipc.localhost", + "headers": null + } + }, + "bundle": { + "active": true + } +} diff --git a/crates/tauri/test/fixture/test.txt b/crates/tauri/test/fixture/test.txt new file mode 100644 index 0000000..e69de29 diff --git a/crates/tauri/test/updater/fixture/archives/archive.linux.tar.gz b/crates/tauri/test/updater/fixture/archives/archive.linux.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..dcb6e145b25f69f14b59af82c57816759c6d5e28 GIT binary patch literal 342 zcmV-c0jd5UiwFQ9j3!|K1MQXFYr-%Pz{6mS`Lw4!=O0*h7u!T1eCUQ^!Lgw-wx<$o z9EDX&jiZ15;uM)1)`8f4E(JU_raoh7AvFk3tgx*d)5&!WDuz|79I<-EKJpt zCVK$@()a0`7E2A>Tj~WUfue_qJ_u_s3k7dBQJ|2FH=4qelGkeupZ3D5pnu&B z@7t_U#^h`?O_|DUhOkaDv56B-#?w~tj&@K=&35~Nb=Z3>bGQz0OP8rt^nd<3hoAnt o>mQJRoc}(8oBvf{)I3ijqi5#3BXKw!4o8)|0?b%7 literal 0 HcmV?d00001 diff --git a/crates/tauri/test/updater/fixture/archives/archive.linux.tar.gz.sig b/crates/tauri/test/updater/fixture/archives/archive.linux.tar.gz.sig new file mode 100644 index 0000000..4b47343 --- /dev/null +++ b/crates/tauri/test/updater/fixture/archives/archive.linux.tar.gz.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldSWTRKaFRZQmJER2NJZ21zb1RObVMwZmdkQUZ2OFFxM2dYdVhLRXpSNFp1VW03Zml2THlDRldpSmJPRkYyQlQ4QzltdkYxTG5MbzdWODZwdFN4aUNoWXY1V0lMem55T3djPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjI5OTE2MjIwCWZpbGU6YXJjaGl2ZS5saW51eC50YXIuZ3oKZXExb3FCVGlxVkc3YThBVklQeVdhS0psOWROT3p4aVcwaE1ZaE9SRWVFU2lBdnRTVFd1SnJ4QjlTWW90d045UXFZZ1VROE1mUFJhUWxaUjVSaXAwREE9PQo= \ No newline at end of file diff --git a/crates/tauri/test/updater/fixture/archives/archive.macos.tar.gz.sig b/crates/tauri/test/updater/fixture/archives/archive.macos.tar.gz.sig new file mode 100644 index 0000000..4db824d --- /dev/null +++ b/crates/tauri/test/updater/fixture/archives/archive.macos.tar.gz.sig @@ -0,0 +1 @@ +dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldSWTRKaFRZQmJER1VrQzlhaWFLOExCM3VoV1gxTW1IU0ZQQTZKTXN6U0MwMDEzcThyc3R3a0pmVkJrdWFhN1JCOTNpaEg5c1JhSGY0QWxROENlbTBxbFhpVTNWUWViWVFBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjE1Mzk5ODg3CWZpbGU6Li4vLi4vdGF1cmktdXBkYXRlci90ZXN0L2ZpeHR1cmUvYXJjaGl2ZXMvYXJjaGl2ZS50YXIuZ3oKczI0cUxjM2YvMGdpMDI1ejE3ZnREQTNtdlBwR2xydW5rVFBvcUxVcUt2dVpxZkpPd1ZLT1Z1K0hZS0hjRVk3Ylg4UVQxWEtWMHJ4ZUwwSXcvZjlaQmc9PQo= \ No newline at end of file diff --git a/crates/tauri/test/updater/fixture/archives/archive.windows.zip b/crates/tauri/test/updater/fixture/archives/archive.windows.zip new file mode 100644 index 0000000000000000000000000000000000000000..bf1ed72452751d4cf17ba41e51b424795b826d0d GIT binary patch literal 778 zcmWIWW@h1H0D;|xACzMwfl7{!c&#F7lXtm6DUu!FRL27xe62Z@B94tU?S>WCE3 znIH`EPf22FQ6^Y>22g7@rgmjT{iMu1{iMX=3@)zJ|!M@gikxsT*(GB zSPx-xlH;Q|CQOq9ycwC~m~jQQ1T@eE7~VR9n9zV{g#5Ie7ex83D*{)C8Ic diw>Y!py BTreeMap { + let mut manifests = BTreeMap::new(); + + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let out_dir = temp_dir(); + + for plugin in plugins { + let plugin_path = manifest_dir.join("fixtures").join("plugins").join(plugin); + + let permission_files = tauri_utils::acl::build::define_permissions( + &format!("{}/*.toml", plugin_path.display()), + plugin, + &out_dir, + |_| true, + ) + .expect("failed to define permissions"); + let manifest = Manifest::new(permission_files, None); + manifests.insert(plugin.to_string(), manifest); + } + + manifests + } + + #[test] + fn resolve_acl() { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let fixtures_path = manifest_dir.join("fixtures").join("capabilities"); + for fixture_path in read_dir(fixtures_path).expect("failed to read fixtures") { + let fixture_entry = fixture_path.expect("failed to read fixture entry"); + + let mut settings = insta::Settings::clone_current(); + settings.set_snapshot_path( + if fixture_entry.path().file_name().unwrap() == "platform-specific-permissions" { + Path::new("../fixtures/snapshots").join(Target::current().to_string()) + } else { + Path::new("../fixtures/snapshots").to_path_buf() + }, + ); + let _guard = settings.bind_to_scope(); + + let fixture_plugins_str = read_to_string(fixture_entry.path().join("required-plugins.json")) + .expect("failed to read fixture required-plugins.json file"); + let fixture_plugins: Vec = serde_json::from_str(&fixture_plugins_str) + .expect("required-plugins.json is not a valid JSON"); + + let manifests = load_plugins(&fixture_plugins); + let capabilities = parse_capabilities(&format!("{}/cap*", fixture_entry.path().display())) + .expect("failed to parse capabilities"); + + let resolved = Resolved::resolve(&manifests, capabilities, Target::current()) + .expect("failed to resolve ACL"); + + insta::assert_debug_snapshot!( + fixture_entry + .path() + .file_name() + .unwrap() + .to_string_lossy() + .to_string(), + resolved + ); + } + } +} diff --git a/crates/tests/app-updater/frameworks/test.framework/Headers b/crates/tests/app-updater/frameworks/test.framework/Headers new file mode 100644 index 0000000..a177d2a --- /dev/null +++ b/crates/tests/app-updater/frameworks/test.framework/Headers @@ -0,0 +1 @@ +Versions/Current/Headers \ No newline at end of file diff --git a/crates/tests/app-updater/frameworks/test.framework/Modules b/crates/tests/app-updater/frameworks/test.framework/Modules new file mode 100644 index 0000000..5736f31 --- /dev/null +++ b/crates/tests/app-updater/frameworks/test.framework/Modules @@ -0,0 +1 @@ +Versions/Current/Modules \ No newline at end of file diff --git a/crates/tests/app-updater/frameworks/test.framework/Resources b/crates/tests/app-updater/frameworks/test.framework/Resources new file mode 100644 index 0000000..953ee36 --- /dev/null +++ b/crates/tests/app-updater/frameworks/test.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/crates/tests/app-updater/frameworks/test.framework/Versions/A/Headers/test.h b/crates/tests/app-updater/frameworks/test.framework/Versions/A/Headers/test.h new file mode 100644 index 0000000..3539143 --- /dev/null +++ b/crates/tests/app-updater/frameworks/test.framework/Versions/A/Headers/test.h @@ -0,0 +1,18 @@ +// +// test.h +// test +// +// Created by Trey Smith on 9/15/23. +// + +#import + +//! Project version number for test. +FOUNDATION_EXPORT double testVersionNumber; + +//! Project version string for test. +FOUNDATION_EXPORT const unsigned char testVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/crates/tests/app-updater/frameworks/test.framework/Versions/A/Modules/module.modulemap b/crates/tests/app-updater/frameworks/test.framework/Versions/A/Modules/module.modulemap new file mode 100644 index 0000000..f154525 --- /dev/null +++ b/crates/tests/app-updater/frameworks/test.framework/Versions/A/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module test { + umbrella header "test.h" + + export * + module * { export * } +} diff --git a/crates/tests/app-updater/frameworks/test.framework/Versions/A/Resources/Info.plist b/crates/tests/app-updater/frameworks/test.framework/Versions/A/Resources/Info.plist new file mode 100644 index 0000000..6f736f0 --- /dev/null +++ b/crates/tests/app-updater/frameworks/test.framework/Versions/A/Resources/Info.plist @@ -0,0 +1,46 @@ + + + + + BuildMachineOSBuild + 22D68 + CFBundleDevelopmentRegion + en + CFBundleExecutable + test + CFBundleIdentifier + com.tauri.test + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + test + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + 1 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + + DTPlatformName + macosx + DTPlatformVersion + 13.3 + DTSDKBuild + 22E245 + DTSDKName + macosx13.3 + DTXcode + 1431 + DTXcodeBuild + 14E300c + LSMinimumSystemVersion + 13.2 + + diff --git a/crates/tests/app-updater/frameworks/test.framework/Versions/A/_CodeSignature/CodeResources b/crates/tests/app-updater/frameworks/test.framework/Versions/A/_CodeSignature/CodeResources new file mode 100644 index 0000000..82d2063 --- /dev/null +++ b/crates/tests/app-updater/frameworks/test.framework/Versions/A/_CodeSignature/CodeResources @@ -0,0 +1,142 @@ + + + + + files + + Resources/Info.plist + + /aPV7Q20g0elr7OiZJoUNggTOcg= + + + files2 + + Headers/test.h + + hash2 + + 5RA6Mnq5sNoaC4wKcFe6zymVmEL5Vb44G4BGqFjgZMM= + + + Modules/module.modulemap + + hash2 + + C6uLLSnQu9M2qLElVCkeo2JpnvWMxtArinQzmlh3v2A= + + + Resources/Info.plist + + hash2 + + nPMotNIMgvMfHtkRdpeehzfBiCZLnksfiD3nldUPzTE= + + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/crates/tests/app-updater/frameworks/test.framework/Versions/A/test b/crates/tests/app-updater/frameworks/test.framework/Versions/A/test new file mode 100644 index 0000000000000000000000000000000000000000..687eb01d82bd2fcc1a41f20ed97a7a791ed1e9b9 GIT binary patch literal 35664 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 literal 0 HcmV?d00001 diff --git a/crates/tests/app-updater/frameworks/test.framework/Versions/Current b/crates/tests/app-updater/frameworks/test.framework/Versions/Current new file mode 100644 index 0000000..8c7e5a6 --- /dev/null +++ b/crates/tests/app-updater/frameworks/test.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/crates/tests/restart/Cargo.toml b/crates/tests/restart/Cargo.toml new file mode 100644 index 0000000..1d3a81e --- /dev/null +++ b/crates/tests/restart/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "restart" +version = "0.1.0" +authors = ["Tauri Programme within The Commons Conservancy"] +license = "Apache-2.0 OR MIT" +edition = "2024" + +[dependencies.tauri] +workspace = true + +[dev-dependencies] +tempfile = "3" + +[features] +process-relaunch-dangerous-allow-symlink-macos = [] diff --git a/crates/tests/restart/LICENSE-APACHE-2.0 b/crates/tests/restart/LICENSE-APACHE-2.0 new file mode 100644 index 0000000..3ef0968 --- /dev/null +++ b/crates/tests/restart/LICENSE-APACHE-2.0 @@ -0,0 +1 @@ +../../../LICENSE-APACHE-2.0 \ No newline at end of file diff --git a/crates/tests/restart/LICENSE-MIT b/crates/tests/restart/LICENSE-MIT new file mode 100644 index 0000000..98cefa6 --- /dev/null +++ b/crates/tests/restart/LICENSE-MIT @@ -0,0 +1 @@ +../../../LICENSE-MIT \ No newline at end of file diff --git a/crates/tests/restart/LICENSE.spdx b/crates/tests/restart/LICENSE.spdx new file mode 100644 index 0000000..e3b1b80 --- /dev/null +++ b/crates/tests/restart/LICENSE.spdx @@ -0,0 +1 @@ +../../../LICENSE.spdx \ No newline at end of file diff --git a/crates/tests/restart/build.rs b/crates/tests/restart/build.rs new file mode 100644 index 0000000..35b4e02 --- /dev/null +++ b/crates/tests/restart/build.rs @@ -0,0 +1,5 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +fn main() {} diff --git a/crates/tests/restart/src/main.rs b/crates/tests/restart/src/main.rs new file mode 100644 index 0000000..85b9bd4 --- /dev/null +++ b/crates/tests/restart/src/main.rs @@ -0,0 +1,30 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri::Env; + +fn main() { + let mut argv = std::env::args(); + let argc = argv.len(); + if argc == 0 || argc > 2 { + panic!("restart test binary expect either no arguments or `restart`.") + } + + println!( + "{}", + tauri::process::current_binary(&Default::default()) + .expect("tauri::process::current_binary could not resolve") + .display() + ); + + match argv.nth(1).as_deref() { + Some("restart") => { + let mut env = Env::default(); + env.args_os.clear(); + tauri::process::restart(&env) + } + Some(invalid) => panic!("only argument `restart` is allowed, {invalid} is invalid"), + None => {} + }; +} diff --git a/crates/tests/restart/tests/restart.rs b/crates/tests/restart/tests/restart.rs new file mode 100644 index 0000000..89bfdd0 --- /dev/null +++ b/crates/tests/restart/tests/restart.rs @@ -0,0 +1,125 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::io; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Helper for generic catch-all errors. +type Result = std::result::Result<(), Box>; + +/// +#[cfg(windows)] +const ERROR_PRIVILEGE_NOT_HELD: i32 = 1314; + +/// Represents a successfully created symlink. +enum Symlink { + /// Path to the created symlink + Created(PathBuf), + + /// A symlink that failed due to missing permissions (Windows). + #[allow(dead_code)] + Privilege, +} + +/// Compile the test binary, run it, and compare it with expected output. +/// +/// Failing to create a symlink due to permissions issues is also a success +/// for the purpose of this runner. +fn symlink_runner(create_symlinks: impl Fn(&Path) -> io::Result) -> Result { + let mut compiled_binary = PathBuf::from(env!("OUT_DIR")).join("../../../restart"); + if cfg!(windows) { + compiled_binary.set_extension("exe"); + } + println!("{compiled_binary:?}"); + + // set up all the temporary file paths + let temp = tempfile::TempDir::new()?; + let bin = temp.path().canonicalize()?.join("restart.exe"); + + // copy the built restart test binary to our temporary directory + std::fs::copy(compiled_binary, &bin)?; + + if let Symlink::Created(link) = create_symlinks(&bin)? { + // run the command from the symlink, so that we can test if restart resolves it correctly + let mut cmd = Command::new(link); + + // add the restart parameter so that the invocation will call tauri::process::restart + cmd.arg("restart"); + + let output = cmd.output()?; + + // run `TempDir` destructors to prevent resource leaking if the assertion fails + drop(temp); + + if output.status.success() { + // gather the output into a string + let stdout = String::from_utf8_lossy(&output.stdout); + + // we expect the output to be the bin path, twice + assert_eq!(stdout, format!("{bin}\n{bin}\n", bin = bin.display())); + } else if cfg!(all( + target_os = "macos", + not(feature = "process-relaunch-dangerous-allow-symlink-macos") + )) { + // we expect this to fail on macOS without the dangerous symlink flag set + let stderr = String::from_utf8_lossy(&output.stderr); + + // make sure it's the error that we expect + assert!(stderr.contains( + "StartingBinary found current_exe() that contains a symlink on a non-allowed platform" + )); + } else { + // we didn't expect the program to fail in this configuration, just panic + panic!("restart integration test runner failed for unknown reason"); + } + } + + Ok(()) +} + +/// Cross-platform way to create a symlink +/// +/// Symlinks that failed to create due to permissions issues (like on Windows) +/// are also seen as successful for the purpose of this testing suite. +fn create_symlink(original: &Path, link: PathBuf) -> io::Result { + #[cfg(unix)] + return std::os::unix::fs::symlink(original, &link).map(|()| Symlink::Created(link)); + + #[cfg(windows)] + return match std::os::windows::fs::symlink_file(original, &link) { + Ok(()) => Ok(Symlink::Created(link)), + Err(e) => match e.raw_os_error() { + Some(ERROR_PRIVILEGE_NOT_HELD) => Ok(Symlink::Privilege), + _ => Err(e), + }, + }; +} + +/// Only use 1 test to prevent cargo from waiting on itself. +/// +/// While not ideal, this is fine because they use the same solution for both cases. +#[test] +fn restart_symlinks() -> Result { + // single symlink + symlink_runner(|bin| { + let mut link = bin.to_owned(); + link.set_file_name("symlink"); + link.set_extension("exe"); + create_symlink(bin, link) + })?; + + // nested symlinks + symlink_runner(|bin| { + let mut link1 = bin.to_owned(); + link1.set_file_name("symlink1"); + link1.set_extension("exe"); + create_symlink(bin, link1.clone())?; + + let mut link2 = bin.to_owned(); + link2.set_file_name("symlink2"); + link2.set_extension("exe"); + create_symlink(&link1, link2) + }) +} diff --git a/dependabot.yml b/dependabot.yml new file mode 100644 index 0000000..29c8dca --- /dev/null +++ b/dependabot.yml @@ -0,0 +1,105 @@ +# Copyright 2019-2024 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +version: 2 +updates: + # Crates + - package-ecosystem: 'cargo' + directory: '/crates/tauri' + schedule: + internal: 'daily' + labels: + - 'type: chore' + # disable version updates + open-pull-requests-limit: 0 + - package-ecosystem: 'cargo' + directory: '/crates/tauri-build' + schedule: + internal: 'daily' + labels: + - 'type: chore' + # disable version updates + open-pull-requests-limit: 0 + - package-ecosystem: 'cargo' + directory: '/crates/tauri-codegen' + schedule: + internal: 'daily' + labels: + - 'type: chore' + # disable version updates + open-pull-requests-limit: 0 + - package-ecosystem: 'cargo' + directory: '/crates/tauri-macros' + schedule: + internal: 'daily' + labels: + - 'type: chore' + # disable version updates + open-pull-requests-limit: 0 + - package-ecosystem: 'cargo' + directory: '/crates/tauri-runtime' + schedule: + internal: 'daily' + labels: + - 'type: chore' + # disable version updates + open-pull-requests-limit: 0 + - package-ecosystem: 'cargo' + directory: '/crates/tauri-runtime-wry' + schedule: + internal: 'daily' + labels: + - 'type: chore' + # disable version updates + open-pull-requests-limit: 0 + - package-ecosystem: 'cargo' + directory: '/crates/tauri-utils' + schedule: + internal: 'daily' + labels: + - 'type: chore' + # disable version updates + open-pull-requests-limit: 0 + - package-ecosystem: 'cargo' + directory: '/crates/tauri-cli' + schedule: + internal: 'daily' + labels: + - 'type: chore' + # disable version updates + open-pull-requests-limit: 0 + - package-ecosystem: 'cargo' + directory: '/crates/tauri-bundler' + schedule: + internal: 'daily' + labels: + - 'type: chore' + # disable version updates + open-pull-requests-limit: 0 + - package-ecosystem: 'cargo' + directory: '/crates/tauri-macos-sign' + schedule: + internal: 'daily' + labels: + - 'type: chore' + # disable version updates + open-pull-requests-limit: 0 + + # NPM Packages + - package-ecosystem: 'npm' + directory: '/packages/api' + schedule: + internal: 'daily' + labels: + - 'type: chore' + # disable version updates + open-pull-requests-limit: 0 + - package-ecosystem: 'npm' + directory: '/packages/cli' + schedule: + internal: 'daily' + labels: + - 'type: chore' + # disable version updates + open-pull-requests-limit: 0 diff --git a/examples/.icons/128x128.png b/examples/.icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..77e7d2338e9d8ccffc731198dc584906627c903f GIT binary patch literal 11059 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

j#(n6`cTK z7WaNlSbCU2SYaJ~J9!EIl=Absd61in`6c7 z2!rKnZY-bA9QxOd{4Z=S0_3y53^x15sxx56rA9s3^eZ7eRU*!T!jS;brox|@Dmbs@ z)LV{9g3&pS@bdnpPmjzHE^TUkmTaE`4_*q=x`t*bmC67J>8E`rU!L>bFU8ItpIBK1 zt(tU#bBZILUVIruUJQMfM)-V%6~kN+00(_J2iOS{_jN4?CDqbi{V#5oC*4Ics&)CL zfXG;fbi@h`QbCfpy~%g39f;ohc>dZaaPZ>!3@VVdYc9woE=YPAJ!M1bIamkPi`ic2 z=3^)V0L54|3FQZKFz=&B6nu}0Bs6*^vk6umxWDbO?2NMj(q%JH0LEv(0$S2O6<(Rz6q_TTo5=jq3I z1^Mj{;w{@ZApHzNSjiXISH&A%b}i6rLUlvlSK#hjMIJ(KX5fOCMyc+NjS2rMP&OL-kT@XW&D|OMh!+FR^9qfpeKhJXtBu<}k_Gqld1UUogeKE` zMNL%XNxW3J0P<^fy8N{}el#M$1(>4P(D;IPd4JZT(mR9GkKtg9jtR5eH#&zS>8#@GR%W3)WdwTfIV-;V;gDS{Wri zspuIA1wsTXFH_( z^OMtaCG-tXX4EI@`fjlW{)p5*!^a^KWusC6=q{RcQgzY2{k$FNW>E+dwP*Pys-WNR z6yR{%y{MhDi=Jg5%f9QCS!_f0Yf-w9P!Dd0?>-@4wJihJ#I~jE;qqP_MzHmK+(7sA zwABxmOVYf34Xd~249uYFxHjhju)>@|azjc=ClaXa6eAn=J9PFRrFm{-I?wqXU`5On zM)uAw9hq(e+uETgY0ABb{p^n0*~&axWAA${`(xx?AlD-*o(f*v?Nv=4()iAk$xB<& zrGeKkHs8+`l2B6Fw@X%UFL@dg!j4J`?Yjg9!OxGw6932*KgR$RFIKB?y;+#o)7n{g zL^s_2;w`&d<$Q&T>nXf_&fo=-&9{>T+cnrN-hPDU`etnykC%_k(5JUZn&WpH-wKP|-=6QPL;+I;sbYSMWotrw>4(9M zCqA>k@Ijgf%85^!9Vb>XJ<3Dozp2&xe)V|AWVF(`W#V&&C){=&>$*ENLjP(;M zlSg7yCf;ZXaYW>`l-aA`{Z{NG|AlIEKHE$kpK-!`U}WJG9WS0L4puU_~`;D?Fealy0X-<}1EccbywkMOL)WgdPk zW{%$HX9x@R8M%l245B&V|9>qq6X8oql;2WYlA+i}_q)chCkFQ%vx|ys&)b&cyL?oX zHOaFChr{XAKiPT7r;mp8-(F#E1xVu*>36zEio1&Ya#z;020=SH{&T?hz&ENy0=?IC zRQ;V??ZuC#L?%U$voY!P}DX_)CCsy6-b}4 zR~ucq*?0P^6auWd$Yp1Yonm{d`Ll+~MHLr1vvB(H?BvN?-kEq#UdDv8HiqG!p7H{< zzHR2?4}KLqeU57~#q|(n9i~*U!Y{fvc`CfkF?ix*d#jr})6^S21S*Lr9QlBxJ$6jk z%;@Fwc(-mBd^Wj=Bf_bJP3@r}`iYK3NpB7qYgd2nvc;auHJVG#EV>?gk!#XL|Ej|- z07z9Tt(#3dy-t5$WUQq|=_R&xeLQlni!nvaeb)`eWN_>>g}zSWc#q)XV~)8g7rdG| zg83mDXtCS70;cKWO6wsK`}vyD;f#_cgIq#|B-(gT6O1V4JKgxM`umJv`(rR27TT0) zh@VUAhHL5axx$8ZfKxKRo1#hI*I3ikE`jNzIykeVSq*3O+`5E#%*Yu@$R@bo%^yCgP4dx$?N+N$<4V>l;qZ z43gkjD*&hAhBg*`ZBD?%BumAf5_9p3DZLVUg6cl=FIEi}+X(E4!9Pa+f%`dle>~kY zO{!`axsp*(!TnYQCMUGk`GZ@i98|1`&pP9$8EXTY@~@&Tmwu^1HLoQ_y<%d5R5^B! z@x5Z%!w@_A5~Y}g93v4n{i}5JHW5B$8JD>KMF<0A2!2h*S4P1cL-|5s79sApQ=?<^ z2_J&cSe~=Bb`3dfkteP*MZ`LTO**#cs#HYWeWrbe=xGbcvkKgA?PQR@8G{#5f{(Qv zMjF>>(DZc?+5mV)JI>ZdvR_!o>KBgb8^nqp+tF%Oaeaf?k3s?2sl>=B{dTcy{dO%n z+YFEklA?*ee5&j+e{n=vYFI-#>er3qG?|lzUj13oAi0<&C?4U?iC>Ab2R@9}dbn#B zPR4qHT1qcF{B;|f9p&`CN_}m?A9^}Tg||9lm$}|od@U(<3anzmgEvd^v-bnYHMBw} z3xsxhZ_^lmHj9kdM!peie2B{lKV82~Np~V^cuMk4{0Pu_8aQzY5)GJ%bMXCql%rPL z$&a8mKD{_KiJd7~-OoKId)Pgz(i837wS_ z`{8iqe8xSnz?yQyownR}zqj8)8I&(}9>J`x{3!hQ+_8O9!5yRt$8+Z^bUvT{uUa<$ z^ZK`W`pLB5P5~EbCk>Pk1sHjA1jzcG@{U$J^=BeWPEA4qPlUsijHARYEm1%lw_zdF z;M@~*?a#0b;CYqFQLExNU~+CDsvnJyA6Pvck{ zNGUrs%yzKoZErT;eOh2Yf!2vuQ9O%frbH(F*t;A!R+p#jbq!m)@&l6Rt-k*is=Q4` zS9!(EF1B1{haJ=$kQAIaJ1Xa^Z>u~AG%=zs_!B!b|8_IFgGdX*QQVl^m?*NA#g<9* zM|Y(I)Sx>t?w`LyI-jyoV)`YX8_Q*yW6YtIkW}@^J9m&<8!<@^Hqq8YN{&#aD0-NN zpdr@Unnh5E%YV$u`M!yK=GK;)hkiAIfnutT13!S&hgIF@OEl2oSri-}6&?Ka`d6@4 zSOQ7j-#Y}r07ZDhQiRYlOo3#ZFxVugoaiw1g$ z&OAB$mV5a;3-WI9l?rDeYD`p$O?bF2g%W6OO=|se`WM<%RT-m_M~!6&b8v)!J=h#u z>r^}2M$#d;?KV6%a+`_rE?Y|bTR60Xyj&x0B$ugu@1dTp3H|m>`;YUiH>l}qhukEE zOzE5^-3jPB^b)?PKaB~Xg+9RlmGH8EplsB9UdJZPk^6Y2Sdp^x6Dv1RAx;g-3A&At zVhSBDDd^9<$3(Q(F8SG?J4?$>ch>XWmZhnx^hUVQRVYE4LB)tcTvByu^ki(j8%wwH_j+;xK3Tq1hw0-v(ji}QIN zJAnJ>vXml|)tB7`BTuJy&riROoq=Po7y;MZO#^Hy7e56kWA8tTA6B_w#gTG2SfPDH zFJ-do1#dIb#BLP@Ph<#b>10r_Zk`%Ti{Yr}XpR)^A;PW}F7W|#Q;AnGmz$sf%W8|n zm9|R($TfuSn~(+siOtkkN`VNsD{UWchi*?uDGtNvs)~vGA(}fml;TcdyU#i~va~mW z==4u1InZ5nSMvzLh(-WutdxfQMQj%ZJ^)($0kvDq5Khw!E*!y|zcak&A@}NPf>f4T3Y<8(77d#ZM7$|={ zd@3a7!7_zsc7zY>7=Mv=uXehX5CT6L~llK;U@;vrNLcHf};69z)cf zhBbpskIggkh=PB<>y^3I?I9<+;~E#U#-^0^8Tau9mtt7?=2He;gc2 z-iUtfpki+ZcYq=9ZzKswC!>2-akmS$9gF@6$n7L%iY{yv$!YOwD+Q3S)qGz{Cb)9QsmfSqDO#cDHSz51U|*<(h*JmG!ZJECrA@+j}XAOYyUtLxO#MDHi{UX+ST{ zwh4Q!ghgp_L0W{!w=16gyAOTS{hk{6IupSG4Z5+g|JWw%n@{iFYd9yu#H9QNiJVEq zfi?)Z>9PiP_XWpIgD2g}@-^VF*p+!pY+Yt8dtt`>WVW#Gqn*-2PiyHFA42+%4;sON z7-qz35-6GdFQ2s;phwZNe-fk3lWPL9;Z*N%qabB6yiPe^7gc)d6~s~>W9$251{R}R z6{EKKU$ieepBr}2eV=Y_=nv?aN)m%#zW={&;u6NViOWE1@>>g>UkBRm1M}vpkqzDg zxv*5^9CZP`Fs&)i!pwe?_cDLT(;*q*qvmgR7S~ZfE@z_XO>{f2vVM>rJtAG{5-LIu zZhj%6z82u<5!ij)yFdu)WNLN6NDyOipa4vrTA%%{Kpd-^8E?zWyMEh`-I{jfW>vsK z0_-WN0oPL+G_2any+F>#;o#>$M-vf^1MkkZ!@H3$Sut}Pv+?RaB-1m@X1n6Q^4&!)Kop1iG z^EdAZ0|RH-0Vmh*uK5UMFci=L24Q;n9#9;o&?jnEA@W!KkpsT>K9@Ni{dT)f0Avb~ zAU$_^Ubrhokn`;%9X1L}z_d3QXs^vt``MgWFgPZvM4e*pui6S4=HVkS$iPkg(# zhRK-pGi|prP2|h-bj_I;UA$LRE&shC) z*9Nc4?XUA7Fh+!N=k=9vo4~=A6dcWGtZGY06YeIO>F#ID7*f#5(6{@&evB+^dte+X+S})l`Ps2zIBRIhT`_?L6T6rkq&V=@U z00}xQ;7P&Gt%N4SgW4&$tBT37FF&_`&+yj;6O-YtCI+??jJRE$RmnYw+ePR0>|$_< zRRw$oY9hGqGti|}?(}+J*g^=Q4gmln$LY7MBRGyP^ap%5R5zWBjn138MiZnf#W6)- z1~n&efjQ%%i?NI2{aHc0m!U<*&d--*H(#_ejR(Rgjxh!uAZ3OqB7vMq*UfK$P(G3r zfWZ{v+M0xRQo4xp1rfFe71XPjdi#7&N5R$37^L}&4oEXKA)J(f5(4!5g!R`uKHD=B z!5@_U=aEkRq-_iqaivq_A$)MqfZn;)+VpjC&#Pr~$v(;3b{nvW{ZgU1lXy0=4z`AP zHF$6g8fQSXB4>TXiea3Js;9>nCMB*9P^6&SYAkOa!EbRIu$!{Zw||WntUV)%=RsYuaGq5BBaKXKZKkr9A1n86;IMozQLJl=QV zQLV}v_icQItVf#<3c4-+b3qLES;}ItGUO^qJ#Y?P9BtM3XZL7mHD_7C$td$lqd8g#7G3FJ9 zC?q=p!M8)^M9;B!N--N!nTXKy=w@Lb$Nu-r(uSkXd>j`RWGJ;|0Hm!%H3>G#>B$C>~5*erFo>G(O4uWx&RGUy|| z`55deGqb2L*JW+`bh<0MzQR?LM{9^wf-Hle#-jU+S(-R# z$$9wjzJioWx!J~Owtb@Cq|f5ANS98o9LFmC+%wD<1W4F*M}z{52WjPemI%eXiT^@T z896x7iuq!|3Y&==8M0hd23{&cV2a%1FH+ty5XHdS{5$&T)Ss^D@|Ilt+UE?NnM*MO zXXTsRoY@Ohc1ZmvXh@r9euy45CI^ea@mW}qWvSBGFh%VMO{%I8{? zr>ZKfo3*gWZci2>oVAPG!L+%LlyP&J1D|lloZ{t|#xNcO0g99O5WLxVa0Xsv$O*9h z4zb3`6lRwBGm%li-0o}a_)-8V%AWDuVR+!MB01CpHUmmnT#&oFlY*|`Orzoa)dobx z+9^4JjpL_9TAni2kyQ253z2XTfX>Vz3IM?A*g%(p6)uVwR!iYGL($IP;Ch=GP*%=+ z!?Jha`#pPF9pVB+lhv|iil`R1!-{x?aWNqdv|o>?c=n$8w;Nz#{&v6@+tBU>`u5kr zZ2bEHf~i}FB_e?R_J{IBNa8nwFPv;uxE(Q$#{AwMPrnGRU5tb36V^x_k^;EIRAyMb z@kmHQ2(yQA>8}~hEAMmI2PA3tK|SOSXMR^%jwdLPWO{!qyB?X)s1g!|#5~08U}lKf zX+6+wTB?Gd_fBB3MEBbgS8eZaZO|a;*7&Ojfs>P1Bc;GB_EVzsYl2c}Su8~;bizOo zLQDA(W@)5hM&3o!Q?G}^!H(wurBP$A)IE_oY$*3@;1VD731kQbH`!nT*`JtGIGpx# zGf&-wlKDC3qmd+;8#ceZmthyZkoVh+{610#JD`Yhbio*zN5;#2!Q0_PM>5a zD*azgWs;|CVg+_&SPm}FRb;ZsLu&fA|9eG`V0fuSr3MUDHNtQ0$Z>~Qp)v}1b$wLJ z1j65;FifqOH;IJb!|*q7s044WRL4~NzOCn2;!|(Y$=lesLbyI@g~%Gub{rEv4mSRE z$T>aGws~al?gO=RD!*4|iS-5o$tn*n|14Lkl&ebm(T{i9*E73%7W)_WmEs)veX!u$; z<)56YiMGDk_*{?6v@Trp7WTj9n=%zCZx8;1Q7si>h6OM*(Y}4+z%Z^2fvLdpBz_BPWJa=X@t2RoRYHx8UDc(FMDes0#t{m zJN~UZ$e5hu)m+P0YCvsy#gNyUH=C`7IXXc&lq<( zO=8fzy=*Cq(L{Mx-sr04QO1bafq?WCbYv*Nlnqbb7jbK3#5TOO&jZRZt`;q=3l_Kf zwbFyo9ag`ZTaEe*d1r-qtj4l_+xE^?v|B}lH@%#hmQ%17FXroFh^odA;zkaY%b+cr z1vBbDoXxt_A`j@Zn@HrUimr|C*wO@|IiAYrs(9b;?+{Q)7N{&IT?EEHwYeZCK0EfAr>{ zSRY-~R@`MDWMVA+;iNa-&W+>Y#$o&c!?x}-bSxEyh4U4SP=i5ny@cCOJQpekPUBr) z%`_)A332nP6s;}efR~4!0G;Xu3VUqyjuPl zBj#Z;-t*TJfm@)F)3;ogmTr@EM)&o)4C0lYKD<`V?}Az6Qlbw7v$n=QuK>--&qz=J zCW{L4GdjWuURi9z`*yad_4?oR`D2>DOx(^+x}x-o#wm6rKePc3R)_suk) zNa)2_=Fl*eNDkTBb3(dy^J=4CNadCJaec>Yy{S*h&4#`3^!i>VO7o`YG|OL=ft?KZ zZDFt_IvPWK%sCztP#l&eCZdU2tx2g)L6c>?=e|^|)VIv&nm6ehc1VL3Dd`o#J8MTm zahs$0vQ!{j-ril&)?TpOW=m8o32$HdQOX2Dt2J+1`qJ*Sz}-YGSO1C7H*j8`I7qv` z-Z#NVZ~k7(&o=lg*jNtqR^G}kt5mjL*sbWYE3WhW-D~ApZv1)e5FTE0{^~O;z^(lO zb{Z9dBQ{KRPl8Y)yHs?}w@C&XDfb08K@=4NK!*l|P#4AM%#yNe#3~|-%b|mkMfjR1seCoi?DE5+n0dikSY)(~K(n8h zfDn%VdVBjtWmH=k5RBjk5RBb!-w=~y)c4*;Sc`g|L^6n z;V<6s5C1s|))2V~f`0`O2i;E;TL8Tj>IPb#U~S6kl?2TuaZQ8BCs4l8(ucT zz&q!`N&*C+m{Ef;gAoRUF61bTmif){Vy`u}K(oq&Dl1S$mII?N!}Cqurs2+n*w!8= zCtISlwk1Xzx^$`=mIItdId~Fc4XCkEu=)oQ#h)>gSh_`6KxM+rVq@0qqQv6rg2d(H z14cD~R6!-Ca$6%k`#=G$&LBoxLaMLrmUi!MQ);%gXshm ztYLNS=bGG<1SxC~3xm?s{KVwN<6>W)Umm}COpY;KkeCAkJSs5m{+N4DPcDteGqg?) zD8D(cZ4S$aHZ^+QSaHJp(#I|Gb`##qFEbHdK-e&WcFut14k`tZOdR3)W$}f#oELuN z!jkw6g3R~;+YqC0Ad{p52UbWyE>5>GH%{@NfEOhcOb(r(DGIwblonmLM~K0GJ%_=6XA zJX?}uOk#$}z>+Kkh?5MGmIY;M$&TBD&w-m=W`9Hh7b=3vs(W`+B*o+Yitc?z&C8azUcS0a&HpH&Vg7~cK#>MV# zj>)@sw<`NpaGCD8*q=GDa9}WYgiX=DtGfH4N9VS^;+3sjQ>O-nTu96wB%W@OQ}ZEn z(9qH>OEgr;%2_xe^5uX+9xkFQUI<%y1KBxX!B=8f&XUF@GcFmRCdlc4gjPbSEpfUH7g7o7?_sS*||C(nSL%n4Btp5@L(GMZZh?ww`oA^1<3BJa3c|psE5qgn z5tIlh=kv<-OTIa~<0+;SmMV{ei#Bkewm14$JhG-}Q8-~IK!TDXup5JUo|sQ*paI6g zHNJP;`@8c4#`)t4^b0IgG(o`hsgrldIiys0)F3PQXw_V6MUsHx%4d+oA4+!lv9?ZOb7WqFJ7on0C#C z$Q@yw_YBZ;4lJ}939jb0^Ew~JWG6X8MafYVLH*&qSC5O_IX+LHgP`=pWVzMB1uPvOy%WLePuv=6un1Osi@i(*|4$xUQ+zW3^?GMQ z|ht5gg?jC8d}<|^!>%jA%vGa(DgPu&)S7x(4<@b52eUY6>!QMQ_0 ztY2O3w{qKJBm}x4$tKMhdg|^-6J%Ab_QySK>OL9W8_-&~fKv^ZHl(DWtcm2*2mVr+ED{POsX69VQ8k4yq_lZETMrF|P4J#T^geIu+37B<0T8V?a< zY&m%6xc}+<3)?QV&0fp<{Qe6|5?8-{)c1O}u5kEOcOps%1~KCd>gIx}1tgFqU`xFWz0 z&24|NHBRf-)@m!kC8nKpV}TN4AN#mfSpy~h#dZ&!PMq981zeVyuiR%<9q*D(LSm1V zX=}uS1<_p+4IM(eDMTmCxvFi(b&o}hCS8n!DjIQHGIbauBJ$^1rl7XhZnMElVh2V# z?~m?AmWc4soX|7eqWSw<7)Wj?dYV%mBl*cp4nAyP1)XaKQ^#>1wSQ&`-%`2W6Kcjbf2qlJ|hQyq925DIK% z!RKH>(S{NsO@!N|NZT>cG@J!F=~X^H;66}C8_Z_dUYyylnQK2kH_X%Qy^xESS@y2)B2t$@T`O-MiWd7 zdx(__pMP3=;+eis6)is@BMmG7ikJz@R=rZg!uytWSIoSc#=9-<5_<>g&tqfT2_bpO zY0Pnq2^0d2s?dPj%9+`axX)H8^C_aO0%?dz>3uZDRG^PCOf4DMkS?T}Cva+VwnHin zCQz1^<7rs(?V4C&)f_3-1=d7mATmQtC-FVmSoECo=(6{I7+Sy=4Jos^#QM zQW~$dB&hm!N95-Bk9l77!uw<(I}uzJS?V<^1rTT%C>W_0PPE2o-LShsbaRkLS)J8O z@98-2^1A>ivrnKHP(@jT@nlBHmRBAg_x5Ae!2@f55AjN{RA0LBJvmWqw`?K&>yG^U z+`0%)c>4m%2|Bj$?>qDEZ;H~e;w=15>$Ba`i!dqIob9?_*30vFYS0*^N0I7hqq+|a zSU@O5J!T|ICY{$j`{t+6DuD6P)Ic#4T%PP{Ke27x|NQgRylrp(0;7|q`6a%E1-Qjb zPXh-&N4BM!I(6OSTZ-=xWK7vW7=dCTKIU$hz>q@sXcd1_I(?MzWr zDB+Z8H;AxL?96-Mog>~qK)GJbNOc!bbr&peCoUB%P6oxE$P1Bd(%2;*0n4q! z3V1f_k^2o6myr@-vupZQcSOpjETpk+bbnHc7-gCepYrytda`uG3lCUG5@ikRSOeBp zh69f3`MO$lC2D71L#3&P7 zU*6&M&zG!!{sD+rgfmE+4R{j_4hE{{2~#j%+3$PdwS(Rl{EAIL<s zba$4nd-fKrQ9gLQi2sN~o(U&51IC7G40MYSES_Vy{AKXKo}?m3(TFpysF zY}HoObYDoN*_;7?p)(d4I#6KLrld{FZ5cI*?>ys1F1sY7nC`Xcd2RM&}dQX4YF0Y&%P5 zX;YVSaD9XJrcdI@HXB;7uuTlAx_a)I3DKg7S3(({Yl6L!5SU+iAHVZM2h^Xv+H4lcpxput?}?u|88FUE1GHqEk;bz%Q!L!hnQZ!amf42P@M19CiQ1o zQdVGL=^blLvC_#`S&C0~6Ijx-RcWHHao-AW!`=-bAJMGbp96+1>JbZzn4HJipyru6 zlOSynp^(V|?9B}i5KuVG*v|d^xgEcrS`u9dNv_eGoaI;*lUuu#-P^ExtRI

Sn%gg9yyk^v{BZg7D>|lL`H-*a!0Mt8UcOgpIdTxJeZT-iG7$v} zLMQ*@`Rz|HuIj$q>SZB9^5AQpWhU)u@~#01>o<^aivzKsB6GI^AqW97atbRAZ|-=4 zV?uhUWdJZ>NrKjVWHpVnhk=aHbel9?pd`>3tI#r==eE{@Odfat~3q=17lHpoEqghtn`U1TGqNinW@%Uxq`9; z86Nw^*4%rTo&L@>gAlV&wXg&uP&^r|g}5<@^x!J_S$dlft-=f)u1 z^KXA1b2w_oIRFl!Ae6_aB;jx@5CR#M8W!HWJW*72J^*TF6T^}qVWTLncKNF*;jjJY zlEwp5iei@oP)1)R7hG;8C`|bG9l7`KZj}#uQ!Ka}bg-O>Vhe`9Xc9KI^j5^Samxv? zK&7K@yFh4PO+RObau7S1H>kE;eB0B+G~Ak3YD>Wq4Js&>q)9Dvzw|UGr*_8I2E{?q z#C>)bR>T%RwYa6`!He3SQ>m5Xw6>-0pTLbvsK%t##{w_@e!u@$LEg(haBH5kb`_>K zcWlb*hUz}3?-427cFd-n%%zoIpoB&BxY;>*;q8e)={TA}?*<19m|?W!`~}f5(-(P9 z>{!hdZ(0l#fdZ4Fw@nUz z$PG)fbc39sL2=MuNO}!HSTmsl3|t%BI&7zxB2D%JPXr?hSIoGHGsp~G8Co#7Lm^Tz z^G1@U`*qu!1&?fdADk8X6)t2zRe~9OUqgwcvlS2kRUgv!;sSNUa<(1MR#qA?u!yUm zKxxD8w#xR{ABX)D)J$|i#Vz4>g{9Zk#eiz6K3UHxno68-m}w&hkEw~=vY9p%s!5vq zBep7+3#ALFWov*51JWIG8|T}a*Y?KQ>oJK|y#w_+HKRhn5Qkw}_tDFvX8j|HlT zn_Ssxw{>&70JT;25J~ZI&zUik!ai)2+qEEba_O}C)p1Ig*dCAdce*;82B0_6DyAfp8MA7!png@#&AXBU@lP4iVfnwl$jBC z+V{Mz0H58mACv~!eMVYac0=hz1R9=@BW*`;1xdPngJ;G(QINZu_X4j=JLaLURG=`a zQOobX5&7gSaQ5lIgct1Jcydd@-9O%t_jTSDP3=M7n}rFtL2l z19J&?XC%$Z;OLINhCRz{Q1wZMHLoPJ{)XLe*{5P`p9mB5067ZFgCb?FTlM`jkQ^is zY^k$P$Id42^1D}*PX6s)|9|3c0^$&D%%8RJ36#1%W#9k-11iG-)0a0^U}n0Og_dEV zG?jCB)3ZvXJ;bOir(2{%i3znqPy30T-rAkZn4r=CFM5XB!K@^zBteOOJ-=QPBH%!aG!7Fp~`%k`P1_mx>?gwy)Kge zSD)cj#%z4yE1nZOH)4}$x?--Y=LvI5@vAZ{Y&TS6aoVx9-t#V2csDm{?*oCr4OC7w z*Vx1GWlfobg>xKYiQL_MSc-Kv7@lA`F!WC>`{nysK!A^zOuCYJ0tzv7-4%S(>vRRn zgbK4*@ZqaJRltf{X5IJ;-IrfLAW?SmFK1Z~o(uMVXrYE9#amv!%X@6wY5@2%XYS^H4R~@1D}JnC@gU#`!)RB*#l zm?#X}1;7fCebQ{|Sa?pj#VY+suWdalx z|3@-nwyTJL0x!HDEKukkGvnr1@xU62?T&j1Uq?g#~CQ+{iES99)6o zrVClq=j0^9t$QrKhCrlFuZh91$l(TMd2=U)fKS-Mp4-P1%heakg@B>trH!84|as zi4`rV+f{VVyzc(GYra!+;@a;nw?)|}#kyFk7V#0~29pFrfQx}x@j|3@%B94P+dU=8 z+`9A#>mGmMt$_)1miUf-v>X>DV1sBs&U;`|o22EK35MrxS(opg%IEdLivCCV{&WnQ0on{CqQ~ zaB|@ArvHXnED8hr{bZQ|W$I9k4tVRAKJsB`()`;J`Qzsq{(?$dQgR?Y0HF`R_R~yu zP=N9$lj09v{)WVK7!(oioK;Vcb`n4J^m-r6?zwx8lE zNvsNILlFB56u2}CYQI9`qWj)Xp&A=f2@QA*~y(_$RAFsmy z4^!)R|2KyO3N6Wzj))*B7Pyn8G!^3m0}~-WM9QZx0#zvHtJ|>-+>J;wNS6C`uHXqo zv>5ofhAZxWBRr=1YHYN@G{9-Van+Ht=OmON%Qn3DCAq7m!SM*TeNKW!Y~CLzc-oF_ zh>e|btM1J!h1qHK+F8WE)55S3SV8fqzSLU0( zbt4Y%dsHMP(pFct=GQmC?BW_E<#^{IQ1t6S?O?ns3apj`M_i@d{p!Y3H-0~F_(WNV@<%P5!DL~$LumKJ zOKxqt{LWWHm6!Y_FhId^(W!e7F$c6f0b0EMkKYUK`O|-aE0oC~ETzqOj0bd&Tud@i z0#dzN-L;Qyj|a*oQ$5;u-yff`@DJDaKyz$7@Tb7xjn8;b?tM=Rw{`G(7>WysJN`Uv zyE0yd#@=^ViO1bRjbQ2c$k+>R?y8!17g+9eRO-=+-R&MoA3zz9ipY6~);(Xc=IJ|e z6^t;<7Bx6v@pp8RO)FSZT6@iRw(DB31l&$~jhZ}oLrR7V!?BMd8fw>Ck8RLekE~PM z>$XXe_G94I+60SN7O`iFLW1f8g`pVow@CoZMOZ&eDI>8bgicAwxGm&30zG3M@8+A`!Sy~o$259&`5=Bel z4Wl-MK$E+-PQfyw0`GAlGCb}BaC1;`u$`~#=KJh|g_?HyYqo53eYh8g|HSsU%U3;l zJ?A(g9WholXkeWJOplsou%x)*$_G|O^Ttk-;@yTTfTo!UJGCQM<96fHS*Z}}x_qTD zvz+IhvZ6FQbhrx^EG;1B9bW%Zam{mI#rlXNhp`a@6dr@OQV@f*;*n@)b92SJKbsw> z-ToHF0v_4Y4%UR6wo-)ezl!=JkOb!5u%vG%-S4RtCh0ckKzxFPScF(97VmiDMhkMGe#M1)ZuvM| zLLgeEaf*Tu_Q72xZ?2Jm)|1?$n_hy1_d2cd;BG8nqSiSQK^j7^oMGZaC3|GKtLY@t zayj1B0HDnQB|euoJVX9NoRE{9wWOFV$>p&C618Pj^tK%NpmgJJzM8jV+2cU#(nwH6>xF<+jBTM30GUG^;~_*HVO%mj~jZhxYfIe2`b4RiT6fkd5RWdJw+ z_ck9|AKbtC=l)|Ky^aYoZ?56XGDi;M8(Cl_hZGw!8E$%EQpmut<3YNSXgij*JK2;R?l4!FD#2-az1?e^rs%(p<><9A`} zwB^k$H~fWFraymz>B$`fR*`SZn$HxWj%Mk)pvSwRaM(g49d%FuJt~LWj)H>Uj=SD) zfH0{Raq}}o_uR6ivkz&6oQIvvZLBN+mD}OGTTTsZ^Eo^6(mfZ)L!z(xg{8RumvcdW! zcuY7WI(-75&&(ly#;$i1tkGi>tkGi>tWmH=k5RBjkJFF;1$_JcEw3?&ML|Vy z2lPj*D3-O9ZY=Q1zXjTDQ^1%6kfdr7jQ3n`?MGPIFU>MYbzY~)of}TQrTkMB; zkmEeH;EQnZZzk38$4myNGXqR{;E+BMLh-y26Pig3Ri}n-+URi!7V%4_CnOssLD?ww zOC;nKbU(b;?MNNK6Bmo9d?c#TuAU_A2rGO?uSzw=q$WZ|ge%LOMFfuRxdk9HN3%rw zKm!LQ3Q7b#J;k6#Ql~mfi(eXz%UNue8)O@$$ly>1uRrj zobwiql9rgbe|LwxZgY$K?>pP%bpWh{0Pph%7OFIAj3NMipn#JCjEaEKI8x{`>3J22 zmCMJ)Z;? z&tG3U=K1YgZPIoHP~#IV3h(3LuX-LhBV^&h0e8`(0GLXz?p--CcJqqy@h_AG)hX-^ zAbAWEA;b6%PtyR%U>BgsG;R4sInLslqB1$MztgpD)#1R8);EZMkAY>*vKQ%XEbntyNC&$13^Q{hbP3ZxoKwj`$YkDBGGK=!V$(t zhTRE@k_C~$3m}EyZfaXSFF&#;c-M{=vDPP=Xo>M7jsNg93#XiMfU&uZC4fn+Pbko< zzPYgb$!llye8a<=AzkG)W_qK9p{Gy6yzl)w{vOk$h6%o+@_tFan@YRd<+Whq zTP}f!sOs{-89LR3Pp28vc=MX6@qhi+d7Up+6(}3umA6Se^G@wOHl1)iXPB0Syk|JUb4CCM)Q<>!Mw?w~ zO#8`2ZJW#d$`l>`4Vx?GNG+v&GKOh~s?0uLjq}y-b>tzI9Tm;~cT-2Ym=FS)T#{Hg zr^r~a@r3)yq()4MA3pGm`m_oJO(aRQ4^^4-AH29@hfh+=4GlqKiCG~9{*R70$C{y( z2k)+A3cTF+#dDIybE5rczt;s$w0O!{%x8?(aQrbDA20$CRjr=nwK)~h6+d3m{<_b_ zxgbPh43{(=)#f;YqX#6*IaTJoA79Y)COqt-VRFD{tc+uDeWo#}0wUN(jH&TTOI$e8 z*ekr<6cY}&#f65rPCG%$MGeC+Wl0eIg64-|Fp5cvS`2km|ZTJMVn8a6w$78cLqi?JOL%K~8)EBPyrOF4iXBe}2bC@Ux1hkvV9b zL*-FgpyARR6IymOdS2Ml;#$4GOWfKV7mvkM2w?Uzh&gJ94^WtxmYh;8L2Wm%_q9p8)5Zcrhd z8@$0HM`(9~Mz*%dn_oQU{n?3_yLOzU% z7~kvxRRv%{DbQyrkqn>Wet@zB04IP*K+xB=iEqDH@A*ZqA~b;tyHJu14|DpRjKHT0 z9uBN6Yi3z;No!QxQQIN!$KT=H^U$8dXyY}Q2zAxu=dCita?_baw zbEN5DNUojx3|GB-e)kg-Lq>T*AH4KBqa_YnB8owbDnBXy&iU?D0W7p}-3? zK{CL6mzuuudg-#K4|@LO<}Hu}1SNrfy7)m#B0%&T{7+o;g?jH(y3V)F2NI2+6C&bz zx|SDZ31FNi%7%Mlz?}ZVG4GQCwfRmSIF49O8l3fwh26gyUznJWyoRIcTZIfXvLPPn za&NwKed(gNJNP3ZmkEJGYMNwtf`D?-Y&-lSN+y;a`n8Df<+DpZwidQ;=lamwWj$3*`H0+z;L$Z!fYJ6i%T-u`Oo z!uA*m2W3md*M~digPaG|Q<2+{HZ;ha{%u{!+@#92BWWK@CLjj@#PrmgX7>DGj9(9e zbk>J5Ha9AOgSo*0aFg6LtLJ`vh6b}6x@CiASF8V}do~nauEWJZHDSK7HnKh=RYNXA z+}j}?xpRHVxk;7p;CV)-)8afB&jRPC=JDvopPm{0X2L)uSwrSk9`V3IX@lvWS4>iF zswzm%GBn%wF%XSFNy2;`q9N9IxwhWBF?5+_8sJkIc}Y?IB_j}Lv9?31|H`J)i!A^G zrkJLya+n`Wg}oZz`=wb$>cqIt4H;1$(ZE6Ci_#|KA^f#9k*}fPW+M6BD6!Kpbtf)_ zn@lOSD7xljNwNLkUn^b_2l*oNL>TF%C@2XFT&A?WMXdeK)}m{W#aZbqQ|1$bPsx8` zYUJx!7e>hk5I87VP}*EJE`EJkAW>r&obGId)n_(?nTr9%Q~(k}2H@kRT31p$-qa)S z{_o92A3Yx7JJEs}4b=mEY75ls>N@wcs}2OegZ?JC_q0?iR!LxL?8Re}pPmxZ#wPUi zEwsU`5gn^SmEx5wa^-~R?ZmK)l?=;bNm(qWgz;y1y*tL&9}dgg_jgNMj`RpS8>8ak z&Lr0oRk;L;A_zY6s0Ky@Xi^JJa=35ui?;&z&MiqUpHQHjW2P4kXHtg;X?w+l=;wa8 zJ@g;o6y%H?TUa9&I854rC3Rkvdj5FWG;pu$0!R&(fdW?5fp+<|&8_Z#>}r!=sS69W z-AP`>03_!G83l|jp}G=AcA3e0E#zn@JrWu*9^4uF{Eshg-9gg~iZWgUvUzEB{4EQsuACN(rXG-8PElRdL?AZ5^;MX-R0%qUl6=FyPU-axCp=GYZvIgX$cb1{l%q9(bx|q z%W4({_pf`H54Sa2f=JmFG8M;I9A^?Up>xJnlM>h5IHUW!#Q|-yeVVk-!1cl|7JBrt zH6_XOubLA5N^QI65S&T<|YjeG~HPao6MXE2CFgY5oklE2bB!=XnK| zOnqjPg@3|;5cq)vE|D1~EcqLs5&Dr07?Ocqp~qk0R2u1HHm;f}xkhsE)O za=D%gG@3z7O#l0@1>L{CXhP((EMM4BQal0@w<-U@y0 z@qM1h0+N+Ac<|@JARs&1q_s+t56glTv<6_j%<%^RukvYAOFep7b4+MO37!i}a})r_ z1`MZbI2ALONdN6crX-pEv3_I9@rckWJ8si6TU7zkrKYa)iG;2N z%0khzSKb<)wCFa)Usg>JEV@DjC5j1Hsbk|^?R6W0nxbiwc^wlxsRA)x9%+rfn*^Zyv`mdY)@oc zQ5HV)0HUbW)wtW@F;&OP$i{>=m30(2=SidNR%^m z;-GrL0=oI7iObM8oC^}kng$ruD)H$xQ$y;!q(MohX$~n>71OwASmitH1e1Xsf%p!e zWEDs}^B|aK25>kaz`A0fS<<%T=3ksR?}jG~tYZTZ;rEd5q3lGm8XmATcw*(Ii^i~$W0WME64wlW zaj@m0Tb_@WPFch!dI2z$%OrFF23T)a_XA4+BH(v%6)XytEBCXrid;r{))EXo<8W+o z>PZrkbdGRf>;Tpzr}J`7oPfgy90U!o#5BER{Dj6!ZeOQ(i^dAch-Ry$jP*jc`rC4h zwINCJ#xV~$**kLTSrP;M?tXuaQs6MqB%@lQZ1S|GWp}&@v85n_C@au7A|tDV1+{1x zrjbJboKH_3q8gJFs03Gssu3UoEJ@TOQZa3bCe6ifoC3efJ4{8 zv*3(A1h$%dOaIY2@u1T&{!7swBdDEK=5@O-deo)3+(`6zhgbzWHYH$0CZSRS*pci{u6)x$ym>TbH&qc%OOX zE&qLf(WKdcCUsqmH_a&0FG!c(QYYkaEF$dZQrSZz9ysU@Hgw(eh2+)+pZpy$*x8z7 z5Tp}SlZL8ERHCibuROS@aO+;OwL<**r}RCXb98Re_K~_ZhK5Vg8lYP z)*Ojxi&J&!V&f9A@DKBL1dudu9`ikQ?`wr01(S+o+d0B1hc=4brfWu_az3$%?m*{f z$J#Wi9gheH&?|0^2H>E93F6;Q`*Obm$do=iKPOvBs>IO@WiPMV^GxJjQ>hOII049P- z-}~5(qJR7PYsJ^0dJuBZ5flk}5}Xb*3zdZh9|{!9 zNtE?=;+DXFmsBUOv!mhz^BW9k^1Q_z47;|5RnGE@*<~S~9^il#1@eY;oPE{zLA8Lq zv_QmDAzjdbZ$|XBAKF;4<|j*uu7YhM3#St_{nVC$jUWBI$?a&9c5Q8yZz*&I#44XL zq1aXo{8sMO`QDR1-X2G&p#q?$P_jH|n$Xg82CAz}R2P)S-yo@4f1c@gZ`et(T z4A^rVyg2d@FAl zXe_LlGG+r9BceTtqjIoFoeF~GjPLB5LXVL$GuN$DQayr-JVf}&r1)KqwBgdZSDZ7k zzr(xvXjnKVgT%~b=k?r^W0W>p(u|LpeQ~s8@?!kEMbnHnV_t*kB4wNZ_$jfq?kIpp z94VlA$89fFE%8&pw(AjP}e zi?;pcP7@q`G{Y)|Gid>L;uM1;x6X+?;CAVSI2k9M*c7YS6iu?Z)%{mcQ_V8?q;lQv zC8xl_I&KU)M#s&&4g!#(QxG@c2gTZNR8d8>FWxJLTO+0bp*ZV<4IJzk->vZam6H-5 zT{5os2GefQo1TGTV~+g2&i632;|w|P=#f)$4C=-(B`@i%p7RN2LZ_223;+S32y%Dh ze*eJ@j~cRv3?Z+f@2I)7QsWjz)?%GjRZg2$WK`cZulx5%x6}1JBj3U{cl+ApwOgCT zH_#FsirzuFlQ;kpFr6*6aLi04Sa~*w^dzlBbC5QmnnDNGeVR5MWy8M#n z@|QqF-3=T{gNS_xpNPbWu9>DN6TBBXjQ%35BE@+X5EdriyP)m)%3uP=UTg4rS{`@S zG!neSyIOrOY;5wZg(&}!^>oe|aO{K|o+Qd@mLjjU(&RRVAalOsJD=pET}cQr|UxRx}|XQ9&gj$X9D$GpWwr7VF;eu z>g+UgEHt~58qp<1T)?j8~H(4b7WqV z{pD&!s60C6tYzJ^F1xRA-(Kh%NLzC!>a908CNdo1^NFKB;$YDs)a zYKn2X*MO+Jjsezx94vTXM~k?pFXSyptdNDAk?hAW8z{{FuQq?jw!Cl;Z z&UHVDmrq?@w*JwN5hbn>LE?rYZKFS*L6CPQcx`T(HtoJe9selttCPV0GN|EgES@Hq zVn;-(e{_%k>s~O$hq{?&PRKD9dSU6U8myRwJ{WA(mxgL%q}bDToRue*-LalZE)02_ zLqVGDK0B5e*QgOUc;i#E;y3^7lGfT1uQmxe%^-)jS?nAEg%9j3yrC;WRZM>!>ZY1G zv#M}+wP5+o)Lh(jJ}oYHN8P5umzSO5dH@VgjhQ`jR@~k-_2S!vc#lRnY?PC3{tsr^ z@RE=WE3wkai`o`l|69&>T<2_$?9QZ?pc>QyAB{G(P^E$@U1F)Hq?k*T( zTT}y9`(UyTHbQC>VqM4Wetp>#se!&RbhBs1vajl*2c$-Cwxz_F@=CgAF8>eDv7M`P z!EdoaY(Wuxr=P8Wq>Cg^ILcTPO`U&SL3}5I2qfIaLR_J>;58NZ{^HuS$vGkvez+2 z_;tmK7miz?lunv2H}BtWiSCSuGCJe_-lAm zP&F?6wM|7!J7Of}6Nzv-r{i$Bzzvf!qU?rvlb>mv0lk&jsEq?_C(??ES|r0pNPTJw zxQJ3beu3^OC}kbHa!~k9F)tm27Fb!Aat2G}<1)KF%%`EG*kz2vCq`5FBnBLvxDy>b zXsC{p^K`(?-J8mm9O&fheUf$BM=Irn_~EAqKa6Zp-bK@UKhtKsuJqX1(^(bKu|Rq< zSb3I}D_>AQe5(bOb8Ly2u_{INLAo`uk%=DwOs1v902&P?jc)MDT^mZz1Aon1I9t>h zhB1*jjE=J*??UF2ggzf4A5Fph!*P5!n>uVb8-BasnIJV5%tzFok=G9H_AKT;6YkSWJl+ zZht9;9kPlknuq?ACe$MI>MgD+P|72%jwDPiL@|qC2^kahHb9a!&|`BKr`_i zry3!71yd@NjVdRPpS~Ah@JD#r6d@$h`|DkWci#3&@wv6_d@W7{b|wLim~`6Qa2$9z zF=c^EM4FgFAJ75;puss9E?>FsEv%+^&NQ1At|)Z`5_)^h_e3pWn9OUOc|r=s;unb4 zhxV=c7EzPz^JRErwhzZ`REAte<+~dp^{d08ykw$SGhROI|JjW%mDfDB$NwOxIO-KG z9?kHP0+^fvhdoFD1S#5a7$r_x2*CVgK_*H;9)HsiTBQk(PbwQeEn}N)ja#FAN-5|5UnB_4kny zx-NOS;I7p_N(L)tchy|@Sux&&>8mt7&oqS3OjFZc0Xf)Jx2^QG-+wv>$z{y>H@!$( zgpCO-8!RG^%?bce&0%rh+6MRUUpnUf{gJTPjNK%HIGzaSeV(RZ<*)*WZ7$AI=x(aT z#3_`#X%Hi(-jb!0E`sPY$wkU2=Kdj(E#2_z8+k*EbxyhHb^@_0UdINNY_cBLuTy7g zcCSfUkX8_Nk6#L$s9jUB{*fz*64!}cv!3x2kCG)IS!79N>^iJ-5=>rfiHiGowaYJU zJmG$NXPdmSN3oj}AYj;l*b!hcoj!@frniV7aas=uiAam?@|T0ie}BT8s)`6!%+iX+ zo-MT;+=bn>vb36}*Z2an>QKb>=Bm2DgL^xqjYoR;-Q7u^wVZVW z`)TRsKi=p)x@`?u zQJz6Bm5JuGXLf{uEYif=ONoYVMG`_0Vp#j&fHwj-?GCec+myv{?%lMTLC>%KTIKr_m zx2E&`)AwYf3U{hYkqhFeNKRJ_wPgJC)+IMT(tOcvtKpusltLegE2p*NSgS-~6Fd#O zS4E2^%%i$uS_AoKoP1%WXxyymlnZVM?s@UIx;rpLz5ZD8&uJF@%vVfP`+)WJtOS{H zK#pks;*c_C*5%=G=X@qnGU-y)SIn2}{`-%`Xh&Rk`>}Duh-~$e+ol7|gDVp6u^oTn zXD+|5Z_w%hj}6v4A*vJ;Xg&`5G zpkj<#R&!ybV&>I}(#gwoZzx2Z5!hn9i}>rd{ti9ibc8zpat<6vUGZ|~u_L~g{i`Em z=3K?A@ihAXVL(yS6>o8Q=iIA*P`2T*+jJzV#2S5#l~C$}+Tp-4j)O@|m(QpA3dgCz z%2`VBxCP1L@#iUl@;Qd=bFmCsU13v=Svj=imb~qU*UN2(cUgjKNQ(hS7I0EJ88#$% zaN|#V0XXTVIj}*lO4RJ0eBrI0=6$Pu2VeV#F=Q%hXqGclNl#YI_)yoZ%f11g>{8tm z8gGDK3(S0$^@zSC%#n6RVB7iD*dZCrKj2)X;LvLiv4)wl#Es-0LAeJG#-PAA<8Ii! z#?^NC4G1YL0ugMGv^sNPR8S?Y3qJXT+|@8c=x#az*BaIi5`&R6@HurOP*Tvm_;b&K z^en@=G3I!LF8~ztJhFta6ghr}q=r~qSHnKf(QQxT1f0`Vx1Q-5lFsTDt0h+4rn;QTZz?f5@dC9HMTP`0DPp3}k(2P3FC1`M@5)~!Ve9^7Xsjgs|kmyku31+!L zw&CFP;W;@kJ@w-jYxhD5-hSn~Ac^3;e6D*0jSk@0ow+=m|JaVzzSjNgz&p&-G@l@* zvebkY4^_`@S^CA7SegbL%7_II8-1-Ch=iskxBMkmGHDU;It?pA$vc`Hyw?Xt@3|~^ zI_q}^>)v=A+pLTR2|IMN9B=@=VUVI-f4>KZU|54lo4_he98(l2opwp{(l5Qh!IMmY zaArNTGv~Vp62wq#&|G}mQ<3VqAH!LrgS$_x*y2gyThHD@l|<&bKW7YZnAC9#?h|{r zhU(t89EwynBbj<%n0x``TtIUGLOtHp~Z zEIj_Z?AP9UsqR5G!J&}nEKjn1mqJudoB*;LRLy<`fZ-IY>A*S^P z(Nqh3Mgq;@@U8(=FKaT1mJdZ*IEarq!CuUutuj!Ae572=Ku2%};#F-B`Pg zjUE^!(&qF4$4TpSwI4l9+&(#4Idd_uCAC3G4D>9USfgTb(dx4=PnJ(x3KDFm+}+fK z`_Gy_GB6H-r{}ed<+9U3kgBv*a4$1vH^C|yKS*4!vh>DgIcm@gl2Hx_;YYyf~ZE@s-OR#S$w3u9UOoBXd%SI z0~}lI3ls;|7gditarxbEX@WbL?ZA5cS@167--)ZIaSuqfzj>P9enIMLc#BU&qR6Q% z2P1X`#ycCUvkqdG-7fdtJ6!Amp% z2l#d{wNR^m>4CX&%fY>9MUBMiI3o`@#IcmXtX3arPZ3l#SPa{}99hR}jOR<3>c0&l}xZ=UWkk)s|sec%D}HC{Dy z9(ad0XdE~*3{``}$<9XuMBE;MiH#ryj&UB&33*_vvy=H+cD6cJB0J4U*$XVLq+=k` z1`DfgrdhcxZ1sdGsTP9fLSMtKH*}YORD%ZBX(7k|yqREWi7rv@K5@d=df@3;^_-9E zvOfq2ZvJ>;r;h#W0|8+&kVwXinm&&9F(F{*s44fRBsy zL<2{+{L=InPe_zbnhX9B&^4$t=uiQh4&7&B_ss)H_+Z11Rh6$ibSdB4-et<(GX^xn z4xAJ(AviYy)JmXk^D}}LZv=~Gg(0{^RwhE7nW-awu@Yk4klZ5PY}w9Lcb2^I#9a_{ zF$mU+jWP${$On$I=~#hj@&fN}+PB@`wD)l>R54o#RZjvQ0_KvgIgYlG2q08TXf=V9 z@$tTny3Lgve!0Sb@U>@6(Zk{AIuob;jS%4U=?@D@Gav2j3>?|~dr65LQ;Nnd1oJpZ zQGH>?KbvMd5+o2x7>nfz;SgGI>UEdweCF=b*Z*(}-`ml~%x&ATH#!sUM-y-ucx=^$ zgq6CQ-`*KGw(Sx0lBuDpxe!qFfI0vGF{ha|gdFJo@M5rxJ(MuA2dgfVMTgcuSib2u z*Lv!AY=pQRCA@UTsiqMRoIcgXq>qfA=A`%du2%yMyM85DdQ$b5PS-sJev3ohhD`*l z6o`hZ-r7p}hv4tvWRy8ea&e*(=?op(@Nn51k6-UUxZ!D#h)Fxb?YN@P@HH$Bz`@-i zMgSq;2t*kt3>*FuUv%8uf9aiY?q?H4<1a8ULlX-G?D&%5gvgztt%C-Wl`7hE`WoY; zJs#s;=u8Bk5OH-L+u}dE zc-8EWL@TCW0pWuA5I(Tm-=;jkNHH7<@P5r7t-je+Z1fOe(`@Yd5)h+`t8%z`r?+|E zYH!1yzeyeSyCGtZV6kU-vUvm_yiwPq2RQu@#8MtClTGr)Fai!@)dQttl%nzHCWKv2uzfF;WbGo_HzzXe1p4-7$uo@6esCnr833GjU?^2;%WAP%<0RBGpjr%<*JZea<%TC%1%Fm(vac9| zngMXcc{R{GNVq|o#4Sw;<1}C{(c2=#yBo1F6sJTJY&Z!*ipN&Nq79Z0sB}5i5=Qmi zpCqjWyHHq2T#0R&A*mLV(R!)#c)i25`^ZlJVskI+TyFkQRmh?#@Y-xfG(`GpK>z>%07*qo IM6N<$g7Cm5Jpcdz literal 0 HcmV?d00001 diff --git a/examples/.icons/Square150x150Logo.png b/examples/.icons/Square150x150Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..dc2b22cea563cca761896877ed8d72502c8fa241 GIT binary patch literal 13032 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 literal 0 HcmV?d00001 diff --git a/examples/.icons/Square30x30Logo.png b/examples/.icons/Square30x30Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..60bf0eadf75c86e5d25cc1da13fe5ecc2c39c559 GIT binary patch literal 2078 zcmV+(2;ujMP)%q2v2@fKtSxpMXgijv z)fq>Pk#L19Fsu4()DBYp45dn$d|w7L$xim4(ghvO{2KV_ z+$Hm-4+=-lCvt6F`pHd82H&_b<~Ep`2qG%4yaCtnEHo$PE&9r=><>InhMM}#Bal70 zUf^jnRszc83E!|N`Rx5SryfTxDP-4{&|41om~VDuv~w6qd_&luzjUVa)#`{hUuj_d z;-TjOk+N;g#m|ICGUpTKol4b;)1*uMHFL8MKCv|ZGRhN>F6J+sEPnK@OTq0qxW^;f zg69Z@bb;=ln|bV!1*zxYUSeI~E&nck;GNdstHrvQ!1<(5!-Ux5Sj-IZ@~z9_hm@9c zE=m9VKe5%jFN9tTX%YuDVJH!eouF`WsM|Wyn`O<*Zm_?u*sYJ{@7&uK*yeKV5{&S) zf#D@;b0Y7ma@VA6+_UYN`OwQ}B0oEqG|om0!Iap5_k7kT+~I_CKtiX80<7AJ0rPaZ z$s@I8-V)2;;r=XZJJD}8sl};Mn=-J3lr@S-XWVVx+a1}{8floz^ZAs;b2h>Rf?{4D z^A`SaVe^surn)X<`3X?*m}zB|{wl#5hVBPZ8SoJ>C^ggHJ& zx0Sp1si3Y+b8e32LIJZ*~&~tlQ1KZVPQ14EFNvvQ_c$BZaG4nui zke92eo8#uY4zLT0B79WUpPv6pSzi13~yl3@aY!Vu178d^4!sM@D=Xy%{JJJL?)ngsl-cqztS?SeY zO~&FDDw{A}5^59%@@`tejg+Ez9(j=&w@AtmakDw%=5h#Acvjf}6hIna>VW==n%wuW zCZH7%=)I1>o{a}fmn#of&BN852^-5mIThw!4j|1IO27=2!$K6yTml7F-~ax}>{KQn zjE2;Xz?@;%>N~$rG97V&c+7&{K$p>Y#^~L7Cb$Xw{ErmEDfhcxH^RoXMF^p8&I4{Q zxRJ`)US#I2eyHR#ESu;RU?yxh-ZOLm3(FF9_x?mxK=kz9&3dZ0L&1z8uv*~egMsD8 zd)R?N6h_bl5S7nlEzr1Es~V=hBY@**wzB?9AT$HY%MG=k_y=&W!NR0AJkVX#dURK~ z_2?_Gpf0HyL{)>vU6ICNaT?DSsfHClwJam54_rP4XwZ=jbBfkyMAasONq0b2&-iAz zBvH5gX{-OjyP?a+_H(P4zygd3HYa(W*-1Y$Yw;KR?s)h$5h#jE-$y08_f<%$sdOf7 z_$KU5AS}l0v`x4-Fg$zlYTz4XY8HKy`fjHgzkGsvIoKYcseu!oae!Q=Y8%(}ec|C< zoEbp@x~w>~BB;^Six{6>uj#+KrjZ|wE4j8-`G01;LAUvP8La~Av50E6zu%( zZY|w^R=t)Fm)B;i7OZi~W-o%i6Ra$VVl8{NmL1voxX>*yAjzj}sjk!d6ucbxl|v0H zpBijfwOJHk8)-|xibe~OjME1Qpjh+UKQ3v0dkbg-j|+CHR{8pk0ANTN33RsnE7<-1 z>mZ_t(1Vpgg{5|`!W&TAnw=gBb)0;o;?SSI6~1`*ub=^P<-=l{CUUA~50uWpwR{i} z@h5!Hn literal 0 HcmV?d00001 diff --git a/examples/.icons/Square310x310Logo.png b/examples/.icons/Square310x310Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c8ca0ad132e0184b64cd2e4a38cfceab3c4b3a46 GIT binary patch literal 28507 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 literal 0 HcmV?d00001 diff --git a/examples/.icons/Square44x44Logo.png b/examples/.icons/Square44x44Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8756459b63d6e8dd6003b97613befbe2743441cb GIT binary patch literal 3419 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 literal 0 HcmV?d00001 diff --git a/examples/.icons/Square71x71Logo.png b/examples/.icons/Square71x71Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2c8023cc824417a29378a68471b8fb3af33fdeb0 GIT binary patch literal 6027 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�rop@t+= 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 V#?y`xQ)U1F002ovPDHLkV1n!EW;y@> literal 0 HcmV?d00001 diff --git a/examples/.icons/StoreLogo.png b/examples/.icons/StoreLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..17d142c0a43208db62ca28b86a3239329ac93aea GIT binary patch literal 3971 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 literal 0 HcmV?d00001 diff --git a/examples/.icons/icon.ico b/examples/.icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..06c23c82fea615c9394c4c7f487392e3f0a2fd4a GIT binary patch literal 37710 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
j#(n6`cTK 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 DDbi4w literal 0 HcmV?d00001 diff --git a/examples/.icons/tray_icon_with_transparency.png b/examples/.icons/tray_icon_with_transparency.png new file mode 100644 index 0000000000000000000000000000000000000000..99d8ff2bb4532690998f5c50b38206b5fec780ba GIT binary patch literal 1015 zcmVNxWUnbcmD%wRJvBA8hhUtjwZ;x$X=!QIFpLd2nU2y$FqKB5Q5mI^5M#*l z^74AG*W1LGQkpoQVRMnVH!@#sw_&EuBv9pPZZ=MA;yS2H}TIk$s?*!`Pv&mlQF?WAskgymu6Z zegwgbJp`~y!s53?J)e$x6?{Czs3Jktjf?4*MUf zeIx>KY{9`6h&4~*>2nxgmwh;RS}K)V)oOJBHL^dGZEz>AuRk&XW~VwpCPO5;xw-Kz zsE?&mDFtV+X@BGVb?5 zrqA_y{gbXoLP7T(vMwV)X19;?eyjylkZo*HD^>vBMIx`-FG0?gAc(?|2uTEM^)Dps z{}qC(={jbB5i=PW@d%0u#Y?0wKq4^6AOs4kkzUcr4Z!^b+v9=*dmaKl0f9GRFB3UW zCX<1Q_z=L}-&49ni}NmEF=7eE?_Fg0QeHxej)fp!LuXBnA<#-#X%0OF6lSRfx2Wp? zjr(e!gk*372d4Q!y%%&Dc@JbV8J_8y5|FR>xUL(V8DIzN-v<$IWB{rYWK2E`qDKY% zS}Yb9dE=3w;?J?GX^va4t3)}L;(HHpPqDXeyv43Sp0|1d2`fI@d;#;eh!9^fVgW%d zClTkpmrp`}(NKqMArp!ha!dq zmSetLP)2*K;oi_nHk-}iRddGDtm^XE&Cbs9wKRBw;qv+j^e5whDj?hj5g0Nsj3&6z zXFi-30-oi&AYkmGFz&yi*a2w4 lWDrQ2Z3Odc5~u$~mEST+7A)-%9(4c!002ovPDHLkV1i&3(Psbv literal 0 HcmV?d00001 diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..3db0e7e --- /dev/null +++ b/examples/README.md @@ -0,0 +1,5 @@ +# Examples + +Following directories contain examples of tauri app functionality. + +There are different execution steps depending on the example. See each directory `README.md` for execution method. diff --git a/examples/api/.gitignore b/examples/api/.gitignore new file mode 100644 index 0000000..fb4eb33 --- /dev/null +++ b/examples/api/.gitignore @@ -0,0 +1,2 @@ +/dist/* +!/dist/.gitkeep diff --git a/examples/api/.setup-cross.sh b/examples/api/.setup-cross.sh new file mode 100644 index 0000000..109d3f3 --- /dev/null +++ b/examples/api/.setup-cross.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +export ICONS_VOLUME="$(realpath ../.icons)" +export DIST_VOLUME="$(realpath dist)" +export ISOLATION_VOLUME="$(realpath isolation-dist)" +export WORKSPACE_VOLUME="$(realpath ../..)" diff --git a/examples/api/.taurignore b/examples/api/.taurignore new file mode 100644 index 0000000..848a0f2 --- /dev/null +++ b/examples/api/.taurignore @@ -0,0 +1,3 @@ +src-tauri/locales/ +src-tauri/Cross.toml +src-tauri/.gitignore diff --git a/examples/api/README.md b/examples/api/README.md new file mode 100644 index 0000000..85e4257 --- /dev/null +++ b/examples/api/README.md @@ -0,0 +1,29 @@ +# API example + +This example demonstrates Tauri's API capabilities using the `@tauri-apps/api` package. It's used as the main validation app, serving as the testbed of our development process. +In the future, this app will be used on Tauri's integration tests. + +![App screenshot](./screenshot.png?raw=true) + +## Running the example + +- Compile Tauri + go to root of the Tauri repo and run: + +``` +pnpm i +pnpm build:debug +``` + +- Run the app in development mode (Run inside of this folder `examples/api/`) + +```bash +$ pnpm tauri dev +``` + +- Build an run the release app (Run inside of this folder `examples/api/`) + +```bash +$ pnpm tauri build +$ ../../target/release/api +``` diff --git a/examples/api/dist/.gitkeep b/examples/api/dist/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/examples/api/index.html b/examples/api/index.html new file mode 100644 index 0000000..98132df --- /dev/null +++ b/examples/api/index.html @@ -0,0 +1,14 @@ + + + + + + + API Example App + + + +
+ + + diff --git a/examples/api/isolation-dist/index.html b/examples/api/isolation-dist/index.html new file mode 100644 index 0000000..a97caa8 --- /dev/null +++ b/examples/api/isolation-dist/index.html @@ -0,0 +1,10 @@ + + + + + Isolation Secure Script + + + + + diff --git a/examples/api/isolation-dist/index.js b/examples/api/isolation-dist/index.js new file mode 100644 index 0000000..1324ffc --- /dev/null +++ b/examples/api/isolation-dist/index.js @@ -0,0 +1,7 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +window.__TAURI_ISOLATION_HOOK__ = (payload, options) => { + return payload +} diff --git a/examples/api/package.json b/examples/api/package.json new file mode 100644 index 0000000..0ba26f0 --- /dev/null +++ b/examples/api/package.json @@ -0,0 +1,26 @@ +{ + "name": "api", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "tauri": "node ../../packages/cli/tauri.js" + }, + "dependencies": { + "@tauri-apps/api": "../../packages/api/dist" + }, + "devDependencies": { + "@iconify-json/codicon": "^1.2.49", + "@iconify-json/ph": "^1.2.2", + "@sveltejs/vite-plugin-svelte": "^7.2.0", + "@types/sortablejs": "^1.15.9", + "@unocss/extractor-svelte": "^66.7.5", + "sortablejs": "^1.15.7", + "svelte": "^5.56.4", + "unocss": "^66.7.5", + "vite": "^8.1.4" + } +} diff --git a/examples/api/public/tauri_logo.png b/examples/api/public/tauri_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2c53b8c4bd721c7346831ba6be581d03a0a4d755 GIT binary patch literal 2810 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 literal 0 HcmV?d00001 diff --git a/examples/api/src-tauri/.gitignore b/examples/api/src-tauri/.gitignore new file mode 100644 index 0000000..7ba10a9 --- /dev/null +++ b/examples/api/src-tauri/.gitignore @@ -0,0 +1,6 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# cargo-mobile +/gen diff --git a/examples/api/src-tauri/.taurignore b/examples/api/src-tauri/.taurignore new file mode 100644 index 0000000..cbff529 --- /dev/null +++ b/examples/api/src-tauri/.taurignore @@ -0,0 +1 @@ +tauri-plugin-sample/ \ No newline at end of file diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml new file mode 100644 index 0000000..a6e8092 --- /dev/null +++ b/examples/api/src-tauri/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "api" +version = "0.1.0" +description = "An example Tauri Application showcasing the api" +edition = "2024" +rust-version = "1.90" +license = "Apache-2.0 OR MIT" + +[lib] +name = "api_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { workspace = true, default-features = true, features = [ + "codegen", + "isolation", +] } + +[dependencies] +serde_json = "1" +serde = { version = "1", features = ["derive"] } +log = "0.4.21" +tauri-plugin-sample = { path = "./tauri-plugin-sample/" } +tauri-plugin-log = "2" + +# WebDriver automation bridge, used by the `@tauri-apps/api` e2e suite (packages/api-e2e). +# Desktop-only and behind the off-by-default `automation` feature so it never ships in a +# regular build. +[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] +tauri-plugin-automation = { version = "0.2", optional = true } + +[dependencies.tauri] +workspace = true +default-features = true +features = [ + "protocol-asset", + "image-ico", + "image-png", + "isolation", + "macos-private-api", + "tray-icon", +] + +[dev-dependencies.tauri] +workspace = true +features = ["test"] + +[features] +prod = ["tauri/custom-protocol"] +# Enables the WebDriver automation bridge; set by the e2e suite's build (packages/api-e2e). +automation = ["dep:tauri-plugin-automation"] diff --git a/examples/api/src-tauri/Cross.toml b/examples/api/src-tauri/Cross.toml new file mode 100644 index 0000000..2b23798 --- /dev/null +++ b/examples/api/src-tauri/Cross.toml @@ -0,0 +1,17 @@ +[build.env] +# must set ICONS_VOLUME, DIST_VOLUME, ISOLATION_VOLUME and WORKSPACE_VOLUME environment variables +# ICONS_VOLUME: absolute path to the .icons folder +# DIST_VOLUME: absolute path to the dist folder +# ISOLATION_VOLUME: absolute path to the isolation dist folder +# WORKSPACE_VOLUME: absolute path to the workspace +# this can be done running `$ . .setup-cross.sh` in the examples/api folder +volumes = [ + "ICONS_VOLUME", + "DIST_VOLUME", + "ISOLATION_VOLUME", + "WORKSPACE_VOLUME", +] + +[target.aarch64-unknown-linux-gnu] +image = "aarch64-unknown-linux-gnu:latest" +#image = "ghcr.io/tauri-apps/tauri/aarch64-unknown-linux-gnu:latest" diff --git a/examples/api/src-tauri/Info.plist b/examples/api/src-tauri/Info.plist new file mode 100644 index 0000000..3c54d6f --- /dev/null +++ b/examples/api/src-tauri/Info.plist @@ -0,0 +1,17 @@ + + + + + NSCameraUsageDescription + Request camera access for WebRTC + NSMicrophoneUsageDescription + Request microphone access for WebRTC + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + + + diff --git a/examples/api/src-tauri/build.rs b/examples/api/src-tauri/build.rs new file mode 100644 index 0000000..441e6ea --- /dev/null +++ b/examples/api/src-tauri/build.rs @@ -0,0 +1,56 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri_build::WindowsAttributes; + +fn main() { + tauri_build::try_build( + tauri_build::Attributes::new() + .codegen(tauri_build::CodegenContext::new()) + .windows_attributes(WindowsAttributes::new_without_app_manifest()) + .plugin( + "app-menu", + tauri_build::InlinedPlugin::new().commands(&["toggle", "popup"]), + ) + .app_manifest(tauri_build::AppManifest::new().commands(&[ + "log_operation", + "perform_request", + "echo", + "spam", + ])), + ) + .expect("failed to run tauri-build"); + + #[cfg(windows)] + { + // workaround needed to prevent `STATUS_ENTRYPOINT_NOT_FOUND` error in tests + // see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864 + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_env = std::env::var("CARGO_CFG_TARGET_ENV"); + let is_tauri_workspace = std::env::var("__TAURI_WORKSPACE__").is_ok_and(|v| v == "true"); + if is_tauri_workspace && target_os == "windows" && Ok("msvc") == target_env.as_deref() { + embed_manifest_for_tests(); + } + } +} + +#[cfg(windows)] +fn embed_manifest_for_tests() { + static WINDOWS_MANIFEST_FILE: &str = "windows-app-manifest.xml"; + + let manifest = std::env::current_dir() + .unwrap() + .join("../../../crates/tauri-build/src") + .join(WINDOWS_MANIFEST_FILE); + + println!("cargo:rerun-if-changed={}", manifest.display()); + // Embed the Windows application manifest file. + println!("cargo:rustc-link-arg=/MANIFEST:EMBED"); + println!( + "cargo:rustc-link-arg=/MANIFESTINPUT:{}", + manifest.to_str().unwrap() + ); + // Turn linker warnings into errors. + println!("cargo:rustc-link-arg=/WX"); +} diff --git a/examples/api/src-tauri/capabilities/.gitignore b/examples/api/src-tauri/capabilities/.gitignore new file mode 100644 index 0000000..c75f615 --- /dev/null +++ b/examples/api/src-tauri/capabilities/.gitignore @@ -0,0 +1 @@ +schemas/ diff --git a/examples/api/src-tauri/capabilities/main.json b/examples/api/src-tauri/capabilities/main.json new file mode 100644 index 0000000..21f8d1f --- /dev/null +++ b/examples/api/src-tauri/capabilities/main.json @@ -0,0 +1,16 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "secondary-window", + "description": "capability for secondary window", + "windows": ["main-*"], + "permissions": [ + { + "identifier": "sample:allow-ping", + "deny": [ + { + "path": "tauri.app" + } + ] + } + ] +} diff --git a/examples/api/src-tauri/capabilities/run-app.json b/examples/api/src-tauri/capabilities/run-app.json new file mode 100644 index 0000000..891160a --- /dev/null +++ b/examples/api/src-tauri/capabilities/run-app.json @@ -0,0 +1,75 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "run-app", + "description": "permissions to run the app", + "windows": ["main", "main-*"], + "permissions": [ + "core:window:allow-is-enabled", + "core:window:allow-set-enabled", + { + "identifier": "allow-log-operation", + "allow": [ + { + "event": "tauri-click" + } + ] + }, + "allow-perform-request", + "allow-echo", + "allow-spam", + "app-menu:default", + "sample:allow-ping-scoped", + "sample:allow-js-values", + "sample:global-scope", + "core:default", + "core:app:allow-app-hide", + "core:app:allow-app-show", + "core:app:allow-set-app-theme", + "core:app:allow-set-dock-visibility", + "core:app:allow-default-window-icon", + "core:app:allow-exit", + "core:window:allow-set-theme", + "core:window:allow-center", + "core:window:allow-request-user-attention", + "core:window:allow-set-resizable", + "core:window:allow-set-maximizable", + "core:window:allow-set-minimizable", + "core:window:allow-set-closable", + "core:window:allow-set-title", + "core:window:allow-maximize", + "core:window:allow-unmaximize", + "core:window:allow-minimize", + "core:window:allow-unminimize", + "core:window:allow-show", + "core:window:allow-hide", + "core:window:allow-close", + "core:window:allow-set-decorations", + "core:window:allow-set-shadow", + "core:window:allow-set-effects", + "core:window:allow-set-always-on-top", + "core:window:allow-set-always-on-bottom", + "core:window:allow-set-content-protected", + "core:window:allow-set-size", + "core:window:allow-set-min-size", + "core:window:allow-set-max-size", + "core:window:allow-set-position", + "core:window:allow-set-fullscreen", + "core:window:allow-set-fullscreen-on-monitor", + "core:window:allow-set-simple-fullscreen", + "core:window:allow-set-focus", + "core:window:allow-set-focusable", + "core:window:allow-set-skip-taskbar", + "core:window:allow-set-cursor-grab", + "core:window:allow-set-cursor-visible", + "core:window:allow-set-cursor-icon", + "core:window:allow-set-cursor-position", + "core:window:allow-set-ignore-cursor-events", + "core:window:allow-start-dragging", + "core:window:allow-set-progress-bar", + "core:window:allow-set-icon", + "core:window:allow-toggle-maximize", + "core:webview:allow-create-webview-window", + "core:webview:allow-print", + "core:webview:allow-set-webview-zoom" + ] +} diff --git a/examples/api/src-tauri/locales/pt-BR.wxl b/examples/api/src-tauri/locales/pt-BR.wxl new file mode 100644 index 0000000..8f58c07 --- /dev/null +++ b/examples/api/src-tauri/locales/pt-BR.wxl @@ -0,0 +1,6 @@ + + Executar Tauri API + Uma versão mais recente de Tauri API está instalada. + Adiciona o caminho do executável de Tauri API para a variável de ambiente PATH. Isso permite Tauri API ser executado pela linha de comando. + Instala Tauri API. + diff --git a/examples/api/src-tauri/permissions/app-menu/default.toml b/examples/api/src-tauri/permissions/app-menu/default.toml new file mode 100644 index 0000000..b17fbd1 --- /dev/null +++ b/examples/api/src-tauri/permissions/app-menu/default.toml @@ -0,0 +1,3 @@ +[default] +description = "Default permissions for the plugin" +permissions = ["allow-toggle", "allow-popup"] diff --git a/examples/api/src-tauri/permissions/autogenerated/echo.toml b/examples/api/src-tauri/permissions/autogenerated/echo.toml new file mode 100644 index 0000000..d8c458e --- /dev/null +++ b/examples/api/src-tauri/permissions/autogenerated/echo.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-echo" +description = "Enables the echo command without any pre-configured scope." +commands.allow = ["echo"] + +[[permission]] +identifier = "deny-echo" +description = "Denies the echo command without any pre-configured scope." +commands.deny = ["echo"] diff --git a/examples/api/src-tauri/permissions/autogenerated/log_operation.toml b/examples/api/src-tauri/permissions/autogenerated/log_operation.toml new file mode 100644 index 0000000..a1e88b5 --- /dev/null +++ b/examples/api/src-tauri/permissions/autogenerated/log_operation.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-log-operation" +description = "Enables the log_operation command without any pre-configured scope." +commands.allow = ["log_operation"] + +[[permission]] +identifier = "deny-log-operation" +description = "Denies the log_operation command without any pre-configured scope." +commands.deny = ["log_operation"] diff --git a/examples/api/src-tauri/permissions/autogenerated/perform_request.toml b/examples/api/src-tauri/permissions/autogenerated/perform_request.toml new file mode 100644 index 0000000..0d12b9d --- /dev/null +++ b/examples/api/src-tauri/permissions/autogenerated/perform_request.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-perform-request" +description = "Enables the perform_request command without any pre-configured scope." +commands.allow = ["perform_request"] + +[[permission]] +identifier = "deny-perform-request" +description = "Denies the perform_request command without any pre-configured scope." +commands.deny = ["perform_request"] diff --git a/examples/api/src-tauri/permissions/autogenerated/spam.toml b/examples/api/src-tauri/permissions/autogenerated/spam.toml new file mode 100644 index 0000000..dfa659e --- /dev/null +++ b/examples/api/src-tauri/permissions/autogenerated/spam.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-spam" +description = "Enables the spam command without any pre-configured scope." +commands.allow = ["spam"] + +[[permission]] +identifier = "deny-spam" +description = "Denies the spam command without any pre-configured scope." +commands.deny = ["spam"] diff --git a/examples/api/src-tauri/src/cmd.rs b/examples/api/src-tauri/src/cmd.rs new file mode 100644 index 0000000..2db3c38 --- /dev/null +++ b/examples/api/src-tauri/src/cmd.rs @@ -0,0 +1,63 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Deserialize, Serialize}; +use tauri::{ + command, + ipc::{Channel, CommandScope}, +}; + +#[derive(Debug, Deserialize)] +#[allow(unused)] +pub struct RequestBody { + id: i32, + name: String, +} + +#[derive(Debug, Deserialize)] +pub struct LogScope { + event: String, +} + +#[command] +pub fn log_operation( + event: String, + payload: Option, + command_scope: CommandScope, +) -> Result<(), &'static str> { + if command_scope.denies().iter().any(|s| s.event == event) { + Err("denied") + } else if !command_scope.allows().iter().any(|s| s.event == event) { + Err("not allowed") + } else { + log::info!("{event} {payload:?}"); + Ok(()) + } +} + +#[derive(Serialize)] +pub struct ApiResponse { + message: String, +} + +#[command] +pub fn perform_request(endpoint: String, body: RequestBody) -> ApiResponse { + println!("{endpoint} {body:?}"); + ApiResponse { + message: "message response".into(), + } +} + +#[command] +pub fn echo(request: tauri::ipc::Request<'_>) -> tauri::ipc::Response { + tauri::ipc::Response::new(request.body().clone()) +} + +#[command] +pub fn spam(channel: Channel) -> tauri::Result<()> { + for i in 1..=1_000 { + channel.send(i)?; + } + Ok(()) +} diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs new file mode 100644 index 0000000..7ca75ef --- /dev/null +++ b/examples/api/src-tauri/src/lib.rs @@ -0,0 +1,231 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +mod cmd; +#[cfg(all(desktop, not(test)))] +mod menu_plugin; +#[cfg(all(desktop, not(test)))] +mod tray; + +use serde::Serialize; +use tauri::{ + App, Emitter, Listener, Runtime, WebviewUrl, + ipc::Channel, + webview::{PageLoadEvent, WebviewWindowBuilder}, +}; +#[allow(unused)] +use tauri::{Manager, RunEvent}; +use tauri_plugin_sample::{PingRequest, SampleExt}; + +#[derive(Clone, Serialize)] +struct Reply { + data: String, +} + +#[cfg(target_os = "macos")] +pub struct AppMenu(pub std::sync::Mutex>>); + +#[cfg(all(desktop, not(test)))] +pub struct PopupMenu(tauri::menu::Menu); + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + run_app(tauri::Builder::default(), |_app| {}) +} + +pub fn run_app) + Send + 'static>( + builder: tauri::Builder, + setup: F, +) { + // WebDriver automation bridge for the `@tauri-apps/api` e2e suite (packages/api-e2e). + // Registered as early as possible per the plugin's docs. Behind the off-by-default + // `automation` feature, and `not(test)` so it never interferes with the mock-runtime + // unit test below. + #[cfg(all(desktop, feature = "automation", not(test)))] + let builder = builder.plugin(tauri_plugin_automation::init()); + + let builder = builder + .plugin( + tauri_plugin_log::Builder::default() + .level(log::LevelFilter::Info) + .build(), + ) + .plugin(tauri_plugin_sample::init()) + .setup(move |app| { + #[cfg(all(desktop, not(test)))] + { + let handle = app.handle(); + tray::create_tray(handle)?; + handle.plugin(menu_plugin::init())?; + } + + #[cfg(target_os = "macos")] + app.manage(AppMenu::(Default::default())); + + #[cfg(all(desktop, not(test)))] + app.manage(PopupMenu( + tauri::menu::MenuBuilder::new(app) + .check("check", "Tauri is awesome!") + .text("text", "Do something") + .copy() + .build()?, + )); + + #[allow(unused_mut)] + let mut window_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) + .disable_drag_drop_handler() + .on_document_title_changed(|_window, title| { + println!("document title changed: {title}"); + }); + + #[cfg(all(desktop, not(test)))] + { + use std::sync::atomic::{AtomicU64, Ordering}; + + let app_ = app.handle().clone(); + let created_window_count = AtomicU64::new(0); + + window_builder = window_builder + .title("Tauri API Validation") + .inner_size(1000., 800.) + .min_inner_size(600., 400.) + .menu(tauri::menu::Menu::default(app.handle())?) + .on_new_window(move |url, features| { + println!("new window requested: {url:?} {features:?}"); + + let number = created_window_count.fetch_add(1, Ordering::Relaxed); + + let builder = WebviewWindowBuilder::new( + &app_, + format!("new-{number}"), + tauri::WebviewUrl::External("about:blank".parse().unwrap()), + ) + .window_features(features) + .on_document_title_changed(|window, title| { + window.set_title(&title).unwrap(); + }) + .title(url.as_str()); + + let window = builder.build().unwrap(); + tauri::webview::NewWindowResponse::Create { window } + }); + + // Liquid Glass effects need a transparent window to show through + #[cfg(target_os = "macos")] + { + window_builder = window_builder.transparent(true); + } + } + + let _webview = window_builder.build()?; + + let value = Some("test".to_string()); + let response = app.sample().ping(PingRequest { + value: value.clone(), + on_event: Channel::new(|event| { + println!("got channel event: {event:?}"); + Ok(()) + }), + }); + log::info!("got response: {response:?}"); + if let Ok(res) = response { + assert_eq!(res.value, value); + } + + setup(app); + + Ok(()) + }) + .on_page_load(|webview, payload| { + if payload.event() == PageLoadEvent::Finished { + let webview_ = webview.clone(); + webview.listen("js-event", move |event| { + println!("got js-event with message '{:?}'", event.payload()); + let reply = Reply { + data: "something else".to_string(), + }; + + webview_ + .emit("rust-event", Some(reply)) + .expect("failed to emit"); + }); + } + }); + + #[allow(unused_mut)] + let mut app = builder + .invoke_handler(tauri::generate_handler![ + cmd::log_operation, + cmd::perform_request, + cmd::echo, + cmd::spam, + ]) + .build(tauri::tauri_build_context!()) + .expect("error while building tauri application"); + + #[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| { + match event { + // 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 + #[cfg(all(desktop, not(test)))] + RunEvent::ExitRequested { api, code, .. } if code.is_none() => { + api.prevent_exit(); + } + #[cfg(desktop)] + RunEvent::WindowEvent { + event: tauri::WindowEvent::CloseRequested { api, .. }, + label, + .. + } => { + println!("closing window..."); + // run the window destroy manually just for fun :) + // usually you'd show a dialog here to ask for confirmation or whatever + api.prevent_close(); + _app_handle + .get_webview_window(&label) + .unwrap() + .destroy() + .unwrap(); + } + #[cfg(target_os = "ios")] + RunEvent::SceneRequested { .. } => { + counter += 1; + WebviewWindowBuilder::new( + _app_handle, + format!("main-from-scene-{counter}"), + WebviewUrl::default(), + ) + .build() + .unwrap(); + } + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))] + RunEvent::Opened { urls } => { + println!("opened urls: {:?}", urls); + } + _ => (), + } + }) +} + +#[cfg(test)] +mod tests { + use tauri::Manager; + + #[test] + fn run_app() { + super::run_app(tauri::test::mock_builder(), |app| { + let window = app.get_webview_window("main").unwrap(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(1)); + window.close().unwrap(); + }); + }) + } +} diff --git a/examples/api/src-tauri/src/main.rs b/examples/api/src-tauri/src/main.rs new file mode 100644 index 0000000..12e1627 --- /dev/null +++ b/examples/api/src-tauri/src/main.rs @@ -0,0 +1,10 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + api_lib::run(); +} diff --git a/examples/api/src-tauri/src/menu_plugin.rs b/examples/api/src-tauri/src/menu_plugin.rs new file mode 100644 index 0000000..26448ea --- /dev/null +++ b/examples/api/src-tauri/src/menu_plugin.rs @@ -0,0 +1,50 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri::{ + Runtime, command, + plugin::{Builder, TauriPlugin}, +}; + +#[cfg(not(target_os = "macos"))] +#[command] +pub fn toggle(window: tauri::Window) { + if window.is_menu_visible().unwrap_or_default() { + let _ = window.hide_menu(); + } else { + let _ = window.show_menu(); + } +} + +#[cfg(target_os = "macos")] +#[command] +pub fn toggle( + app: tauri::AppHandle, + app_menu: tauri::State<'_, crate::AppMenu>, +) { + if let Some(menu) = app.remove_menu().unwrap() { + app_menu.0.lock().unwrap().replace(menu); + } else { + app + .set_menu(app_menu.0.lock().unwrap().clone().expect("no app menu")) + .unwrap(); + } +} + +#[command] +pub fn popup( + window: tauri::Window, + popup_menu: tauri::State<'_, crate::PopupMenu>, +) { + window.popup_menu(&popup_menu.0).unwrap(); +} + +pub fn init() -> TauriPlugin { + Builder::new("app-menu") + .invoke_handler(tauri::generate_handler![ + #![plugin(app_menu)] + popup, toggle + ]) + .build() +} diff --git a/examples/api/src-tauri/src/tray.rs b/examples/api/src-tauri/src/tray.rs new file mode 100644 index 0000000..71655a0 --- /dev/null +++ b/examples/api/src-tauri/src/tray.rs @@ -0,0 +1,126 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::atomic::{AtomicBool, Ordering}; +use tauri::{ + Manager, Runtime, WebviewUrl, include_image, + menu::{Menu, MenuItem}, + tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, +}; + +pub fn create_tray(app: &tauri::AppHandle) -> tauri::Result<()> { + let toggle_i = MenuItem::with_id(app, "toggle", "Toggle", true, None::<&str>)?; + let new_window_i = MenuItem::with_id(app, "new-window", "New window", true, None::<&str>)?; + let icon_i_1 = MenuItem::with_id(app, "icon-1", "Icon 1", true, None::<&str>)?; + let icon_i_2 = MenuItem::with_id(app, "icon-2", "Icon 2", true, None::<&str>)?; + #[cfg(target_os = "macos")] + let set_title_i = MenuItem::with_id(app, "set-title", "Set Title", true, None::<&str>)?; + let switch_i = MenuItem::with_id(app, "switch-menu", "Switch Menu", true, None::<&str>)?; + let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + let remove_tray_i = + MenuItem::with_id(app, "remove-tray", "Remove Tray icon", true, None::<&str>)?; + let menu1 = Menu::with_items( + app, + &[ + &toggle_i, + &new_window_i, + &icon_i_1, + &icon_i_2, + #[cfg(target_os = "macos")] + &set_title_i, + &switch_i, + &quit_i, + &remove_tray_i, + ], + )?; + let menu2 = Menu::with_items( + app, + &[&toggle_i, &new_window_i, &switch_i, &quit_i, &remove_tray_i], + )?; + + let is_menu1 = AtomicBool::new(true); + + let _tray = TrayIconBuilder::with_id("tray-1") + .tooltip("Tauri") + .icon(app.default_window_icon().unwrap().clone()) + .menu(&menu1) + .show_menu_on_left_click(false) + .on_menu_event(move |app, event| match event.id.as_ref() { + "quit" => { + app.exit(0); + } + "remove-tray" => { + app.remove_tray_by_id("tray-1"); + } + "toggle" => { + if let Some(window) = app.get_webview_window("main") { + let new_title = if window.is_visible().unwrap_or_default() { + let _ = window.hide(); + "Show" + } else { + let _ = window.show(); + let _ = window.set_focus(); + "Hide" + }; + toggle_i.set_text(new_title).unwrap(); + } + } + "new-window" => { + let _webview = + tauri::WebviewWindowBuilder::new(app, "new", WebviewUrl::App("index.html".into())) + .title("Tauri") + .build() + .unwrap(); + } + #[cfg(target_os = "macos")] + "set-title" => { + if let Some(tray) = app.tray_by_id("tray-1") { + let _ = tray.set_title(Some("Tauri")); + } + } + i @ "icon-1" | i @ "icon-2" => { + if let Some(tray) = app.tray_by_id("tray-1") { + let icon = if i == "icon-1" { + include_image!("../../.icons/icon.ico") + } else { + include_image!("../../.icons/tray_icon_with_transparency.png") + }; + let _ = tray.set_icon(Some(icon)); + } + } + "switch-menu" => { + let flag = is_menu1.load(Ordering::Relaxed); + let (menu, tooltip) = if flag { + (menu2.clone(), "Menu 2") + } else { + (menu1.clone(), "Tauri") + }; + if let Some(tray) = app.tray_by_id("tray-1") { + let _ = tray.set_menu(Some(menu)); + let _ = tray.set_tooltip(Some(tooltip)); + } + is_menu1.store(!flag, Ordering::Relaxed); + } + + _ => {} + }) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } = event + { + let app = tray.app_handle(); + if let Some(window) = app.get_webview_window("main") { + let _ = window.unminimize(); + let _ = window.show(); + let _ = window.set_focus(); + } + } + }) + .build(app)?; + + Ok(()) +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/.gitignore b/examples/api/src-tauri/tauri-plugin-sample/.gitignore new file mode 100644 index 0000000..24ae128 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/.gitignore @@ -0,0 +1 @@ +.tauri diff --git a/examples/api/src-tauri/tauri-plugin-sample/Cargo.toml b/examples/api/src-tauri/tauri-plugin-sample/Cargo.toml new file mode 100644 index 0000000..0675403 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "tauri-plugin-sample" +version = "0.1.0" +edition = "2024" +links = "tauri-plugin-sample" + +[dependencies] +tauri = { workspace = true } +log = "0.4" +serde = "1" +serde_json = "1" +thiserror = "2" + +[target.'cfg(any(target_os = "ios", target_os = "android"))'.dependencies] +tauri = { workspace = true, features = ["wry"] } + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/.gitignore b/examples/api/src-tauri/tauri-plugin-sample/android/.gitignore new file mode 100644 index 0000000..ae485e8 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/.gitignore @@ -0,0 +1,2 @@ +/build +.tauri diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/build.gradle.kts b/examples/api/src-tauri/tauri-plugin-sample/android/build.gradle.kts new file mode 100644 index 0000000..697344a --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/build.gradle.kts @@ -0,0 +1,40 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.plugin.sample" + compileSdk = 36 + + defaultConfig { + minSdk = 21 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.6.0") + implementation("com.google.android.material:material:1.7.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation(project(":tauri-android")) +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/consumer-rules.pro b/examples/api/src-tauri/tauri-plugin-sample/android/consumer-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/consumer-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/settings.gradle b/examples/api/src-tauri/tauri-plugin-sample/android/settings.gradle new file mode 100644 index 0000000..d7782a4 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/settings.gradle @@ -0,0 +1,31 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + google() + } + resolutionStrategy { + eachPlugin { + switch (requested.id.id) { + case "com.android.library": + useVersion("8.0.2") + break + case "org.jetbrains.kotlin.android": + useVersion("1.8.20") + break + } + } + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + google() + + } +} + +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/src/androidTest/java/com/plugin/sample/ExampleInstrumentedTest.kt b/examples/api/src-tauri/tauri-plugin-sample/android/src/androidTest/java/com/plugin/sample/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..795867b --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/src/androidTest/java/com/plugin/sample/ExampleInstrumentedTest.kt @@ -0,0 +1,28 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.plugin.sample + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.plugin.sample", appContext.packageName) + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/src/main/AndroidManifest.xml b/examples/api/src-tauri/tauri-plugin-sample/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/src/main/java/com/plugin/sample/Example.kt b/examples/api/src-tauri/tauri-plugin-sample/android/src/main/java/com/plugin/sample/Example.kt new file mode 100644 index 0000000..63d3581 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/src/main/java/com/plugin/sample/Example.kt @@ -0,0 +1,14 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.plugin.sample + +import android.util.Log + +class Example { + fun pong(value: String): String { + Log.i("Pong", value) + return value + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/src/main/java/com/plugin/sample/ExamplePlugin.kt b/examples/api/src-tauri/tauri-plugin-sample/android/src/main/java/com/plugin/sample/ExamplePlugin.kt new file mode 100644 index 0000000..25bc1f7 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/src/main/java/com/plugin/sample/ExamplePlugin.kt @@ -0,0 +1,57 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.plugin.sample + +import android.app.Activity +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Channel +import app.tauri.plugin.JSArray +import app.tauri.plugin.JSObject +import app.tauri.plugin.Plugin +import app.tauri.plugin.Invoke + +@InvokeArg +class PingArgs { + var value: String? = null + var onEvent: Channel? = null +} + +class JsValues(val array: JSArray, val obj: JSObject) + +@TauriPlugin +class ExamplePlugin(private val activity: Activity): Plugin(activity) { + private val implementation = Example() + + @Command + fun ping(invoke: Invoke) { + val args = invoke.parseArgs(PingArgs::class.java) + + val event = JSObject() + event.put("kind", "ping") + args.onEvent?.send(event) + + val ret = JSObject() + ret.put("value", implementation.pong(args.value ?: "default value :(")) + invoke.resolve(ret) + } + + // Resolves org.json values through the plugin JSON mapper (`resolveObject`), + // both as a property of a plain object and nested in one another. + @Command + fun jsValues(invoke: Invoke) { + val array = JSArray() + array.put("a") + array.put(1) + array.put(true) + + val obj = JSObject() + obj.put("kind", "object") + obj.put("nested", JSArray(listOf(1, 2))) + + invoke.resolveObject(JsValues(array, obj)) + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/src/test/java/com/plugin/sample/ExampleUnitTest.kt b/examples/api/src-tauri/tauri-plugin-sample/android/src/test/java/com/plugin/sample/ExampleUnitTest.kt new file mode 100644 index 0000000..db6837e --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/src/test/java/com/plugin/sample/ExampleUnitTest.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.plugin.sample + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/api-iife.js b/examples/api/src-tauri/tauri-plugin-sample/api-iife.js new file mode 100644 index 0000000..cb6dfc7 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/api-iife.js @@ -0,0 +1,7 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +if ('__TAURI__' in window) { + window.__TAURI__.sample = {} +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/build.rs b/examples/api/src-tauri/tauri-plugin-sample/build.rs new file mode 100644 index 0000000..43ef403 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/build.rs @@ -0,0 +1,13 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const COMMANDS: &[&str] = &["ping", "js_values"]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS) + .android_path("android") + .ios_path("ios") + .global_api_script_path("./api-iife.js") + .build(); +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/ios/.gitignore b/examples/api/src-tauri/tauri-plugin-sample/ios/.gitignore new file mode 100644 index 0000000..5922fda --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/ios/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/examples/api/src-tauri/tauri-plugin-sample/ios/Package.swift b/examples/api/src-tauri/tauri-plugin-sample/ios/Package.swift new file mode 100644 index 0000000..537153a --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/ios/Package.swift @@ -0,0 +1,35 @@ +// swift-tools-version:5.3 +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "tauri-plugin-sample", + platforms: [ + .macOS(.v10_13), + .iOS(.v13), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-sample", + type: .static, + targets: ["tauri-plugin-sample"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(name: "Tauri", path: "../../../../../crates/tauri/mobile/ios-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-sample", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/examples/api/src-tauri/tauri-plugin-sample/ios/README.md b/examples/api/src-tauri/tauri-plugin-sample/ios/README.md new file mode 100644 index 0000000..5459044 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/ios/README.md @@ -0,0 +1,3 @@ +# Tauri Plugin sample + +A description of this package. diff --git a/examples/api/src-tauri/tauri-plugin-sample/ios/Sources/ExamplePlugin.swift b/examples/api/src-tauri/tauri-plugin-sample/ios/Sources/ExamplePlugin.swift new file mode 100644 index 0000000..0b97b64 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/ios/Sources/ExamplePlugin.swift @@ -0,0 +1,32 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import SwiftRs +import Tauri +import UIKit +import WebKit + +class PingArgs: Decodable { + let value: String? + let onEvent: Channel? +} + +class ExamplePlugin: Plugin { + @objc public func ping(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(PingArgs.self) + try args.onEvent?.send(["kind": "ping"]) + invoke.resolve(["value": args.value ?? ""]) + } + + @objc public func jsValues(_ invoke: Invoke) throws { + let array: JSArray = ["a", 1, true] + let obj: JSObject = ["kind": "object", "nested": [1, 2] as JSArray] + invoke.resolve(["array": array, "obj": obj]) + } +} + +@_cdecl("init_plugin_sample") +func initPlugin() -> Plugin { + return ExamplePlugin() +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/ios/Tests/PluginTests/PluginTests.swift b/examples/api/src-tauri/tauri-plugin-sample/ios/Tests/PluginTests/PluginTests.swift new file mode 100644 index 0000000..114c4be --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/ios/Tests/PluginTests/PluginTests.swift @@ -0,0 +1,12 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import XCTest +@testable import ExamplePlugin + +final class ExamplePluginTests: XCTestCase { + func testExample() throws { + let plugin = ExamplePlugin() + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/permissions/.gitignore b/examples/api/src-tauri/tauri-plugin-sample/permissions/.gitignore new file mode 100644 index 0000000..8629fe2 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/permissions/.gitignore @@ -0,0 +1 @@ +schemas/ \ No newline at end of file diff --git a/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/commands/js_values.toml b/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/commands/js_values.toml new file mode 100644 index 0000000..f7a78c2 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/commands/js_values.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-js-values" +description = "Enables the js_values command without any pre-configured scope." +commands.allow = ["js_values"] + +[[permission]] +identifier = "deny-js-values" +description = "Denies the js_values command without any pre-configured scope." +commands.deny = ["js_values"] diff --git a/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/commands/ping.toml b/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/commands/ping.toml new file mode 100644 index 0000000..1d13588 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/commands/ping.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-ping" +description = "Enables the ping command without any pre-configured scope." +commands.allow = ["ping"] + +[[permission]] +identifier = "deny-ping" +description = "Denies the ping command without any pre-configured scope." +commands.deny = ["ping"] diff --git a/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/reference.md b/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/reference.md new file mode 100644 index 0000000..072ce6d --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/permissions/autogenerated/reference.md @@ -0,0 +1,87 @@ +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`sample:allow-js-values` + + + +Enables the js_values command without any pre-configured scope. + +
+ +`sample:deny-js-values` + + + +Denies the js_values command without any pre-configured scope. + +
+ +`sample:allow-ping` + + + +Enables the ping command without any pre-configured scope. + +
+ +`sample:deny-ping` + + + +Denies the ping command without any pre-configured scope. + +
+ +`sample:global-scope` + + + +Sets a global scope. + +
+ +`sample:allow-ping-scoped` + + + +Enables the ping command with a test scope. + +
diff --git a/examples/api/src-tauri/tauri-plugin-sample/permissions/global-scope.toml b/examples/api/src-tauri/tauri-plugin-sample/permissions/global-scope.toml new file mode 100644 index 0000000..d80fb78 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/permissions/global-scope.toml @@ -0,0 +1,7 @@ +"$schema" = "schemas/schema.json" + +[[permission]] +identifier = "global-scope" +description = "Sets a global scope." +[[permission.scope.allow]] +path = "global" diff --git a/examples/api/src-tauri/tauri-plugin-sample/permissions/ping-scoped.toml b/examples/api/src-tauri/tauri-plugin-sample/permissions/ping-scoped.toml new file mode 100644 index 0000000..f6a0502 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/permissions/ping-scoped.toml @@ -0,0 +1,8 @@ +"$schema" = "schemas/schema.json" + +[[permission]] +identifier = "allow-ping-scoped" +description = "Enables the ping command with a test scope." +commands.allow = ["ping"] +[[permission.scope.allow]] +path = "x" diff --git a/examples/api/src-tauri/tauri-plugin-sample/src/desktop.rs b/examples/api/src-tauri/tauri-plugin-sample/src/desktop.rs new file mode 100644 index 0000000..3b91e55 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/src/desktop.rs @@ -0,0 +1,30 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::de::DeserializeOwned; +use tauri::{AppHandle, Runtime, plugin::PluginApi}; + +use crate::models::*; + +pub fn init( + app: &AppHandle, + _api: PluginApi, +) -> crate::Result> { + Ok(Sample(app.clone())) +} + +/// A helper class to access the sample APIs. +pub struct Sample(AppHandle); + +impl Sample { + pub fn ping(&self, payload: PingRequest) -> crate::Result { + payload.on_event.send(Event { + kind: "ping".to_string(), + value: payload.value.clone(), + })?; + Ok(PingResponse { + value: payload.value, + }) + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/src/error.rs b/examples/api/src-tauri/tauri-plugin-sample/src/error.rs new file mode 100644 index 0000000..c30dbab --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/src/error.rs @@ -0,0 +1,14 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[cfg(mobile)] + #[error(transparent)] + PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), + #[error(transparent)] + Tauri(#[from] tauri::Error), +} + +pub type Result = std::result::Result; diff --git a/examples/api/src-tauri/tauri-plugin-sample/src/lib.rs b/examples/api/src-tauri/tauri-plugin-sample/src/lib.rs new file mode 100644 index 0000000..3411913 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/src/lib.rs @@ -0,0 +1,101 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Deserialize; +use std::path::PathBuf; +use tauri::{ + Manager, Runtime, State, + plugin::{Builder, TauriPlugin}, +}; + +pub use models::*; + +#[cfg(desktop)] +mod desktop; +#[cfg(mobile)] +mod mobile; + +mod error; +mod models; + +#[cfg(desktop)] +use desktop::Sample; +#[cfg(mobile)] +use mobile::Sample; + +pub use error::*; + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the sample APIs. +pub trait SampleExt { + fn sample(&self) -> State<'_, Sample>; +} + +impl> crate::SampleExt for T { + fn sample(&self) -> State<'_, Sample> { + self.state::>() + } +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +struct PingScope { + path: PathBuf, +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +struct SampleScope { + path: PathBuf, +} + +#[tauri::command] +fn ping( + app: tauri::AppHandle, + value: Option, + scope: tauri::ipc::CommandScope, + global_scope: tauri::ipc::GlobalScope, +) -> std::result::Result { + println!("local scope {scope:?}"); + println!("global scope {global_scope:?}"); + app + .sample() + .ping(PingRequest { + value, + on_event: tauri::ipc::Channel::new(|_| Ok(())), + }) + .map_err(|e| e.to_string()) +} + +/// Resolves `JSArray` and `JSObject` values from the native (Kotlin or Swift) plugin. +#[tauri::command] +fn js_values( + app: tauri::AppHandle, +) -> std::result::Result { + #[cfg(mobile)] + return app.sample().js_values().map_err(|e| e.to_string()); + #[cfg(desktop)] + { + let _ = app; + Err("js_values is only implemented on mobile".into()) + } +} + +pub fn init() -> TauriPlugin { + Builder::new("sample") + .setup(|app, api| { + #[cfg(mobile)] + let sample = mobile::init(app, api)?; + #[cfg(desktop)] + let sample = desktop::init(app, api)?; + app.manage(sample); + + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ping, js_values]) + .on_navigation(|window, url| { + println!("navigation {} {url}", window.label()); + true + }) + .build() +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/src/mobile.rs b/examples/api/src-tauri/tauri-plugin-sample/src/mobile.rs new file mode 100644 index 0000000..125d6ac --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/src/mobile.rs @@ -0,0 +1,45 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::de::DeserializeOwned; +use tauri::{ + AppHandle, Runtime, + plugin::{PluginApi, PluginHandle}, +}; + +use crate::models::*; + +#[cfg(target_os = "android")] +const PLUGIN_IDENTIFIER: &str = "com.plugin.sample"; + +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_sample); + +// initializes the Kotlin or Swift plugin classes +pub fn init( + _app: &AppHandle, + api: PluginApi, +) -> crate::Result> { + #[cfg(target_os = "android")] + let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "ExamplePlugin")?; + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_sample)?; + Ok(Sample(handle)) +} + +/// A helper class to access the sample APIs. +pub struct Sample(PluginHandle); + +impl Sample { + pub fn ping(&self, payload: PingRequest) -> crate::Result { + self + .0 + .run_mobile_plugin("ping", payload) + .map_err(Into::into) + } + + pub fn js_values(&self) -> crate::Result { + self.0.run_mobile_plugin("jsValues", ()).map_err(Into::into) + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/src/models.rs b/examples/api/src-tauri/tauri-plugin-sample/src/models.rs new file mode 100644 index 0000000..a6b11d4 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/src/models.rs @@ -0,0 +1,24 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Deserialize, Serialize}; +use tauri::ipc::Channel; + +#[derive(Serialize)] +pub struct Event { + pub kind: String, + pub value: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PingRequest { + pub value: Option, + pub on_event: Channel, +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct PingResponse { + pub value: Option, +} diff --git a/examples/api/src-tauri/tauri.conf.json b/examples/api/src-tauri/tauri.conf.json new file mode 100644 index 0000000..5ed38e5 --- /dev/null +++ b/examples/api/src-tauri/tauri.conf.json @@ -0,0 +1,132 @@ +{ + "$schema": "../../../crates/tauri-schema-generator/schemas/config.schema.json", + "productName": "Tauri API", + "version": "1.0.0", + "identifier": "com.tauri.api", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:1420", + "beforeDevCommand": "pnpm dev", + "beforeBuildCommand": "pnpm build", + "removeUnusedCommands": true + }, + "app": { + "withGlobalTauri": true, + "macOSPrivateApi": true, + "security": { + "pattern": { + "use": "isolation", + "options": { + "dir": "../isolation-dist/" + } + }, + "csp": { + "default-src": "'self' customprotocol: asset:", + "connect-src": "ipc: http://ipc.localhost", + "font-src": ["https://fonts.gstatic.com"], + "img-src": "'self' asset: http://asset.localhost blob: data:", + "style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com" + }, + "freezePrototype": true, + "assetProtocol": { + "enable": true, + "scope": { + "allow": ["$APPDATA/db/**", "$RESOURCE/**"], + "deny": ["$APPDATA/db/*.stronghold"] + } + } + } + }, + "plugins": { + "cli": { + "description": "Tauri API example", + "args": [ + { + "short": "c", + "name": "config", + "takesValue": true, + "description": "Config path" + }, + { + "short": "t", + "name": "theme", + "takesValue": true, + "description": "App theme", + "possibleValues": ["light", "dark", "system"] + }, + { + "short": "v", + "name": "verbose", + "description": "Verbosity level" + } + ], + "subcommands": { + "update": { + "description": "Updates the app", + "args": [ + { + "short": "b", + "name": "background", + "description": "Update in background" + } + ] + } + } + } + }, + "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", + "../../.icons/128x128@2x.png", + "../../.icons/icon.icns", + "../../.icons/icon.ico", + "../../.icons/AppIcon.icon" + ], + "windows": { + "wix": { + "language": { + "en-US": {}, + "pt-BR": { + "localePath": "locales/pt-BR.wxl" + } + } + }, + "nsis": { + "compression": "none" + } + } + } +} diff --git a/examples/api/src/App.svelte b/examples/api/src/App.svelte new file mode 100644 index 0000000..4c67c9e --- /dev/null +++ b/examples/api/src/App.svelte @@ -0,0 +1,374 @@ + + + + + + + +
+ +
+
+

{selected.label}

+
+
+ +
+
+
+ +
+
+
+

Console

+
(e.key === 'Enter' ? clear() : {})} + onclick={clear} + > +
+
+
+
+ {#each $messages as messageHtml} + {@html messageHtml} + {/each} +
+
+
+
diff --git a/examples/api/src/app.css b/examples/api/src/app.css new file mode 100644 index 0000000..4b93dc6 --- /dev/null +++ b/examples/api/src/app.css @@ -0,0 +1,86 @@ +:root { + line-height: 1.5; +} + +:root.dark { + color-scheme: dark; +} +:root.light { + color-scheme: light; +} + +input, +select { + min-width: 0; +} + +*:not(h1, h2, h3, h4, h5, h6) { + margin: 0; + padding: 0; +} + +* { + box-sizing: border-box; + font-family: 'Rubik', sans-serif; +} + +::-webkit-scrollbar { + width: 0.25rem; + height: 3px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + border-radius: 0.25rem; +} + +code { + padding: 0.05rem 0.25rem; +} + +code.code-block { + padding: 0.5rem; +} + +#sidebar { + width: 18.75rem; +} + +/* let window effects (vibrancy / Liquid Glass) show through the whole page */ +:root.window-effects #sidebar, +:root.window-effects main { + background-color: transparent; +} + +@media screen and (width < 640px) { + #sidebar { + --translate-x: -18.75rem; + transform: translateX(var(--translate-x)); + } +} + +#sidebar, +main { + padding-top: calc(env(safe-area-inset-top) + 1rem); +} + +#sidebar { + padding-left: calc(env(safe-area-inset-left) + 0.5rem); +} + +main { + padding-right: env(safe-area-inset-right); +} + +#sidebarToggle { + margin-top: calc(env(safe-area-inset-top) + 0.5rem); + margin-left: calc(env(safe-area-inset-left) + 0.5rem); +} + +#sidebar, +#console { + padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem); +} diff --git a/examples/api/src/components/MenuBuilder.svelte b/examples/api/src/components/MenuBuilder.svelte new file mode 100644 index 0000000..e0a6143 --- /dev/null +++ b/examples/api/src/components/MenuBuilder.svelte @@ -0,0 +1,293 @@ + + + + +
+
+
+

Custom

+
+ + + +
+
+
+

Predefined

+
+ {#each predefinedOptions as predefinedOption} + + {/each} +
+
+
+
+
+ {#key items} + {#each items as item, i (item.id)} + { + item.menu?.setText(item.text ?? '') + } + : undefined} + onIconPathChange={item.iconPath !== undefined + ? () => { + ;(item.menu as IconMenuItem | undefined)?.setIcon( + item.iconPath! + ) + } + : undefined} + onCheckedChange={item.checked !== undefined + ? () => { + ;(item.menu as CheckMenuItem | undefined)?.setChecked( + item.checked! + ) + } + : undefined} + onRemove={async () => { + items.splice(i, 1) + await onItemRemoved?.(item).finally(() => { + item.menu?.close() + }) + }} + /> + {:else} +
+ Drag and drop the menu items above to start building +
+ {/each} + {/key} +
+
diff --git a/examples/api/src/components/MenuItemComponent.svelte b/examples/api/src/components/MenuItemComponent.svelte new file mode 100644 index 0000000..5b82871 --- /dev/null +++ b/examples/api/src/components/MenuItemComponent.svelte @@ -0,0 +1,79 @@ + + + + + diff --git a/examples/api/src/main.ts b/examples/api/src/main.ts new file mode 100644 index 0000000..14633e3 --- /dev/null +++ b/examples/api/src/main.ts @@ -0,0 +1,14 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import 'uno.css' +import './app.css' +import App from './App.svelte' +import { mount } from 'svelte' + +const app = mount(App, { + target: document.querySelector('#app')! +}) + +export default app diff --git a/examples/api/src/views/App.svelte b/examples/api/src/views/App.svelte new file mode 100644 index 0000000..13fe566 --- /dev/null +++ b/examples/api/src/views/App.svelte @@ -0,0 +1,43 @@ + + +
+ + + +
diff --git a/examples/api/src/views/Communication.svelte b/examples/api/src/views/Communication.svelte new file mode 100644 index 0000000..33aaf4a --- /dev/null +++ b/examples/api/src/views/Communication.svelte @@ -0,0 +1,70 @@ + + +
+ + + + + +
diff --git a/examples/api/src/views/Menu.svelte b/examples/api/src/views/Menu.svelte new file mode 100644 index 0000000..aa9cef7 --- /dev/null +++ b/examples/api/src/views/Menu.svelte @@ -0,0 +1,126 @@ + + +
+ { + if (item.menu) { + await submenu?.append(item.menu) + } + }} + onItemRemoved={async (item) => { + if (item.menu) { + await submenu?.remove(item.menu) + } + }} + onItemMoved={(item, toIndex) => { + if (submenu) { + reorderMenuItems(submenu, item, toIndex) + } + }} + /> +
+ + + + +
+
diff --git a/examples/api/src/views/Tray.svelte b/examples/api/src/views/Tray.svelte new file mode 100644 index 0000000..f522c2d --- /dev/null +++ b/examples/api/src/views/Tray.svelte @@ -0,0 +1,134 @@ + + +
+ { + if (item.menu) { + await menu?.append(item.menu) + } + }} + onItemRemoved={async (item) => { + if (item.menu) { + await menu?.remove(item.menu) + } + }} + onItemMoved={(item, toIndex) => { + if (menu) { + reorderMenuItems(menu, item, toIndex) + } + }} + /> + +
+
+ + + + + + + + + +
+ +
+ {#if tray} + + {:else} + + {/if} +
+
+
diff --git a/examples/api/src/views/WebRTC.svelte b/examples/api/src/views/WebRTC.svelte new file mode 100644 index 0000000..51064a1 --- /dev/null +++ b/examples/api/src/views/WebRTC.svelte @@ -0,0 +1,72 @@ + + +
+
Not available for Linux
+ +
diff --git a/examples/api/src/views/Welcome.svelte b/examples/api/src/views/Welcome.svelte new file mode 100644 index 0000000..b5afafb --- /dev/null +++ b/examples/api/src/views/Welcome.svelte @@ -0,0 +1,62 @@ + + +
+

+ This is a demo of Tauri's API capabilities using the @tauri-apps/api package. It's used as the main validation app, serving as the test bed of our + development process. In the future, this app will be used on Tauri's integration + tests. +

+
+    App name: {appName}
+    App version: {version}
+    Tauri version: {tauriVersion}
+    Bundle type: {bundleType}
+  
+ +
+ + +
+
diff --git a/examples/api/src/views/Window.svelte b/examples/api/src/views/Window.svelte new file mode 100644 index 0000000..ca1e941 --- /dev/null +++ b/examples/api/src/views/Window.svelte @@ -0,0 +1,996 @@ + + +
+
+ {#if Object.keys(webviewMap).length >= 1} +
+

Selected Window

+ +
+ {/if} +
+

Create New Window

+
{ + createWebviewWindow() + ev.preventDefault() + }} + > + + +
+
+
+ {#if selectedWebview} +
+
+

Change Window Icon

+
{ + changeIcon() + ev.preventDefault() + }} + > + + +
+
+
+

Set Window Title

+
{ + setTitle() + ev.preventDefault() + }} + > + + +
+
+
+
+ + + + + + + + +
{ + setFullscreenOnMonitor() + ev.preventDefault() + }} + > + + +
+
+
+ + + + + + + + + + + +
+
+
+ + +
+
+ +
+ Height + +
+
+
+ + +
+
+ + +
+
+
+
+
+ Inner Size +
+ Width: {innerSize.width} + Height: {innerSize.height} +
+
+
+ Outer Size +
+ Width: {outerSize.width} + Height: {outerSize.height} +
+
+
+ Inner Logical Size +
+ Width: {innerSize.toLogical(scaleFactor).width.toFixed(3)} + Height: {innerSize.toLogical(scaleFactor).height.toFixed(3)} +
+
+
+ Outer Logical Size +
+ Width: {outerSize.toLogical(scaleFactor).width.toFixed(3)} + Height: {outerSize.toLogical(scaleFactor).height.toFixed(3)} +
+
+
+ Inner Position +
+ x: {innerPosition.x} + y: {innerPosition.y} +
+
+
+ Outer Position +
+ x: {outerPosition.x} + y: {outerPosition.y} +
+
+
+ Inner Logical Position +
+ x: {innerPosition.toLogical(scaleFactor).x.toFixed(3)} + y: {innerPosition.toLogical(scaleFactor).y.toFixed(3)} +
+
+
+ Outer Logical Position +
+ x: {outerPosition.toLogical(scaleFactor).x.toFixed(3)} + y: {outerPosition.toLogical(scaleFactor).y.toFixed(3)} +
+
+
+ Current Monitor +
+ {monitor} +
+
+
+

Cursor

+
+ + + +
+
+ + + +
+
+ +
+
+ + + +
+
+ + {#if isWindows || isMacOS} +
+
+
+ Applied effects: {effects.length ? effects.join(', ') : 'None'} +
+ + +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+
+ {/if} + {/if} +
diff --git a/examples/api/src/vite-env.d.ts b/examples/api/src/vite-env.d.ts new file mode 100644 index 0000000..b8c0f0f --- /dev/null +++ b/examples/api/src/vite-env.d.ts @@ -0,0 +1,6 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/// +/// diff --git a/examples/api/svelte.config.js b/examples/api/svelte.config.js new file mode 100644 index 0000000..1a90ad4 --- /dev/null +++ b/examples/api/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess() +} diff --git a/examples/api/tsconfig.json b/examples/api/tsconfig.json new file mode 100644 index 0000000..1dd39c5 --- /dev/null +++ b/examples/api/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "moduleResolution": "bundler", + "target": "ESNext", + "module": "ESNext", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true + }, + "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.svelte"] +} diff --git a/examples/api/unocss.config.js b/examples/api/unocss.config.js new file mode 100644 index 0000000..c1665d2 --- /dev/null +++ b/examples/api/unocss.config.js @@ -0,0 +1,101 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { defineConfig, presetIcons, presetWind3, presetWebFonts } from 'unocss' +import extractorSvelte from '@unocss/extractor-svelte' + +export default defineConfig({ + theme: { + colors: { + primary: '#FFFFFF', + primaryLighter: '#e9ecef', + darkPrimary: '#1B1B1D', + darkPrimaryLighter: '#242526', + primaryText: '#1C1E21', + darkPrimaryText: '#E3E3E3', + secondaryText: '#858A91', + darkSecondaryText: '#C2C5CA', + accent: '#3578E5', + accentDark: '#306cce', + accentDarker: '#2d66c3', + accentDarkest: '#2554a0', + accentLight: '#538ce9', + accentLighter: '#72a1ed', + accentLightest: '#9abcf2', + accentText: '#FFFFFF', + darkAccent: '#67d6ed', + darkAccentDark: '#49cee9', + darkAccentDarker: '#39cae8', + darkAccentDarkest: '#19b5d5', + darkAccentLight: '#85def1', + darkAccentLighter: '#95e2f2', + darkAccentLightest: '#c2eff8', + darkAccentText: '#1C1E21', + code: '#d6d8da', + codeDark: '#282a2e', + hoverOverlay: 'rgba(0,0,0,.05)', + hoverOverlayDarker: 'rgba(0,0,0,.1)', + darkHoverOverlay: 'hsla(0,0%,100%,.05)', + darkHoverOverlayDarker: 'hsla(0,0%,100%,.1)' + } + }, + preflights: [ + { + getCSS: ({ theme }) => ` + ::-webkit-scrollbar-thumb { + background-color: ${theme.colors.accent}; + } + + .dark ::-webkit-scrollbar-thumb { + background-color: ${theme.colors.darkAccent}; + } + + code { + font-size: ${theme.fontSize.xs[0]}; + font-family: ${theme.fontFamily.mono}; + border-radius: ${theme.borderRadius['DEFAULT']}; + background-color: ${theme.colors.code}; + } + + .code-block { + font-family: ${theme.fontFamily.mono}; + font-size: ${theme.fontSize.sm[0]}; + } + + .dark code { + background-color: ${theme.colors.codeDark}; + } + ` + } + ], + shortcuts: { + btn: `select-none outline-none shadow-md p-2 rd-1 text-primaryText border-none font-400 dark:font-600 + bg-accent hover:bg-accentDarker active:bg-accentDarkest text-accentText + dark:bg-darkAccent dark:hover:bg-darkAccentDarker dark:active:bg-darkAccentDarkest dark:text-darkAccentText`, + nv: `decoration-none flex items-center relative p-2 rd-1 transition-all-125 ease + text-darkSecondaryText + hover:text-accent dark:hover:text-darkAccent + hover:bg-darkHoverOverlay hover:border-l-4`, + nv_selected: `nv bg-darkHoverOverlay text-accent dark:text-darkAccent border-l-4`, + note: `decoration-none flex-inline items-center relative p-2 rd-1 + border-l-4 border-accent dark:border-darkAccent + bg-accent/10 dark:bg-darkAccent/10`, + 'note-red': + 'note bg-red-700/10 dark:bg-red-700/10 after:bg-red-700 dark:after:bg-red-700', + input: + 'flex items-center outline-none border-none py-3 px-2 rd-1 shadow-md bg-primaryLighter dark:bg-darkPrimaryLighter text-primaryText dark:text-darkPrimaryText', + checkbox: 'accent-accent' + }, + presets: [ + presetWind3(), + presetIcons(), + presetWebFonts({ + fonts: { + sans: 'Rubik', + mono: ['Fira Code', 'Fira Mono:400,700'] + } + }) + ], + extractors: [extractorSvelte] +}) diff --git a/examples/api/vite.config.js b/examples/api/vite.config.js new file mode 100644 index 0000000..f519ad7 --- /dev/null +++ b/examples/api/vite.config.js @@ -0,0 +1,44 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { defineConfig } from 'vite' +import Unocss from 'unocss/vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +const host = process.env.TAURI_DEV_HOST + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [Unocss(), svelte()], + build: { + emptyOutDir: false, + rollupOptions: { + output: { + entryFileNames: `assets/[name].js`, + chunkFileNames: `assets/[name].js`, + assetFileNames: `assets/[name].[ext]` + } + } + }, + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // prevent Vite from obscuring rust errors + clearScreen: false, + // tauri expects a fixed port, fail if that port is not available + server: { + host: host ?? false, + port: 1420, + strictPort: true, + hmr: host + ? { + protocol: 'ws', + host: host, + port: 1430 + } + : undefined, + fs: { + allow: ['.', '../../packages/api/dist'] + } + } +}) diff --git a/examples/commands/README.md b/examples/commands/README.md new file mode 100644 index 0000000..671b450 --- /dev/null +++ b/examples/commands/README.md @@ -0,0 +1,5 @@ +# Commands Example + +A simple Tauri Application showcasing the command API. + +To execute run the following on the root directory of the repository: `cargo run --example commands`. diff --git a/examples/commands/commands.rs b/examples/commands/commands.rs new file mode 100644 index 0000000..b323780 --- /dev/null +++ b/examples/commands/commands.rs @@ -0,0 +1,32 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri::{State, command}; + +#[command] +pub fn cmd(_argument: String) {} + +#[command] +pub fn invoke(_argument: String) {} + +#[command] +pub fn message(_argument: String) {} + +#[command] +pub fn resolver(_argument: String) {} + +#[command] +pub fn simple_command(the_argument: String) { + println!("{the_argument}"); +} + +#[command] +pub fn stateful_command(the_argument: Option, state: State<'_, super::MyState>) { + println!("{:?} {:?}", the_argument, *state); +} + +#[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 new file mode 100644 index 0000000..ef95405 --- /dev/null +++ b/examples/commands/index.html @@ -0,0 +1,88 @@ + + + + + + Tauri + + + +

Tauri Commands

+
Response:
+
Without Args:
+
+ + + diff --git a/examples/commands/main.rs b/examples/commands/main.rs new file mode 100644 index 0000000..3a56a2a --- /dev/null +++ b/examples/commands/main.rs @@ -0,0 +1,276 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +// we move some basic commands to a separate module just to show it works +mod commands; +use commands::{cmd, invoke, message, renamed_command_in_mod, resolver}; + +use serde::Deserialize; +use tauri::{ + State, Window, command, + ipc::{Request, Response}, +}; + +#[derive(Debug)] +pub struct MyState { + #[allow(dead_code)] + value: u64, + #[allow(dead_code)] + label: String, +} + +#[derive(Debug, serde::Serialize)] +enum MyError { + FooError, +} + +// ------------------------ Commands using Window ------------------------ +#[command] +fn window_label(window: Window) { + println!("window label: {}", window.label()); +} + +// Async commands + +#[command] +async fn async_simple_command(the_argument: String) { + println!("{the_argument}"); +} + +#[command(rename_all = "snake_case")] +async fn async_simple_command_snake(the_argument: String) { + println!("{the_argument}"); +} + +#[command] +async fn async_stateful_command( + the_argument: Option, + state: State<'_, MyState>, +) -> Result<(), ()> { + println!("{:?} {:?}", the_argument, *state); + Ok(()) +} +// ------------------------ Raw future commands ------------------------ + +#[command(async)] +fn future_simple_command(the_argument: String) -> impl std::future::Future { + println!("{the_argument}"); + std::future::ready(()) +} + +#[command(async)] +fn future_simple_command_with_return( + the_argument: String, +) -> impl std::future::Future { + println!("{the_argument}"); + std::future::ready(the_argument) +} + +#[command(async)] +fn future_simple_command_with_result( + the_argument: String, +) -> impl std::future::Future> { + println!("{the_argument}"); + std::future::ready(Ok(the_argument)) +} + +#[command(async)] +fn force_async(the_argument: String) -> String { + the_argument +} + +#[command(async)] +fn force_async_with_result(the_argument: &str) -> Result<&str, MyError> { + (!the_argument.is_empty()) + .then_some(the_argument) + .ok_or(MyError::FooError) +} + +// ------------------------ Raw future commands - snake_case ------------------------ + +#[command(async, rename_all = "snake_case")] +fn future_simple_command_snake(the_argument: String) -> impl std::future::Future { + println!("{the_argument}"); + std::future::ready(()) +} + +#[command(async, rename_all = "snake_case")] +fn future_simple_command_with_return_snake( + the_argument: String, +) -> impl std::future::Future { + println!("{the_argument}"); + std::future::ready(the_argument) +} + +#[command(async, rename_all = "snake_case")] +fn future_simple_command_with_result_snake( + the_argument: String, +) -> impl std::future::Future> { + println!("{the_argument}"); + std::future::ready(Ok(the_argument)) +} + +#[command(async, rename_all = "snake_case")] +fn force_async_snake(the_argument: String) -> String { + the_argument +} + +#[command(rename_all = "snake_case", async)] +fn force_async_with_result_snake(the_argument: &str) -> Result<&str, MyError> { + (!the_argument.is_empty()) + .then_some(the_argument) + .ok_or(MyError::FooError) +} + +// ------------------------ Commands returning Result ------------------------ + +#[command] +fn simple_command_with_result(the_argument: String) -> Result { + println!("{the_argument}"); + (!the_argument.is_empty()) + .then_some(the_argument) + .ok_or(MyError::FooError) +} + +#[command] +fn stateful_command_with_result( + the_argument: Option, + state: State<'_, MyState>, +) -> Result { + println!("{:?} {:?}", the_argument, *state); + dbg!(the_argument.ok_or(MyError::FooError)) +} + +// ------------------------ Commands returning Result - snake_case ------------------------ + +#[command(rename_all = "snake_case")] +fn simple_command_with_result_snake(the_argument: String) -> Result { + println!("{the_argument}"); + (!the_argument.is_empty()) + .then_some(the_argument) + .ok_or(MyError::FooError) +} + +#[command(rename_all = "snake_case")] +fn stateful_command_with_result_snake( + the_argument: Option, + state: State<'_, MyState>, +) -> Result { + println!("{:?} {:?}", the_argument, *state); + dbg!(the_argument.ok_or(MyError::FooError)) +} + +// Async commands + +#[command] +async fn async_simple_command_with_result(the_argument: String) -> Result { + println!("{the_argument}"); + Ok(the_argument) +} + +#[command] +async fn async_stateful_command_with_result( + the_argument: Option, + state: State<'_, MyState>, +) -> Result { + println!("{:?} {:?}", the_argument, *state); + Ok(the_argument.unwrap_or_default()) +} + +// Non-Ident command function arguments + +#[command] +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, + age: u8, +} + +#[command] +fn command_arguments_struct(Person { name, age }: Person<'_>) { + println!("received person struct with name: {name} | age: {age}") +} + +#[derive(Deserialize)] +struct InlinePerson<'a>(&'a str, u8); + +#[command] +fn command_arguments_tuple_struct(InlinePerson(name, age): InlinePerson<'_>) { + println!("received person tuple with name: {name} | age: {age}") +} + +#[command] +fn borrow_cmd(the_argument: &str) -> &str { + the_argument +} + +#[command] +fn borrow_cmd_async(the_argument: &str) -> &str { + the_argument +} + +#[command] +fn raw_request(request: Request<'_>) -> Response { + println!("{request:?}"); + Response::new(include_bytes!("./README.md").to_vec()) +} + +fn main() { + tauri::Builder::default() + .manage(MyState { + value: 0, + label: "Tauri!".into(), + }) + .invoke_handler(tauri::generate_handler![ + borrow_cmd, + borrow_cmd_async, + raw_request, + window_label, + force_async, + force_async_with_result, + commands::simple_command, + commands::stateful_command, + cmd, + invoke, + message, + resolver, + async_simple_command, + 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, + future_simple_command_snake, + future_simple_command_with_return_snake, + future_simple_command_with_result_snake, + force_async_snake, + force_async_with_result_snake, + simple_command_with_result_snake, + stateful_command_with_result_snake, + stateful_command_with_result, + command_arguments_tuple_struct, + async_simple_command_with_result, + future_simple_command_with_return, + future_simple_command_with_result, + async_stateful_command_with_result, + ]) + .run(tauri::generate_context!( + "../../examples/commands/tauri.conf.json" + )) + .expect("error while running tauri application"); +} diff --git a/examples/commands/tauri.conf.json b/examples/commands/tauri.conf.json new file mode 100644 index 0000000..b2a351e --- /dev/null +++ b/examples/commands/tauri.conf.json @@ -0,0 +1,35 @@ +{ + "$schema": "../../crates/tauri-schema-generator/schemas/config.schema.json", + "productName": "Commands", + "version": "0.1.0", + "identifier": "com.tauri.dev", + "build": { + "frontendDist": ["index.html"] + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "Welcome to Tauri!", + "width": 800, + "height": 600, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost" + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "../.icons/32x32.png", + "../.icons/128x128.png", + "../.icons/128x128@2x.png", + "../.icons/icon.icns", + "../.icons/icon.ico" + ] + } +} diff --git a/examples/drag/README.md b/examples/drag/README.md new file mode 100644 index 0000000..593d575 --- /dev/null +++ b/examples/drag/README.md @@ -0,0 +1,3 @@ +# Drag Window Example + +To execute run the following on the root directory of the repository: `cargo run --example drag`. diff --git a/examples/drag/index.html b/examples/drag/index.html new file mode 100644 index 0000000..49208b1 --- /dev/null +++ b/examples/drag/index.html @@ -0,0 +1,418 @@ + + + + + + + + + +

Drag.js Paths & Configuration - QA Test Suite

+ +
+

Configuration Reference

+
+ Attribute: data-tauri-drag-region +
+
+ +
+

Drag Region Values

+
bare / no value → self (direct clicks only)
+
"deep" → deep (subtree clicks)
+
"false" → disabled (drag blocked)
+
+ +
+

QA Test Cases - Try Dragging Each Section

+ +
+
+ 1. No Attribute (Default - Should NOT Drag) +
+
+ No data-tauri-drag-region attribute +
+
+ +
+
+ 2. Bare Attribute (Self Only - Should Drag Direct Click) +
+
+ data-tauri-drag-region (bare) +
+
+ +
+
+ 3. Deep Attribute (Should Drag from Subtree) +
+
+ data-tauri-drag-region="deep" +
+ Nested content - should also drag from here +
+
+
+ +
+
4. False Attribute (Drag Disabled)
+
+ data-tauri-drag-region="false" (drag disabled) +
+
+ +
+
+ 5. Drag Region with Clickable Element (Button Blocks Drag) +
+
+ data-tauri-drag-region with button inside + +
+
+ +
+
+ 6. Drag Region with Input (Input Blocks Drag) +
+
+ data-tauri-drag-region with input inside + +
+
+ +
+
+ 7. Deep Drag Region with Clickable Elements +
+
+ data-tauri-drag-region="deep" with clickables +
+ + Link (Blocks) +
+
+
+ +
+
+ 8. Deep Drag Region with Non-Clickable Text +
+
+ data-tauri-drag-region="deep" with text +
+ Just plain text - should drag from here +
+
+
+ +
+
9. Nested: False Blocks Parent Drag
+
+ data-tauri-drag-region="deep" parent +
+ data-tauri-drag-region="false" child (blocks parent drag) +
+
+
+ +
+
+ 10. Double Click to Maximize (Test Platform Behavior) +
+
+ Double-click to test maximize (platform-dependent) +
+
+ macOS: Maximize on mouseup if cursor unmoved | Windows/Linux: Maximize + on double-click +
+
+ +
+
+ 11. Deep Drag Region with ARIA Roles (Role Elements Should Block Drag) +
+
+ data-tauri-drag-region="deep" with role-based elements +
+
role="button" (Blocks)
+
role="link" (Blocks)
+ +
Plain text between role boxes (Drags)
+
+
+
+ +
+
+ 12. Clickable Element with Bare Drag Region (Button Should Drag on + Direct Click) +
+
+ + + Link with data-tauri-drag-region (Should Drag) + +
+
+ +
+
+ 13. Clickable Element with Deep Drag Region (Should Drag from + Children) +
+ +
+ +
+
+ 14. Clickable Element with False Drag Region (Should NOT Drag) +
+ +
+ +
+
+ 15. ARIA Role Element with Drag Region (Should Drag) +
+
+
+ role="button" with data-tauri-drag-region (Should Drag) +
+ +
+
+ +
+
+ 16. Bare Drag Region with ARIA Role Child (Role Child Should Block + Drag) +
+
+ data-tauri-drag-region (bare) parent +
+ +
role="switch" (Blocks)
+
role="option" (Blocks)
+
+
+
+
+ +
+

Clickable Tags (Prevent Drag)

+

+    
+ +
+

Interactive Roles (Prevent Drag)

+

+    
+ +
+

Window Commands

+
+ Command: plugin:window|start_dragging +
+
+ Command: plugin:window|internal_toggle_maximize +
+
+ + + + diff --git a/examples/drag/main.rs b/examples/drag/main.rs new file mode 100644 index 0000000..3f913e5 --- /dev/null +++ b/examples/drag/main.rs @@ -0,0 +1,24 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + tauri::Builder::default() + .run(generate_context()) + .expect("error while running tauri application"); +} + +fn generate_context() -> tauri::Context { + let mut context = tauri::generate_context!("../../examples/drag/tauri.conf.json"); + for cmd in [ + "plugin:window|start_dragging", + "plugin:window|internal_toggle_maximize", + ] { + context + .runtime_authority_mut() + .__allow_command(cmd.to_string(), tauri_utils::acl::ExecutionContext::Local); + } + context +} diff --git a/examples/drag/tauri.conf.json b/examples/drag/tauri.conf.json new file mode 100644 index 0000000..ef82349 --- /dev/null +++ b/examples/drag/tauri.conf.json @@ -0,0 +1,35 @@ +{ + "$schema": "../../crates/tauri-schema-generator/schemas/config.schema.json", + "productName": "Hello World", + "version": "0.1.0", + "identifier": "com.tauri.helloworld", + "build": { + "frontendDist": ["index.html"] + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "Welcome to Tauri!", + "width": 800, + "height": 600, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost" + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "../.icons/32x32.png", + "../.icons/128x128.png", + "../.icons/128x128@2x.png", + "../.icons/icon.icns", + "../.icons/icon.ico" + ] + } +} diff --git a/examples/file-associations/README.md b/examples/file-associations/README.md new file mode 100644 index 0000000..0579e7e --- /dev/null +++ b/examples/file-associations/README.md @@ -0,0 +1,19 @@ +# File Associations Example + +This example demonstrates how to make associations between the application and certain file types. + +This feature is commonly used for functionality such as previewing or editing files. + +## Running the example + +1. Run the following inside `examples/file-associations/src-tauri` + + ``` + cargo build --features tauri/protocol-asset + ``` + +## Associations + +This example creates associations with PNG, JPG, JPEG and GIF files. + +Additionally, it defines two new extensions - `taurid` (derives from a raw data file) and `taurijson` (derives from JSON). They have special treatment on macOS (see `exportedType` in `src-tauri/tauri.conf.json`). diff --git a/examples/file-associations/index.html b/examples/file-associations/index.html new file mode 100644 index 0000000..d0846ac --- /dev/null +++ b/examples/file-associations/index.html @@ -0,0 +1,19 @@ + + + + + + + File Associations + + + +

File Associations

+

+
+    
+  
+
diff --git a/examples/file-associations/package.json b/examples/file-associations/package.json
new file mode 100644
index 0000000..a920e21
--- /dev/null
+++ b/examples/file-associations/package.json
@@ -0,0 +1,13 @@
+{
+  "name": "file-associations",
+  "version": "1.0.0",
+  "pkg": {
+    "assets": [
+      "src/**/*"
+    ]
+  },
+  "scripts": {
+    "tauri": "node ../../packages/cli/tauri.js"
+  },
+  "devDependencies": {}
+}
diff --git a/examples/file-associations/src-tauri/.license_template b/examples/file-associations/src-tauri/.license_template
new file mode 100644
index 0000000..9601f8a
--- /dev/null
+++ b/examples/file-associations/src-tauri/.license_template
@@ -0,0 +1,3 @@
+// Copyright {20\d{2}(-20\d{2})?} Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
\ No newline at end of file
diff --git a/examples/file-associations/src-tauri/Cargo.toml b/examples/file-associations/src-tauri/Cargo.toml
new file mode 100644
index 0000000..c1bb7e1
--- /dev/null
+++ b/examples/file-associations/src-tauri/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "tauri-file-associations-demo"
+version = "0.1.0"
+description = "A Tauri application that associate file types"
+edition = "2024"
+rust-version = "1.90"
+
+[build-dependencies]
+tauri-build = { workspace = true, default-features = true, features = [
+  "codegen",
+] }
+
+[dependencies]
+serde_json = "1"
+serde = { version = "1", features = ["derive"] }
+tauri = { workspace = true, default-features = true, features = [
+  "protocol-asset",
+] }
+url = "2"
diff --git a/examples/file-associations/src-tauri/Info.plist b/examples/file-associations/src-tauri/Info.plist
new file mode 100644
index 0000000..25b43ae
--- /dev/null
+++ b/examples/file-associations/src-tauri/Info.plist
@@ -0,0 +1,21 @@
+
+
+
+
+    
+        CFBundleURLTypes
+        
+            
+                CFBundleURLName
+                
+                com.tauri.dev-file-associations-demo
+                CFBundleURLSchemes
+                
+                    
+                    myapp
+                    myscheme
+                
+            
+        
+    
+
diff --git a/examples/file-associations/src-tauri/build.rs b/examples/file-associations/src-tauri/build.rs
new file mode 100644
index 0000000..e43a276
--- /dev/null
+++ b/examples/file-associations/src-tauri/build.rs
@@ -0,0 +1,7 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+fn main() {
+  tauri_build::build()
+}
diff --git a/examples/file-associations/src-tauri/src/main.rs b/examples/file-associations/src-tauri/src/main.rs
new file mode 100644
index 0000000..cdca9fd
--- /dev/null
+++ b/examples/file-associations/src-tauri/src/main.rs
@@ -0,0 +1,97 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+#![cfg_attr(
+  all(not(debug_assertions), target_os = "windows"),
+  windows_subsystem = "windows"
+)]
+
+use std::path::PathBuf;
+use tauri::{AppHandle, Manager};
+
+fn handle_file_associations(app: AppHandle, files: Vec) {
+  // -- Scope handling start --
+
+  // You can remove this block if you only want to know about the paths, but not actually "use" them in the frontend.
+
+  // This requires the `fs` tauri plugin and is required to make the plugin's frontend work:
+  // use tauri_plugin_fs::FsExt;
+  // let fs_scope = app.fs_scope();
+
+  // This is for the `asset:` protocol to work:
+  let asset_protocol_scope = app.asset_protocol_scope();
+
+  for file in &files {
+    // This requires the `fs` plugin:
+    // let _ = fs_scope.allow_file(file);
+
+    // This is for the `asset:` protocol:
+    let _ = asset_protocol_scope.allow_file(file);
+  }
+
+  // -- Scope handling end --
+
+  let files = files
+    .into_iter()
+    .map(|f| {
+      let file = f.to_string_lossy().replace('\\', "\\\\"); // escape backslash
+      format!("\"{file}\"",) // wrap in quotes for JS array
+    })
+    .collect::>()
+    .join(",");
+
+  tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
+    .initialization_script(format!("window.openedFiles = [{files}]"))
+    .build()
+    .unwrap();
+}
+
+fn main() {
+  tauri::Builder::default()
+    .setup(|#[allow(unused_variables)] app| {
+      #[cfg(any(windows, target_os = "linux"))]
+      {
+        let mut files = Vec::new();
+
+        // NOTICE: `args` may include URL protocol (`your-app-protocol://`)
+        // or arguments (`--`) if your app supports them.
+        // files may also be passed as `file://path/to/file`
+        for maybe_file in std::env::args().skip(1) {
+          // skip flags like -f or --flag
+          if maybe_file.starts_with('-') {
+            continue;
+          }
+
+          // handle `file://` path urls and skip other urls
+          if let Ok(url) = url::Url::parse(&maybe_file) {
+            if let Ok(path) = url.to_file_path() {
+              files.push(path);
+            }
+          } else {
+            files.push(PathBuf::from(maybe_file))
+          }
+        }
+
+        handle_file_associations(app.handle().clone(), files);
+      }
+
+      Ok(())
+    })
+    .build(tauri::generate_context!())
+    .expect("error while running tauri application")
+    .run(
+      #[allow(unused_variables)]
+      |app, event| {
+        #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
+        if let tauri::RunEvent::Opened { urls } = event {
+          let files = urls
+            .into_iter()
+            .filter_map(|url| url.to_file_path().ok())
+            .collect::>();
+
+          handle_file_associations(app.clone(), files);
+        }
+      },
+    );
+}
diff --git a/examples/file-associations/src-tauri/tauri.conf.json b/examples/file-associations/src-tauri/tauri.conf.json
new file mode 100644
index 0000000..3d9afcd
--- /dev/null
+++ b/examples/file-associations/src-tauri/tauri.conf.json
@@ -0,0 +1,57 @@
+{
+  "$schema": "../../../crates/tauri-cli/config.schema.json",
+  "identifier": "com.tauri.dev-file-associations-demo",
+  "build": {
+    "frontendDist": ["../index.html"]
+  },
+  "app": {
+    "security": {
+      "csp": "default-src 'self'",
+      "assetProtocol": {
+        "enable": true
+      }
+    }
+  },
+  "bundle": {
+    "active": true,
+    "targets": "all",
+    "icon": [
+      "../../.icons/32x32.png",
+      "../../.icons/128x128.png",
+      "../../.icons/128x128@2x.png",
+      "../../.icons/icon.icns",
+      "../../.icons/icon.ico"
+    ],
+    "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"]
+        }
+      }
+    ]
+  }
+}
diff --git a/examples/helloworld/README.md b/examples/helloworld/README.md
new file mode 100644
index 0000000..cb223fb
--- /dev/null
+++ b/examples/helloworld/README.md
@@ -0,0 +1,3 @@
+# Hello World Example
+
+To execute run the following on the root directory of the repository: `cargo run --example helloworld`.
diff --git a/examples/helloworld/index.html b/examples/helloworld/index.html
new file mode 100644
index 0000000..d2ea9d5
--- /dev/null
+++ b/examples/helloworld/index.html
@@ -0,0 +1,34 @@
+
+
+  
+    
+    
+    Welcome to Tauri!
+  
+  
+    

Welcome to Tauri!

+ +
+ + +
+ +

+ + + + diff --git a/examples/helloworld/main.rs b/examples/helloworld/main.rs new file mode 100644 index 0000000..ee82280 --- /dev/null +++ b/examples/helloworld/main.rs @@ -0,0 +1,19 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +#[tauri::command] +fn greet(name: &str) -> String { + format!("Hello {name}, You have been greeted from Rust!") +} + +fn main() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![greet]) + .run(tauri::generate_context!( + "../../examples/helloworld/tauri.conf.json" + )) + .expect("error while running tauri application"); +} diff --git a/examples/helloworld/tauri.conf.json b/examples/helloworld/tauri.conf.json new file mode 100644 index 0000000..5b6d195 --- /dev/null +++ b/examples/helloworld/tauri.conf.json @@ -0,0 +1,35 @@ +{ + "$schema": "../../crates/tauri-schema-generator/schemas/config.schema.json", + "productName": "Hello World", + "version": "0.1.0", + "identifier": "com.tauri.dev", + "build": { + "frontendDist": ["index.html"] + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "Welcome to Tauri!", + "width": 800, + "height": 600, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost" + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "../.icons/32x32.png", + "../.icons/128x128.png", + "../.icons/128x128@2x.png", + "../.icons/icon.icns", + "../.icons/icon.ico" + ] + } +} diff --git a/examples/isolation/README.md b/examples/isolation/README.md new file mode 100644 index 0000000..252679c --- /dev/null +++ b/examples/isolation/README.md @@ -0,0 +1,3 @@ +# Isolation Example + +To execute run the following on the root directory of the repository: `cargo run --example isolation --features isolation`. diff --git a/examples/isolation/dist/index.html b/examples/isolation/dist/index.html new file mode 100644 index 0000000..bb72789 --- /dev/null +++ b/examples/isolation/dist/index.html @@ -0,0 +1,34 @@ + + + + + Hello Tauri! + + + +
+

Hello, Tauri!

+
+ + +
+
+ + + + diff --git a/examples/isolation/dist/linked.js b/examples/isolation/dist/linked.js new file mode 100644 index 0000000..8740dc1 --- /dev/null +++ b/examples/isolation/dist/linked.js @@ -0,0 +1 @@ +console.log("linked", window.__TAURI_INTERNALS__.__TAURI_PATTERN__); diff --git a/examples/isolation/isolation-dist/index.html b/examples/isolation/isolation-dist/index.html new file mode 100644 index 0000000..a97caa8 --- /dev/null +++ b/examples/isolation/isolation-dist/index.html @@ -0,0 +1,10 @@ + + + + + Isolation Secure Script + + + + + diff --git a/examples/isolation/isolation-dist/index.js b/examples/isolation/isolation-dist/index.js new file mode 100644 index 0000000..6510a21 --- /dev/null +++ b/examples/isolation/isolation-dist/index.js @@ -0,0 +1,8 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +window.__TAURI_ISOLATION_HOOK__ = (payload, options) => { + console.log('hook', payload, options) + return payload +} diff --git a/examples/isolation/main.rs b/examples/isolation/main.rs new file mode 100644 index 0000000..9709381 --- /dev/null +++ b/examples/isolation/main.rs @@ -0,0 +1,19 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +#[tauri::command] +fn ping() { + println!("ping: {:?}", std::time::Instant::now()); +} + +fn main() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![ping]) + .run(tauri::generate_context!( + "../../examples/isolation/tauri.conf.json" + )) + .expect("error while running tauri application"); +} diff --git a/examples/isolation/tauri.conf.json b/examples/isolation/tauri.conf.json new file mode 100644 index 0000000..c10453a --- /dev/null +++ b/examples/isolation/tauri.conf.json @@ -0,0 +1,43 @@ +{ + "$schema": "../../crates/tauri-schema-generator/schemas/config.schema.json", + "productName": "Isolation", + "version": "0.1.0", + "identifier": "com.tauri.dev", + "build": { + "frontendDist": "dist", + "beforeDevCommand": "", + "beforeBuildCommand": "" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "Isolation", + "width": 800, + "height": 600, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'; connect-src ipc: http://ipc.localhost", + "pattern": { + "use": "isolation", + "options": { + "dir": "isolation-dist" + } + } + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "../.icons/32x32.png", + "../.icons/128x128.png", + "../.icons/128x128@2x.png", + "../.icons/icon.icns", + "../.icons/icon.ico" + ] + } +} diff --git a/examples/multiwebview/README.md b/examples/multiwebview/README.md new file mode 100644 index 0000000..a239fc5 --- /dev/null +++ b/examples/multiwebview/README.md @@ -0,0 +1,3 @@ +# Hello World Example + +To execute run the following on the root directory of the repository: `cargo run --example multiwebview --features unstable`. diff --git a/examples/multiwebview/index.html b/examples/multiwebview/index.html new file mode 100644 index 0000000..63bf891 --- /dev/null +++ b/examples/multiwebview/index.html @@ -0,0 +1,11 @@ + + + + + + Welcome to Tauri! + + +

Welcome to Tauri!

+ + diff --git a/examples/multiwebview/main.rs b/examples/multiwebview/main.rs new file mode 100644 index 0000000..ad0bf69 --- /dev/null +++ b/examples/multiwebview/main.rs @@ -0,0 +1,62 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use tauri::{LogicalPosition, LogicalSize, WebviewUrl}; + +fn main() { + tauri::Builder::default() + .setup(|app| { + let width = 800.; + let height = 600.; + + let window = tauri::window::WindowBuilder::new(app, "main") + .inner_size(width, height) + .build()?; + + let _webview1 = window.add_child( + tauri::webview::WebviewBuilder::new("main1", WebviewUrl::App(Default::default())) + .auto_resize(), + LogicalPosition::new(0., 0.), + LogicalSize::new(width / 2., height / 2.), + )?; + + let _webview2 = window.add_child( + tauri::webview::WebviewBuilder::new( + "main2", + WebviewUrl::External("https://github.com/tauri-apps/tauri".parse().unwrap()), + ) + .auto_resize(), + LogicalPosition::new(width / 2., 0.), + LogicalSize::new(width / 2., height / 2.), + )?; + + let _webview3 = window.add_child( + tauri::webview::WebviewBuilder::new( + "main3", + WebviewUrl::External("https://tauri.app".parse().unwrap()), + ) + .auto_resize(), + LogicalPosition::new(0., height / 2.), + LogicalSize::new(width / 2., height / 2.), + )?; + + let _webview4 = window.add_child( + tauri::webview::WebviewBuilder::new( + "main4", + WebviewUrl::External("https://twitter.com/TauriApps".parse().unwrap()), + ) + .auto_resize(), + LogicalPosition::new(width / 2., height / 2.), + LogicalSize::new(width / 2., height / 2.), + )?; + + Ok(()) + }) + .run(tauri::generate_context!( + "../../examples/multiwebview/tauri.conf.json" + )) + .expect("error while running tauri application"); +} diff --git a/examples/multiwebview/tauri.conf.json b/examples/multiwebview/tauri.conf.json new file mode 100644 index 0000000..c6a7907 --- /dev/null +++ b/examples/multiwebview/tauri.conf.json @@ -0,0 +1,25 @@ +{ + "productName": "MultiWebview", + "version": "0.1.0", + "identifier": "com.tauri.dev", + "$schema": "../../crates/tauri-schema-generator/schemas/config.schema.json", + "build": { + "frontendDist": ["index.html"] + }, + "app": { + "security": { + "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost" + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "../.icons/32x32.png", + "../.icons/128x128.png", + "../.icons/128x128@2x.png", + "../.icons/icon.icns", + "../.icons/icon.ico" + ] + } +} diff --git a/examples/multiwindow/README.md b/examples/multiwindow/README.md new file mode 100644 index 0000000..ee52a12 --- /dev/null +++ b/examples/multiwindow/README.md @@ -0,0 +1,5 @@ +# Multi-Window Example + +An example Tauri Multi-Window Application. + +To execute run the following on the root directory of the repository: `cargo run --example multiwindow`. diff --git a/examples/multiwindow/index.html b/examples/multiwindow/index.html new file mode 100644 index 0000000..3dc6497 --- /dev/null +++ b/examples/multiwindow/index.html @@ -0,0 +1,62 @@ + + +

Send a message to a window using its label:

+
+ + + +
+ +
+ +

Create new window

+
+ + + +
+ +
+ +

Messages received from other windows:

+

+
+  
+    
+  
+
diff --git a/examples/multiwindow/main.rs b/examples/multiwindow/main.rs
new file mode 100644
index 0000000..15fd959
--- /dev/null
+++ b/examples/multiwindow/main.rs
@@ -0,0 +1,35 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+use tauri::WebviewWindowBuilder;
+
+fn main() {
+  tauri::Builder::default()
+    .setup(|app| {
+      WebviewWindowBuilder::new(app, "Third", tauri::WebviewUrl::default())
+        .title("Tauri - Third")
+        .build()?;
+
+      Ok(())
+    })
+    .run(generate_context())
+    .expect("failed to run tauri application");
+}
+
+fn generate_context() -> tauri::Context {
+  let mut context = tauri::generate_context!("../../examples/multiwindow/tauri.conf.json");
+  for cmd in [
+    "plugin:event|listen",
+    "plugin:event|emit",
+    "plugin:event|emit_to",
+    "plugin:webview|create_webview_window",
+  ] {
+    context
+      .runtime_authority_mut()
+      .__allow_command(cmd.to_string(), tauri_utils::acl::ExecutionContext::Local);
+  }
+  context
+}
diff --git a/examples/multiwindow/tauri.conf.json b/examples/multiwindow/tauri.conf.json
new file mode 100644
index 0000000..37eef15
--- /dev/null
+++ b/examples/multiwindow/tauri.conf.json
@@ -0,0 +1,46 @@
+{
+  "$schema": "../../crates/tauri-schema-generator/schemas/config.schema.json",
+  "productName": "Multi Window",
+  "version": "0.1.0",
+  "identifier": "com.tauri.dev",
+  "build": {
+    "frontendDist": ["index.html"]
+  },
+  "app": {
+    "withGlobalTauri": true,
+    "windows": [
+      {
+        "label": "Main",
+        "title": "Tauri - Main",
+        "tabbingIdentifier": "Main",
+        "width": 800,
+        "height": 600
+      },
+      {
+        "label": "Secondary",
+        "title": "Tauri - Secondary",
+        "tabbingIdentifier": "Secondary",
+        "width": 600,
+        "height": 400
+      }
+    ],
+    "security": {
+      "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost"
+    }
+  },
+  "bundle": {
+    "active": true,
+    "targets": "all",
+    "icon": [
+      "../.icons/32x32.png",
+      "../.icons/128x128.png",
+      "../.icons/128x128@2x.png",
+      "../.icons/icon.icns",
+      "../.icons/icon.ico"
+    ],
+    "resources": [],
+    "externalBin": [],
+    "copyright": "",
+    "category": "DeveloperTool"
+  }
+}
diff --git a/examples/resources/README.md b/examples/resources/README.md
new file mode 100644
index 0000000..d3589ca
--- /dev/null
+++ b/examples/resources/README.md
@@ -0,0 +1,39 @@
+# Resource example
+
+This example demonstrates the Tauri bundle resources functionality. The example adds `src-tauri/assets/index.js` as a resource (defined on `tauri.conf.json > bundle > resources`) and executes it using `Node.js`, locating the JavaScript file using the `tauri::App::path_resolver` APIs.
+
+## Running the example
+
+- Compile Tauri
+  go to root of the Tauri repo and run:
+  Linux / Mac:
+
+```
+# choose to install node cli (1)
+bash .scripts/setup.sh
+```
+
+Windows:
+
+```
+./.scripts/setup.ps1
+```
+
+- Install dependencies (Run inside of this folder `examples/resources/`)
+
+```bash
+$ pnpm i
+```
+
+- Run the app in development mode (Run inside of this folder `examples/resources/`)
+
+```bash
+$ pnpm tauri dev
+```
+
+- Build an run the release app (Run inside of this folder `examples/resources/`)
+
+```bash
+$ pnpm tauri build
+$ ./src-tauri/target/release/app
+```
diff --git a/examples/resources/index.html b/examples/resources/index.html
new file mode 100644
index 0000000..3ba78dd
--- /dev/null
+++ b/examples/resources/index.html
@@ -0,0 +1,32 @@
+
+
+  
+    
+    
+  
+
+  
+    
+ Resource `assets/index.js` path: + +
+ Resource `assets/index.js` content: +

+
+    
+  
+
diff --git a/examples/resources/package.json b/examples/resources/package.json
new file mode 100644
index 0000000..085fd2c
--- /dev/null
+++ b/examples/resources/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "resources",
+  "version": "0.1.0",
+  "scripts": {
+    "tauri": "node ../../packages/cli/tauri.js"
+  }
+}
diff --git a/examples/resources/src-tauri/Cargo.toml b/examples/resources/src-tauri/Cargo.toml
new file mode 100644
index 0000000..b0ff92d
--- /dev/null
+++ b/examples/resources/src-tauri/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "resources"
+version = "0.1.0"
+description = "A Tauri application that uses Node.js with app resources"
+edition = "2024"
+rust-version = "1.90"
+
+[build-dependencies]
+tauri-build = { workspace = true, default-features = true, features = [
+  "codegen",
+] }
+
+[dependencies]
+serde_json = "1"
+serde = { version = "1", features = ["derive"] }
+tauri = { workspace = true, default-features = true }
diff --git a/examples/resources/src-tauri/assets/index.js b/examples/resources/src-tauri/assets/index.js
new file mode 100644
index 0000000..effa98a
--- /dev/null
+++ b/examples/resources/src-tauri/assets/index.js
@@ -0,0 +1,5 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+console.log('hello world')
diff --git a/examples/resources/src-tauri/build.rs b/examples/resources/src-tauri/build.rs
new file mode 100644
index 0000000..e43a276
--- /dev/null
+++ b/examples/resources/src-tauri/build.rs
@@ -0,0 +1,7 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+fn main() {
+  tauri_build::build()
+}
diff --git a/examples/resources/src-tauri/capabilities/app.json b/examples/resources/src-tauri/capabilities/app.json
new file mode 100644
index 0000000..c50f095
--- /dev/null
+++ b/examples/resources/src-tauri/capabilities/app.json
@@ -0,0 +1,6 @@
+{
+  "$schema": "../gen/schemas/desktop-schema.json",
+  "identifier": "app",
+  "permissions": ["core:path:default"],
+  "windows": ["main"]
+}
diff --git a/examples/resources/src-tauri/src/main.rs b/examples/resources/src-tauri/src/main.rs
new file mode 100644
index 0000000..3c3ddae
--- /dev/null
+++ b/examples/resources/src-tauri/src/main.rs
@@ -0,0 +1,32 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+use tauri::Manager;
+
+#[tauri::command]
+fn read_to_string(path: &str) -> String {
+  std::fs::read_to_string(path).unwrap_or_default()
+}
+
+fn main() {
+  tauri::Builder::default()
+    .setup(move |app| {
+      let path = app
+        .path()
+        .resolve("assets/index.js", tauri::path::BaseDirectory::Resource)
+        .unwrap();
+
+      let content = std::fs::read_to_string(&path).unwrap();
+
+      println!("Resource `assets/index.js` path: {}", path.display());
+      println!("Resource `assets/index.js` content:\n{content}\n");
+
+      Ok(())
+    })
+    .invoke_handler(tauri::generate_handler![read_to_string])
+    .run(tauri::generate_context!())
+    .expect("error while running tauri application");
+}
diff --git a/examples/resources/src-tauri/tauri.conf.json b/examples/resources/src-tauri/tauri.conf.json
new file mode 100644
index 0000000..89658d4
--- /dev/null
+++ b/examples/resources/src-tauri/tauri.conf.json
@@ -0,0 +1,37 @@
+{
+  "$schema": "../../../crates/tauri-schema-generator/schemas/config.schema.json",
+  "productName": "Resources",
+  "version": "0.1.0",
+  "identifier": "com.tauri.resources",
+  "build": {
+    "frontendDist": ["../index.html"]
+  },
+  "app": {
+    "withGlobalTauri": true,
+
+    "windows": [
+      {
+        "title": "Welcome to Tauri!",
+        "width": 800,
+        "height": 600,
+        "resizable": true,
+        "fullscreen": false
+      }
+    ],
+    "security": {
+      "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost"
+    }
+  },
+  "bundle": {
+    "active": true,
+    "targets": "all",
+    "icon": [
+      "../../.icons/32x32.png",
+      "../../.icons/128x128.png",
+      "../../.icons/128x128@2x.png",
+      "../../.icons/icon.icns",
+      "../../.icons/icon.ico"
+    ],
+    "resources": ["assets/*"]
+  }
+}
diff --git a/examples/run-return/README.md b/examples/run-return/README.md
new file mode 100644
index 0000000..9195770
--- /dev/null
+++ b/examples/run-return/README.md
@@ -0,0 +1,3 @@
+# Run Return Example
+
+To execute run the following on the root directory of the repository: `cargo run --example run-return`.
diff --git a/examples/run-return/index.html b/examples/run-return/index.html
new file mode 100644
index 0000000..63bf891
--- /dev/null
+++ b/examples/run-return/index.html
@@ -0,0 +1,11 @@
+
+
+  
+    
+    
+    Welcome to Tauri!
+  
+  
+    

Welcome to Tauri!

+ + diff --git a/examples/run-return/main.rs b/examples/run-return/main.rs new file mode 100644 index 0000000..93f5dcd --- /dev/null +++ b/examples/run-return/main.rs @@ -0,0 +1,21 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + let app = tauri::Builder::default() + .build(tauri::generate_context!( + "../../examples/run-return/tauri.conf.json" + )) + .expect("error while building tauri application"); + + let exit_code = app.run_return(|_app, _event| { + //println!("{:?}", _event); + }); + + println!("I run after exit"); + + std::process::exit(exit_code); +} diff --git a/examples/run-return/tauri.conf.json b/examples/run-return/tauri.conf.json new file mode 100644 index 0000000..fb3eaf9 --- /dev/null +++ b/examples/run-return/tauri.conf.json @@ -0,0 +1,34 @@ +{ + "$schema": "../../crates/tauri-schema-generator/schemas/config.schema.json", + "productName": "RunReturn", + "version": "0.1.0", + "identifier": "com.tauri.dev", + "build": { + "frontendDist": ["index.html"] + }, + "app": { + "windows": [ + { + "title": "Welcome to Tauri!", + "width": 800, + "height": 600, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost" + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "../.icons/32x32.png", + "../.icons/128x128.png", + "../.icons/128x128@2x.png", + "../.icons/icon.icns", + "../.icons/icon.ico" + ] + } +} diff --git a/examples/splashscreen/README.md b/examples/splashscreen/README.md new file mode 100644 index 0000000..526f261 --- /dev/null +++ b/examples/splashscreen/README.md @@ -0,0 +1,11 @@ +# Splashscreen example + +This example demonstrates how a splashscreen can be implemented when waiting on an initialization code on Rust or on the UI. + +## Running the example + +Run the following scripts on the root directory of the repository: + +```bash +$ cargo run --example splashscreen +``` diff --git a/examples/splashscreen/dist/icon.png b/examples/splashscreen/dist/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3444fd1b2c2e7f4cdce9939a27af04d0169a956e GIT binary patch literal 14165 zcmd73hgTEb7cM*rp@X!SCQ9!}6DdjwO?oe)6oUm6qzNJ*B>@p>N|h!8f^-EHqzVZt zB1J&~=_De(h7M`-jla9Tzv14j#R`*i=A3BRbLkyCg;(?v z<}&O$eIl=DJrF^kedLw?Pqg@@MCJ#V`3);}xO2}sKT0O3f~=eJnl+mB1FCR<7^UiSto-9lhtuk#HeQ`{Z7z~h3#HAHE zY5Iv1^NfMNCUC$?KTH{`#|1}{vf32l+bF|rzzstV4J?(F8Bk%+AddMH z&^PJ8WT4FQ;`f4>8IlbqGaUe=uE%nGO*SydPDB8*%+WDcPxWr<0cDWUCQjMp;-RIU z`0~vB_SgJh-$J*FD{n*DqZRM5RyLisQlf|vgH~Zg7)eVuZ&kz?KnHYlBWZo5CowQ+ z=;*l-4qFu=Os6EgL}r(o8}MjLsY^t-Y6@RF=vVIRfw8oM0ZR7MYbfQ-eY(!OqZqeF zM3AsNfTQ;#q6TcK2=r0EGu{1-tp#X!^mTp%RKV@Q_aqptY>W=MN zoM#3g8je7FUY3N3mC<8LUR)*`5g8f0g@;Zs@iPN)UY%g@GXh6yvZSa}Bfu#cxr36c zBf(_pH!wr#fT-3UMd4H42yOfAQO+XTI%O-81`6Xx!odz-4;3$HOfi_g9TSZw^fKZ& zhbrl_fG+A~JvKxCtiO-WQT?W=)poO_0{s@8N;yt@a%OTKN6ka+CW$gLK@WbM{q!!L z7v81S;)x%yCCXb1Vch<*?0oLadRP#XA;VyD^4FV_Z#V9qF@qcG!!e?7=PU0i5ds%v z$bvI9u5Y(oXyj69)I4)6*~HLJT!Vr16amai9Ob@>{Dgn+McJ@B>{`OQe363hJh5gt zY53vxxtV6VCd5^77-lTlh3Y9tPG)- zKZ<{&GZg;Uf1UBDp%kAI6KYHP$v{mhG-J3F--i_85s(=SV^k$$LNS~>*7DwC`13dm2^dCm;Ly^x`&%-BDHaPiL;UFe z4sbw*u(DBKc>iXmj2emuu^7cZGlgUN8bn?{Dc$4H91WaTlBxAwRjSC0JSYqGz}b0r zX8Nx5w)-UtBUy%f=&S_B3XOp8Zx`~EeKErH5|16Wq&OQQ< zTqJDuQS)1C`rkZ8ppg@(W@}BhT#h?m`*nvY#&+kO06X)GE1>?|dtY+h#0CnRyYjY4do1I_X;$ zI!3#I=hYk+d~RIKcjuWs3>sT5FY>LFKCVr8L zv^n)8=Gr)gSU#XeqU{$_3OzoAoo?G=n3t4J(tF9P5)BQ*eDK-xofg&f(`(g*w5;TM z*x^8^M`=3#UW9DkNp8R?ZsKaN3vHFV(}?Wc{K*7aDAU#o1khXYDgss>hcAm|^@zVH zbZLY%*7*%la^x53q`PeKzxOgceX9V^8gbX7&%0FuX6HV~o?$9DDG%h;xpIHz!7AQ= zdBg5Fs3ek6{9cltr)baxGeAZC*huc{Ym0H$?%rY#n!x2SGo>R*PmRon`U~lR=YzKT zSC9JhtmBX-ycfAW2V6T5Y&;j?m}-?NRNJh#hoRR)tlO~#(NwV14DK5%!X%$4rf=m= zba|0F@xBXe0;JTYQZ&p~+v1biV|gyp16gAmhoe$fimJgb7*(cD?xAQUK^{XmCaKRB z{BwdCjJc;y%g+0)FxSD%Yf z-l-e=&%J4{>)kqh<6k1f#flm2CNe!C6Bw`3eHqW&D5tBYl_4KTl!T=n-KHr%D`=X3 zb1ZWw`cFetxYAD#=7m3sS6R;d8|_of(d$b@ulmmvHv_$>q12ZqBCroUL$Pw~!f^aP^MdwpWqb8;G*-s?$+}XTR2_ zXg(s~b!VnZ>fL^+Dj$^quOih|GMw+atVWG>aDO{vV_pT6+*>3o7hByV;rBOZN_b!* zElwBRf%_my5#mR|XD#DW5GG{(8;+H~zg8RndZ^56b_{QK0*bDSt_iaafRf z68sR$N^*-(MrLhkEzlXQ=CwDm1c`RmR=X+v{x3C^1+Z}bC08c2$20S)kt4guPkEA1 zy*8(v0JAip2g(H_j_S3)H20c|2xM6aTZ(U*VDNOYNz?DpHx-r=%>cV1V+PdN1Ut#? z;Ngl}oSw?n%{v3qs~rBJ8yQDdqyB&)K&m1CTZuU<OIX(fuLJ%Abt8E#V|I<7L-l>en>b?sB$(6K#10212H zG$aefWy1#)tBfuw*zvm1=>p@v!uPMSY%K?L6gnDXqF_ufEV^yy`x>d73xIjvtYho( zwZYlW;+Y5Z+Rohn!#(<6+IBB^8%25D z{MjT%z3Q(cB?$nW?N(934EMUw_9`k6_Dw8&1AlcD#e8bT0lt9Wse z^ua4{Z~uwk)DOd6Q$|AZC(6mE?)4X0pvFZiHMBk;{x0#+Np)VW?Oy^mSs6a5>jUL| z9F|Rh+S!^(6 zyWwh{HT~@_^wAP9eWz7z#(jmIx@+9jPcb8{!XMW0moIbgmAkFl{@zI2H0Q1MGw+$M zR9DfNVhHy{i;s1i4#3#r-2kRZd)(l;D>jmM1boJx^hZ7++d8N2RD@%993o4bO#N0J zoK2>Yf0HL@J1>>MMkN7v>8V+b$9Im`FnZ0Z(#b49+?)kTt$u2=Z$i^|AesGOzn?~H z@hH{D%7x@E`h{j1@*?NhXGO$cuvW;WamgV`4>0vE*N$qB)Qua6AS zmtQnb#f9W=VcmIe6q%_E(2=+H7^x(gzR%RV{W;SrF9Lh-s8M|nZyDIGLs9wr z#=81q%ygv2Zd-`wL#)Dr#!X`rtG2^Y^jrJQQwNP7g)b461sc4wFhp9%SJC2EF5A0n zWjCtedr2&Se6_%0gOpjvt$xysW8%}OMP#*{)@KG#J;rmiM-l>6u5_v-PQh|jrYy0EXspaQ)3}T` z6k3&tQ#>H)ZXh~1KQfB|&XQ|mxaiS6Gbe&HPY0>)zjwYg80JzU!wpzX_u`6k$!QNL zS)~bouvxxIYsX_!CP{i?m)$?25_%eacDI7s?*vD83HY-F zYaG2}ViOS3KG4XT^->TrtsY>nwmhupd2);7uz zf>xF>Bvk6hJ@n*j>bbp*cV)fAkQ11jbt20w~L%YhM&>%BtOYKR$Ew5TDg=ZqY zs*O7w`OF8j0}sSuxJNsW^qR&3(YZQ1HDhR)bC$(`EXWb3fiu^;O8<&}h=&>Yv9P(n z#2T%G+%JB$HA`_6e-=n9le~~;+{t-r`)*7Eq6FH-g?I1OV}G$mC%lComM=PVIPXS- zyB<5QoG3={AvzXN#Yxbjsa2W4o<2Qr-eqU&c2A0b z%x4u}aV%WmlKL$}?+KRhR6c!-8IW#!{P^+?()JC16(j<&CojsF@n-^o&8UqeYHMuPbSrXHZ5sv(9wMtr<$*9#h%M8Hy zHtq4l?vHp8`zzy#UkiaCl87)ejERp~|0b=a0qdJ-GenIkG~g8C>%YmV;th!ZI(?v) zCTUnB$O`~ku`K$Mi}w$H?>_7NncN03U$^XFiTa%rwgb8_phO?e*>e0M_jwb3P#xQ1 zPX&S)nDtAhPlNfI8vCwVjvo1IrPS!7o>yky@k~tcOmpmAz;|0=M_dEL%u$@&ut0MCvkcA$Pi=3lJys}$TtN6Y-4#osNP5O+ z(q_Q4IUq9$%d_r^tkL@+U=Lf4Mc~wSk*Cf~q8bxX`$Ct8M{br=d+S|>%gd{foa;$V z=0YX5`B5@jNrD(a6|tN#7|v3gH+my0u+VU1!fdDQcz z6m)W0bE=bLSGgIW^M{XmZ}wi1>i3xH3XAvqn_1v-4h|h_B|J5M+nS>D(>kHAf^hjA z;u{vPe0taRf9I&~R_w+T&@U_4eb>n7{K}r0hwbd1P5hecX}{^0%tKrE-w-5GVX3d; zELG_Uu~BoVfn>rt{v#o>=3*chg^^GLv|z6A7cP2%#fncmG61LPe}3uO9LNR-nc75B z5p5wsB&4l)|6H-t5RW2;rT;O_YAHjJ%DUqpQ!U+QF{BJ@0G34MF66eum#0iQw}8Dz zEc%Z^vtMEh-!15<-l-2b<1TfSD^U->x%jfAKTpvH4<#e$!>-lUtdR8X44{xd+fmfiMx;i zUuF)|G>(=kW8mP(P4Dkb@J>#2ZYNtGyTNSf9{yoQ!qHcOJ}|lzZpStA3mtVxof++< z*dV*mHOX$UW~rP%)dSN~N2nha#CjA(sDqX)y9f&~Cz@lf zd;sg*;`*g_o*X4USA;-S6-K}k+`J0TK6%uxGwG=KM+T;G6oSXv7ImKNWyf?@d)BzG zVBc)eCB-Wo%bafQI2?teUh(GOL7Nfh@>OnzW?J|Y0={@|3_mznJ`mNX4NzagRlMw< z(MB6DRD`4uP|-clFe-i3$5_@bv?%+wV+sL}VSYdy|Jh4&?NE z-z?r)m*5QqiL$|L3_90P`$>o&H0`?1Ha%0kx=J@@+_8H0SIOt7dSonG@?R1)F6ry6 z^Qyo7OC!tGd3KNDe# z7sN2L#t;V?&>za@I-XmR9{-SrKR;-<-lhI}&)!+vL}`=Hn%5&b``t0@^xA1Z)zsD? zKbr%tfE>xey$E{r5-58V@?F`Ydup4VTpipaO~)?CsJZawy$?u!PTE%zlC<>BYOOq2 z8y{rBSUkk@JQRcFl^ly47LN~uHSvjrG#2iy`G6{O=sc&uC3;)H>bTRXF@?h%3T+K4E3P?3i zuLO<9eyt(UqKKLAPZpmZrLLhRpxrEOGPn0=ORV1lCWw3>&$$d%YY1j0R zqAAF4!T*qG|J)HTU?BUO);3txcGQLfQNv!xa-c82zHk0ZWPfWmr1ua~LV zR!R&eYcA^ot_LQvU!uZe%`?@vsguQjH)(j^@;HYe_P2J2U1%v(E`?c|}h&~s!Hoh2KPjRyPkwhOJ1 zt_^z3d`DN8I7tE$YIo*T>!>hTjZ~Us-Y;y>exQ{ZV4?yRGp2OnsV>eopuWBz9<7)d ziZyK^X==QN3&*PZ*FOe#hS9&T)$lPH$1Etn>cnO(w|Sm{HiG_>2hsjdhmo95OSiV_ zDBmr#9H zQ3qMeb@4q#w6KWWn*&t<+e?Y9lXI%p_Z)%nH{tU#guu&B3t|_3g94%zuS&h(j&67 zcanfv?_JNG`#hl#u#2yS97(NQFZ6t}4?Gy84cO?2-BY9^KWnG0Uf3uU_`*ezdNwdE z4$C`}05B3rpdJI-)L9nH#nIDRMGlK50E*wqF8p0J`X=tTKDBVhv7)U=swl4#zd4aV zY$J_;gjo59#1^oAqq&gJl#qk$;Gi^}LBS;9*A(hVty0KXM)W0`4J$JaNG(VR|Mm<# z>?Cni^gTXzIM(r@>E1NsT_gErS+2LteZy_mPKa4nQW5CWw5OcM!i|9)JphY$z@VqL z)FhpqSNjcTB{pVSv9uSFBf{G6W}B6|ZV&?t%mC7SMMrcrEq4bb`~&T-3y$C;C%?g# zgKyGGX{+t+VjZj^k0{rQ1BhvV#+06=-a!u=!-r&=hc4G@MDhLh z4UEBBTH-`BjsnM#dYQN9AEF<}Rg1xvUBo+202=hs?FWBp3Az^(V(dwX@*Jy(T=tTaM#0rNBvSVwlw(@av~c zfx6@T8|8rlS+(p0s!c9q=H^)84o1XKz%6?o@ z{Cp?Pt93BK>5?4P1xijtQoTdB?Rpv`T*&Z%Ufb)w_6&y45wG8$xg8AM998S%;%j6Y zYU^y8ld2>@B?i%zw1dU}@Qjv=+mvmT$GQ{C8(mM?ppVlRP*A`huCk?iD0&3glj=pe zF)>wWw^pa@1tfnSKB85=)Bi}}IaOEpMwV#Tcw4s6Ojv7R=)rJA6q7E;`a4$T1eh{r$ht8-#mb}%hl zQ*Dk?C2H}aD{PuY;EM3kj0UogDl>mby<&Xnnk1mx=vAR4?29^n2E#c{uizO#=dQ>) zUgAL*7f0oj#fLbNrP0n^!G?=?EXfRbU?*amVsZi^UxX*Lx?j-fZ+X2p;q_FxNI+W< zkh$$!Xw<)wmF~C$>DHVOZy#70M8YDRF`X|iZ1?C9!itQh4zU;Uc!;a$hzL6ytbn42 zoEo0a8Dj|*s|~BM9Qm!5#KWG{62>>}>8m`>Su8epn3TM00hd`ydnDV+JJH;&b=%Aj z*JB4?-x8s7#0?b(LT0Ic#j;>t(g$U>9}do=1Dk^9Ehg<{fQX^6-s{#|P<79kO3PzU zpizuiOBh8S1L|B1fUS!8&XW?6C(BbnK!h##ie>cYRU3-h-jWzMMmW{{BUyiaV8p@T zhwg$OnoIEas^d=vfV3b@is$)FpDthlzBl5jNM&USRO5`wNoS z)HVZf7f0$o_^Jz+)XEWfNkKA6q}VsZ&Kjw#+4hAZL8yH$Yph;33XwYZ_f>rhLh5+DETXl!5N2O%zobE+%daPCy>$u`2az5{k1hnTsE0qQS&rE%9kE zV|59bY?ZM`pK#I7wl695#SKifSg6T3(@VHWv>G_|vxRPgID$P?w)iqO_!v45B4OWN z2DLxdBB{;jde2fn1c-wQ^ngu>3pEEq!jRe_$mGpTHS6_mUC<2Tw}GdPOtfvnc~1Zn zdHh<=B%ommDGX8CKL{6l$Me|ziLV0vZo@>~`<|JVXg1XKBS0u4)_*(l?Ijo6Lz(h2 zLf!Gq2Psg|AlnRxyKx!qrsC@$Mki2sP0$9Bx5uc6=@2<_Hy@iBdp|19i%fm;+XxnqAUL=_KYy->p3{rE@h;3o z@A5H;it~xFYEO68bwYhkV5acVc?1w%aIH99cz!?gS3u&E34P$7uH|q8Q-9t zXRUv9-)RSY0F}i!>|pxK1qVV~Q-}TE{?Jg1f7$>0DhaHyx0e94!ei+(ckOPei8t4dWqR#yRngdV5*jOYqoKWe#yZG+|-&kT^pEL(+tja?iAqV#ay*o zDxMRP2X3{WTO5NLfUs5CUV@3>Xl42TV2L-~b(|Qs3s)1l$;_HGH zop}qwmCZeoT_Rr9aY6{lCaXe-O!uhe#JKtF{o7EIsC@rAAAzorhP3Od&yQ*YYm}OW zgFkS1Nx+uHI8F)|o#Y9C7xsUL+tF(oWQE#*$Go@%rzBOki-aW`_?$wO7qI@jcF))d7j%2IDyN#?*ntkF?1+C|Lt!+kb0`g z`nmCPqVzEJL4?Rr;LoL=SLwiB_p3RM~!yk4TSBLau-BFmRM3bl_%qXZjQNNNy*9WW+^p>9yY1w`ac052|a6fcLdMTuYatGF;!UCwMXZ-dGDX zm&X4Zeq8S~luO#|S|50Rpgbob(VUzVw&xRYtNx{Lk-*QkBeU-e!?ccks9a0-=t#rv z6OD(!rzL5e5Ki`__2e>MWkAGr+3Der-8$a52v4Z|8T4n7cr4w!+1W;`Y|%V@AchBv zmm5;R1RX-NacIhWJwszwPf_#X82Q)9x02T|k^mhkvNplS9TbpGFwqOd)3pS&Zc1v;IBHD^mS>>2OAsdyKEjLYyE? znF;CqxXkD&02xy-H34=PwNTvZZbW`=f)lGt{_9D?{NyD&pIgCmSeyG9_9Thmm>|4; z?l20h7D!9@WHYCszcO9Xtxyj}cUR43sBUJ5J(t}8zj}*L{O(^v@9f-_2Sz`!;V{#N zqJM{-j?9zpy1|~u{rZnC?D^(%O9tN!yy6}2lgAIPs8JHm^Apv~3zBZvQZoWhM;-SR z(*qX9^q%@x2VvtH`s6HB{aGo!^Sj88?gM(i1~Y9OGz-2@<`_`-@MWtBKlg@;HXVn5 z^Ka2MnzK<7|1@bTh`=R45_zR=n@{vR)`a1_J=4 zQ0MrtV)yOH?=^jNFu?c;!joy`m$PQc=znN+sD>B{{g#68G?PynbH?P)&*;Jkyod7D z{(s(}B554n5h_qJDmFSWwe{szeqCGS8oXrZDI6SF`w@HRPpBTr#Sz1m0kBnv%hkzs zI^@5IAZynE`j1|mpmgv5m5vm*W8lWvS1q~ipS@DX0yw3$SF0UHZT+mq6^AIiq?d&; zrUi6% z5(_SIXMH9b7Fp--T~`5tX1J%{B3kS6IrGT!!C{)sd#)#RuL{0@;F{0@V&OldtQkRlf(Lxg4tXU6=9lh2Br8p@CoJ3gd;_?geSNd zVE4|BpU)AeW9%$c}Q8P^DjF+PQZ%YxXmd4 z87)934@mVEkB5eZMK9IS_YH8<#p15W*c`K~PUbm{*85Mo3~wITmz;z6hMgE0T;#Mm z!+bPu@XMK+jAwEmj+@rU{`$i#zH)jd!37!aoeV)!$&)mY8fAnr`;Rq(&pAtZwwO58 z5P(re%2}_qWHX^23$Q|SvUmxom?}W}Um3FN&V5x?wc^&{=<9TyTyHG9RVa_|LiB8D zlnidSgPrW$uGASK1b;LG5M5$;w@_QF2Z#JEYn}N&5Bvw2$~p#E=Af9JCNk}A zZ^SC(ve?}{wk|Xlo|vUx=k(XgknR%sYYbO8k*||uH-1UqT6^$c5*Sq?ZdW`d>Fiu= zroX-dz65SSeHgkg%v2ygDKgD<>pNX>(@yj2{iqcC?T{8i>dDIhQd2U9-XE%msoPI5>Uzm?4$sG-O|=K% z`3FdoEs-q9x`035;fd|u&XK!hQ@EA6whxlsXL*aM5^crJjLUm;nLfx*%JYME6us5& z4Iv(N;;oM&1M02Nj#DQzt$p&+7%w3lAN9}7L3TdkBk^s+6SnkZ7maPZvlA`$T z6WQx&+T_|U2DD5DyA7Dfzz4Ioqd5@Pei+<#$$k1xmPsn43u(_Y?Ey+`c4J*M`RZ6y z=$!y*S+tyr>L0x~wP)}6wpnTeUQD`2OXsfeds^22_+pD%v$m-ZS6eiiE2q zUXH_lPZy+o9)Z@|n;H>@|J}j(&=H}| ziz@*Cc-s5E5EVH%pEh-Lxcb$QsHRV(Q^d%zMi>!%Hs$^l+%NKoq^0p1>`kj9v+fmL z{Ey!H{}!YeJsW~oB|=`H{oS6v z?d_)T10JsM?Zh{Dbo?)5O(Ki_#etg}Q})v=M|KZ$^mW{Ov5GI2xjihvDtV|t!#(i^ z!yy>af)0uC;5-%9U4Kx~5HN*2lr*%WmerU6^3oMf>vh%PZ8ZZ$^ZFeTu0HJ+U`ff~78L9y+(1R(^1!X!gay20kiyq=cS^}3UU z08_gQ`)SWthF@1x8` z`NPT$a2R)p5V$4bS*8SRUh3MB_rAlFWO(Y9B#!jVc!R&7;QRZ<&_Q~Z6pAAXk zZ<~mr0i19?6O-ARD=^v;U*cnRaQ$E8(s9_^;fK7w)CzXz^KZnXpV}eQrC1!Ap*a97TkHVz~P0%Wdi^=n6!q8MU(h}1F z#E+P~nJ^JwI3XB;Hck-3nY~rzY{|3thz^Z@nj=_h2%Ls7vH$89nFu1UfEWX=>~ZNk|IM`A04IcINtK?HYQL8#vDm z%>NIFmwmX!#wdP{jSO45!2q-#hQRumng7@|{5(EtmWo3L@$mwMUGBigTb8sM@93?s zpKic{lprBSN)GU`ykhjnh3517bh(f&KBP4V*Pd2?rd7cae)K7a(mKbu_m~BEP=JqE zxyl=*uMRjbFi_9^biPD_Q_J9(BIt7Z@lDZb(pME+A4(%|uqChn42Dp*u4)khc3yxj z-WQOMbwsM1l?Mb)jsjMN1vyo9Tt~4#A+^uVb#~nN-FlIK5>gBMb@rf9j5EyQW+e4_ z(=;0C;duxgG0y(p-*6}NI?KRVNP5n>YQ?Lq#aZ|$SWXiZmvHY13N#x^^_cG92wETM^lh^ z?xWZhh~klIaicTYe@eY(#CfuA7sd<8e=YLCKMSQJ^TMo;baHc#zgJWRKVtwZtIl_|=z}40RR3!d(&j-eVWX}hF zLz)_dT6PK|8VBhyh`)Za!^?U#CFyvA#h07wDWo`VBmt8Dh7U^7eK^?4$+9V88j_YB zPTJ8k5MhE|K8gG5&EQAyoyWIa(h}e&I=cp>4@zE1+0o;Kwg6(I>Ox7r&p3WlN$&E0 zg}?7vK7e$qccH89oXa*O>G4I{g2+OO`ICAELpGT1dla^rbstt4aoD7m}!Y&qJBxRLzSd=h0b~ z!l}JetY4Scz1}eJf3u~vvMwLg3!ls|8eJcvj?*97WS)`^{64q=s_n8?MnII>{{tUP aG8tt%!mFPsu95oB9P^7d#&w3CasMCDN4stS literal 0 HcmV?d00001 diff --git a/examples/splashscreen/dist/index.html b/examples/splashscreen/dist/index.html new file mode 100644 index 0000000..f503129 --- /dev/null +++ b/examples/splashscreen/dist/index.html @@ -0,0 +1,14 @@ + + + +

This is the main window!

+ + + + diff --git a/examples/splashscreen/dist/splashscreen.html b/examples/splashscreen/dist/splashscreen.html new file mode 100644 index 0000000..7117605 --- /dev/null +++ b/examples/splashscreen/dist/splashscreen.html @@ -0,0 +1,26 @@ + + + + Splashscreen + + + + +
+ + diff --git a/examples/splashscreen/main.rs b/examples/splashscreen/main.rs new file mode 100644 index 0000000..e7fb281 --- /dev/null +++ b/examples/splashscreen/main.rs @@ -0,0 +1,29 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use tauri::{AppHandle, Manager}; + +#[tauri::command] +fn close_splashscreen(app: AppHandle) { + // Close splashscreen + app + .get_webview_window("splashscreen") + .unwrap() + .close() + .unwrap(); + // Show main window + app.get_webview_window("main").unwrap().show().unwrap(); +} + +fn main() { + tauri::Builder::default() + .menu(tauri::menu::Menu::default) + .invoke_handler(tauri::generate_handler![close_splashscreen]) + .run(tauri::generate_context!( + "../../examples/splashscreen/tauri.conf.json" + )) + .expect("error while running tauri application"); +} diff --git a/examples/splashscreen/tauri.conf.json b/examples/splashscreen/tauri.conf.json new file mode 100644 index 0000000..1f3348f --- /dev/null +++ b/examples/splashscreen/tauri.conf.json @@ -0,0 +1,43 @@ +{ + "$schema": "../../crates/tauri-schema-generator/schemas/config.schema.json", + "productName": "Splashscreen", + "version": "0.1.0", + "identifier": "com.tauri.dev", + "build": { + "frontendDist": "dist" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "label": "main", + "title": "Tauri", + "width": 800, + "height": 600, + "visible": false + }, + { + "label": "splashscreen", + "width": 400, + "height": 200, + "decorations": false, + "resizable": false, + "url": "splashscreen.html" + } + ], + "security": { + "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost" + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "../.icons/32x32.png", + "../.icons/128x128.png", + "../.icons/128x128@2x.png", + "../.icons/icon.icns", + "../.icons/icon.ico" + ] + } +} diff --git a/examples/state/README.md b/examples/state/README.md new file mode 100644 index 0000000..bd11e72 --- /dev/null +++ b/examples/state/README.md @@ -0,0 +1,5 @@ +# State example + +A simple Tauri Application showcase the application State usage. + +To execute run the following on the root directory of the repository: `cargo run --example state` diff --git a/examples/state/index.html b/examples/state/index.html new file mode 100644 index 0000000..8b7d808 --- /dev/null +++ b/examples/state/index.html @@ -0,0 +1,49 @@ + + + + + + Tauri + + + +

Counter:

+
+ + + +
+ +

Press Ctrl+R to reload and see the state persist.

+ + + + diff --git a/examples/state/main.rs b/examples/state/main.rs new file mode 100644 index 0000000..64b4780 --- /dev/null +++ b/examples/state/main.rs @@ -0,0 +1,47 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::sync::Mutex; + +use tauri::State; + +struct Counter(Mutex); + +#[tauri::command] +fn increment(counter: State<'_, Counter>) -> isize { + let mut c = counter.0.lock().unwrap(); + *c += 1; + *c +} + +#[tauri::command] +fn decrement(counter: State<'_, Counter>) -> isize { + let mut c = counter.0.lock().unwrap(); + *c -= 1; + *c +} + +#[tauri::command] +fn reset(counter: State<'_, Counter>) -> isize { + let mut c = counter.0.lock().unwrap(); + *c = 0; + *c +} + +#[tauri::command] +fn get(counter: State<'_, Counter>) -> isize { + *counter.0.lock().unwrap() +} + +fn main() { + tauri::Builder::default() + .manage(Counter(Mutex::new(0))) + .invoke_handler(tauri::generate_handler![increment, decrement, reset, get]) + .run(tauri::generate_context!( + "../../examples/state/tauri.conf.json" + )) + .expect("error while running tauri application"); +} diff --git a/examples/state/tauri.conf.json b/examples/state/tauri.conf.json new file mode 100644 index 0000000..e0395a6 --- /dev/null +++ b/examples/state/tauri.conf.json @@ -0,0 +1,35 @@ +{ + "$schema": "../../crates/tauri-schema-generator/schemas/config.schema.json", + "productName": "State", + "version": "0.1.0", + "identifier": "com.tauri.dev", + "build": { + "frontendDist": ["index.html"] + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "Welcome to Tauri!", + "width": 800, + "height": 600, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost" + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "../.icons/32x32.png", + "../.icons/128x128.png", + "../.icons/128x128@2x.png", + "../.icons/icon.icns", + "../.icons/icon.ico" + ] + } +} diff --git a/examples/streaming/README.md b/examples/streaming/README.md new file mode 100644 index 0000000..dd59a60 --- /dev/null +++ b/examples/streaming/README.md @@ -0,0 +1,9 @@ +# Streaming example + +A simple Tauri Application showcase how to stream video through custom protocol but can be adapted to stream any type of files. + +To execute run the following on the root directory of the repository: `cargo run --example streaming`. + +### Note + +Tauri has a built-in `asset` protocol that implements this streaming functionality so you don't need to. This example just exists as a reference. diff --git a/examples/streaming/index.html b/examples/streaming/index.html new file mode 100644 index 0000000..22bc76d --- /dev/null +++ b/examples/streaming/index.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/examples/streaming/main.rs b/examples/streaming/main.rs new file mode 100644 index 0000000..58d602d --- /dev/null +++ b/examples/streaming/main.rs @@ -0,0 +1,213 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode}; +use http_range::HttpRange; +use std::{ + io::{Read, Seek, SeekFrom, Write}, + path::PathBuf, + process::{Command, Stdio}, +}; + +fn get_stream_response( + request: http::Request>, +) -> Result>, Box> { + // skip leading `/` + let path = percent_encoding::percent_decode(&request.uri().path().as_bytes()[1..]) + .decode_utf8_lossy() + .to_string(); + + // return error 404 if it's not our video + if path != "streaming_example_test_video.mp4" { + return Ok(ResponseBuilder::new().status(404).body(Vec::new())?); + } + + let mut file = std::fs::File::open(&path)?; + + // get file length + let len = { + let old_pos = file.stream_position()?; + let len = file.seek(SeekFrom::End(0))?; + file.seek(SeekFrom::Start(old_pos))?; + len + }; + + let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4"); + + // if the webview sent a range header, we need to send a 206 in return + let http_response = if let Some(range_header) = request.headers().get("range") { + let not_satisfiable = || { + ResponseBuilder::new() + .status(StatusCode::RANGE_NOT_SATISFIABLE) + .header(CONTENT_RANGE, format!("bytes */{len}")) + .body(vec![]) + }; + + // parse range header + let ranges = if let Ok(ranges) = HttpRange::parse(range_header.to_str()?, len) { + ranges + .iter() + // map the output back to spec range , example: 0-499 + .map(|r| (r.start, r.start + r.length - 1)) + .collect::>() + } else { + return Ok(not_satisfiable()?); + }; + + /// The Maximum bytes we send in one range + const MAX_LEN: u64 = 1000 * 1024; + + if ranges.len() == 1 { + let &(start, mut end) = ranges.first().unwrap(); + + // check if a range is not satisfiable + // + // this should be already taken care of by HttpRange::parse + // but checking here again for extra assurance + if start >= len || end >= len || end < start { + return Ok(not_satisfiable()?); + } + + // adjust end byte for MAX_LEN + end = start + (end - start).min(len - start).min(MAX_LEN - 1); + + // calculate number of bytes needed to be read + let bytes_to_read = end + 1 - start; + + // allocate a buf with a suitable capacity + let mut buf = Vec::with_capacity(bytes_to_read as usize); + // seek the file to the starting byte + file.seek(SeekFrom::Start(start))?; + // read the needed bytes + file.take(bytes_to_read).read_to_end(&mut buf)?; + + resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); + resp = resp.header(CONTENT_LENGTH, end + 1 - start); + resp = resp.status(StatusCode::PARTIAL_CONTENT); + resp.body(buf) + } else { + let mut buf = Vec::new(); + let ranges = ranges + .iter() + .filter_map(|&(start, mut end)| { + // filter out unsatisfiable ranges + // + // this should be already taken care of by HttpRange::parse + // but checking here again for extra assurance + if start >= len || end >= len || end < start { + None + } else { + // adjust end byte for MAX_LEN + end = start + (end - start).min(len - start).min(MAX_LEN - 1); + Some((start, end)) + } + }) + .collect::>(); + + let boundary = random_boundary(); + let boundary_sep = format!("\r\n--{boundary}\r\n"); + let boundary_closer = format!("\r\n--{boundary}--\r\n"); + + // `Builder::header` appends, we want to replace the file mime type set earlier + if let Some(headers) = resp.headers_mut() { + headers.insert( + CONTENT_TYPE, + HeaderValue::from_str(&format!("multipart/byteranges; boundary={boundary}"))?, + ); + } + + for (start, end) in ranges { + // a new range is being written, write the range boundary + buf.write_all(boundary_sep.as_bytes())?; + + // write the needed headers `Content-Type` and `Content-Range` + buf.write_all(format!("{CONTENT_TYPE}: video/mp4\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())?; + + // calculate number of bytes needed to be read + let bytes_to_read = end + 1 - start; + + let mut local_buf = vec![0_u8; bytes_to_read as usize]; + file.seek(SeekFrom::Start(start))?; + file.read_exact(&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())?; + + resp = resp.status(StatusCode::PARTIAL_CONTENT); + resp.body(buf) + } + } else { + resp = resp.header(CONTENT_LENGTH, len); + let mut buf = Vec::with_capacity(len as usize); + file.read_to_end(&mut buf)?; + resp.body(buf) + }; + + http_response.map_err(Into::into) +} + +fn random_boundary() -> String { + let mut x = [0_u8; 30]; + getrandom::fill(&mut x).expect("failed to get random bytes"); + (x[..]) + .iter() + .map(|&x| format!("{x:x}")) + .fold(String::new(), |mut a, x| { + a.push_str(x.as_str()); + a + }) +} + +fn download_video() { + let video_file = PathBuf::from("streaming_example_test_video.mp4"); + if !video_file.exists() { + let video_url = + "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; + + // Downloading with curl this saves us from adding + // a Rust HTTP client dependency. + println!("Downloading {video_url}"); + let status = Command::new("curl") + .arg("-L") + .arg("-o") + .arg(&video_file) + .arg(video_url) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .output() + .unwrap(); + + assert!(status.status.success()); + assert!(video_file.exists()); + } +} + +fn main() { + download_video(); + + tauri::Builder::default() + .register_asynchronous_uri_scheme_protocol("stream", move |_ctx, request, responder| { + match get_stream_response(request) { + Ok(http_response) => responder.respond(http_response), + Err(e) => responder.respond( + ResponseBuilder::new() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, "text/plain") + .body(e.to_string().as_bytes().to_vec()) + .unwrap(), + ), + } + }) + .run(tauri::generate_context!( + "../../examples/streaming/tauri.conf.json" + )) + .expect("error while running tauri application"); +} diff --git a/examples/streaming/tauri.conf.json b/examples/streaming/tauri.conf.json new file mode 100644 index 0000000..7797608 --- /dev/null +++ b/examples/streaming/tauri.conf.json @@ -0,0 +1,36 @@ +{ + "$schema": "../../crates/tauri-schema-generator/schemas/config.schema.json", + "productName": "Streaming", + "version": "0.1.0", + "identifier": "com.tauri.dev", + "build": { + "frontendDist": ["index.html"] + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "Stream video using custom protocol", + "width": 800, + "height": 600 + } + ], + "security": { + "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost; media-src stream: http://stream.localhost", + "assetProtocol": { + "scope": ["**/streaming_example_test_video.mp4"] + } + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "../.icons/32x32.png", + "../.icons/128x128.png", + "../.icons/128x128@2x.png", + "../.icons/icon.icns", + "../.icons/icon.ico" + ] + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b84acc4 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "tauri-workspace", + "version": "0.0.0", + "license": "Apache-2.0 OR MIT", + "private": true, + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "repository": { + "type": "git", + "url": "https://github.com/tauri-apps/tauri.git" + }, + "scripts": { + "format": "prettier --write .", + "format:check": "prettier --check .", + "eslint:check": "pnpm run -r eslint:check", + "ts:check": "pnpm run -r ts:check", + "build": "pnpm run -F !api build && pnpm run -F api build", + "build:debug": "pnpm run -F !api build:debug && pnpm run -F api build:debug", + "build:api": "pnpm run --filter \"@tauri-apps/api\" build", + "build:api:debug": "pnpm run --filter \"@tauri-apps/api\" build:debug", + "build:cli": "pnpm run --filter \"@tauri-apps/cli\" build", + "build:cli:debug": "pnpm run --filter \"@tauri-apps/cli\" build:debug", + "test": "pnpm run -r test", + "test:api-e2e": "pnpm run --filter \"@tauri-apps/api-e2e\" e2e", + "test:api-e2e:android": "pnpm run --filter \"@tauri-apps/api-e2e\" e2e:android", + "test:api-e2e:ios": "pnpm run --filter \"@tauri-apps/api-e2e\" e2e:ios", + "example:api:dev": "pnpm run --filter \"api\" tauri dev" + }, + "devDependencies": { + "prettier": "^3.8.1" + }, + "minimumReleaseAge": 4320, + "packageManager": "pnpm@12.4.2" +} diff --git a/packages/api-e2e/README.md b/packages/api-e2e/README.md new file mode 100644 index 0000000..9fad1a6 --- /dev/null +++ b/packages/api-e2e/README.md @@ -0,0 +1,196 @@ +# `@tauri-apps/api` end-to-end tests + +WebdriverIO suite that exercises every [`@tauri-apps/api`](../api) module against a +**real** Tauri app — the [`examples/api`](../../examples/api) validation app — rather than +a mocked backend, on desktop (Linux, macOS, Windows) and mobile (Android, iOS). Each module +has its own spec file, shared by every platform, and adding coverage for a new API is +normally just dropping in one more spec. + +## How it works + +- The example app is built with `withGlobalTauri: true`, so the entire API surface is + reachable on `window.__TAURI__` inside the webview. +- On desktop, WebdriverIO drives the app through [`@crabnebula/tauri-driver`](https://www.npmjs.com/package/@crabnebula/tauri-driver), + which bridges the WebDriver protocol to each platform's webview: + - **macOS** — the CrabNebula Webdriver, which needs [`tauri-plugin-automation`](https://crates.io/crates/tauri-plugin-automation) + (registered in `examples/api` behind its off-by-default `automation` Cargo feature, which + the suite's build enables) and a locally-running `@crabnebula/test-runner-backend`, + authenticated with `CN_API_KEY`. + - **Linux** — `webkit2gtk-driver` (`WebKitWebDriver` on `PATH`). + - **Windows** — `msedgedriver.exe` on `PATH`. It hands the app the `--remote-debugging-port` + it attaches to through `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS`, which WebView2 ignores in an + elevated process ([wry#1782](https://github.com/tauri-apps/wry/issues/1782)), so the suite has + to run unelevated. +- On mobile, WebdriverIO drives the app through [Appium](https://appium.io) (started by + `@wdio/appium-service`; the drivers are plain devDependencies of this package, which Appium + picks up on its own): + - **Android** — the UiAutomator2 driver. The suite switches to the app's `WEBVIEW_*` + context, which chromedriver reaches through the WebView's debugging socket. Debug builds + turn that on (`setWebContentsDebuggingEnabled`), so the suite builds a debug APK. A + chromedriver matching the device's WebView is downloaded on demand (see `E2E_CHROMEDRIVER`). + - **iOS** — the XCUITest driver on a simulator, attaching to the WKWebView through the + WebKit remote inspector. Debug builds mark the webview `isInspectable`, so the suite + builds an unsigned debug simulator app. The inspector identifies an app by the + `application-identifier` entitlement that Xcode embeds when it code signs a simulator + build; an unsigned one has none and is listed as `process-` instead of + its bundle identifier, so the config has the driver match that name too + (`appium:additionalWebviewBundleIds`). The driver also starts with a script timeout of + 0, which the config raises to the 30s the other drivers default to, or every + `executeAsync` would time out at once. +- Specs never `eval` in the page. They pass a function to the [`tauri()`](test/helpers/index.ts) + helper, which serializes it and runs it via the driver's own (CSP-exempt) script injection, + handing it `window.__TAURI__` as the first argument and returning its JSON result. +- Each spec file gets its own session — a fresh `tauri-driver` (and therefore a fresh app + instance) on desktop, a fresh Appium session (which relaunches the app) on mobile — so + each module's suite runs in isolation. + +## Prerequisites + +```sh +# from the repo root +pnpm install +pnpm build:api # examples/api resolves @tauri-apps/api from packages/api/dist +pnpm build:cli # examples/api's `tauri` script uses the local native CLI +``` + +Platform driver dependencies: + +| Platform | Requirement | +| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| macOS | `CN_API_KEY` env var (CrabNebula Cloud). The automation plugin and test-runner-backend are wired up already. | +| Linux | `webkit2gtk-driver` package (provides `WebKitWebDriver`). | +| Windows | `msedgedriver.exe` matching your Edge version, on `PATH`. Run the suite unelevated. | +| Android | The usual Tauri Android setup (`ANDROID_HOME`, `NDK_HOME`, a JDK), plus a running emulator or a connected device with USB debugging. Network access the first time, for the chromedriver download. | +| iOS | macOS with Xcode and an iOS simulator runtime. `tauri ios init` installs [xcodegen](https://github.com/yonaskolb/XcodeGen) through Homebrew if missing. The first session compiles WebDriverAgent (a few minutes). | + +## Running + +```sh +# from the repo root +pnpm test:api-e2e + +# or from this package +pnpm e2e + +# iterate without rebuilding the app every run +E2E_SKIP_BUILD=1 pnpm e2e + +# run a single module's spec +pnpm exec wdio run ./wdio.conf.ts --spec test/specs/window.spec.ts + +# mobile (from the repo root; or `pnpm e2e:android` / `pnpm e2e:ios` from this package) +pnpm test:api-e2e:android +pnpm test:api-e2e:ios +``` + +The first run builds the app with [`tauri.e2e.conf.json`](tauri.e2e.conf.json) as a config +override, which enables the example's `automation` feature; afterwards use `E2E_SKIP_BUILD=1` +to reuse the existing binary. A binary supplied through `E2E_SKIP_BUILD` or `E2E_APP_PATH` +must have been built with that feature whenever the CrabNebula Webdriver is in use (always +on macOS). + +The mobile configs ([`wdio.android.conf.ts`](wdio.android.conf.ts), [`wdio.ios.conf.ts`](wdio.ios.conf.ts), +sharing [`wdio.mobile.ts`](wdio.mobile.ts)) initialize the example's Android/Xcode project if +`src-tauri/gen` is missing, then run `tauri android build --debug --apk` / +`tauri ios build --debug --target aarch64-sim --no-sign`, compiling only the Rust target the +device runs (the Android one is read from the connected device through `adb`). The app must be +a **debug** build — release builds have webview debugging off, and Appium cannot see the page. +`E2E_SKIP_BUILD` and `E2E_APP_PATH` (an `.apk` / simulator `.app`) work as on desktop. + +On iOS, the XCUITest driver compiles WebDriverAgent with xcodebuild in the first session, which +takes minutes on a CI runner. Appium's own release build of it for the simulator skips that: +`APPIUM_HOME=$PWD pnpm exec appium driver run xcuitest download-wda -- --outdir
--platform iOS --kind sim` +(from this package), then `E2E_IOS_WDA=/WebDriverAgentRunner-Runner.app`, and the driver +installs and launches it as is (`appium:usePreinstalledWDA`). + +The generated Gradle and Xcode projects call back into the CLI with `pnpm tauri …` from +`src-tauri` / `gen/apple`, which pnpm 12.0–12.3 could not resolve to the package's scripts +([pnpm/pnpm#14645](https://github.com/pnpm/pnpm/pull/14645)); the repo's `packageManager` pins a +fixed version, so run the suite through that pnpm (corepack) rather than an older global one. + +## Environment variables + +| Variable | Purpose | +| ------------------- | --------------------------------------------------------------------------------- | +| `CN_API_KEY` | CrabNebula Cloud key. Required on macOS (and whenever `E2E_CN_WEBDRIVER=1`). | +| `E2E_SKIP_BUILD` | Skip the `tauri build` step and reuse the existing binary. | +| `E2E_APP_PATH` | Absolute path to a prebuilt app/binary to test (also implies skip-build). | +| `E2E_SKIP` | Comma-separated module names to skip, e.g. `E2E_SKIP=tray,menu`. | +| `E2E_SKIP_WM` | Skip window-manager-dependent tests (minimize/maximize/fullscreen/position/hide). | +| `E2E_SPEC_RETRIES` | Retry count for flaky spec files (default `0`). | +| `E2E_CN_WEBDRIVER` | Use the CrabNebula Webdriver on Linux/Windows too (instead of the native driver). | +| `E2E_NATIVE_DRIVER` | Path passed to `tauri-driver --native-driver` (e.g. a specific chromedriver). | +| `CARGO_TARGET_DIR` | Override the target dir the app binary is looked up in. | + +Mobile only: + +| Variable | Purpose | +| -------------------- | ---------------------------------------------------------------------------------------------------- | +| `E2E_ANDROID_TARGET` | Rust target for the APK (`aarch64`, `armv7`, `i686`, `x86_64`); default: the connected device's ABI. | +| `E2E_ANDROID_DEVICE` | `adb` serial of the device/emulator to use (`appium:udid`); default: the first connected one. | +| `E2E_ANDROID_AVD` | Name of an AVD for Appium to boot (`appium:avd`) instead of using an already-running emulator. | +| `E2E_CHROMEDRIVER` | chromedriver binary matching the device's WebView, instead of letting Appium download one. | +| `E2E_IOS_TARGET` | Rust target for the simulator app (`aarch64-sim` or `x86_64`); default: the host architecture. | +| `E2E_IOS_DEVICE` | Simulator UDID or name (as in `xcrun simctl list`); default: a booted iPhone, else the newest one. | +| `E2E_IOS_WDA` | Prebuilt `WebDriverAgentRunner-Runner.app` for the simulator, used instead of compiling WDA. | +| `E2E_PLATFORM` | Set by the mobile configs for the spec workers (`android`/`ios`) — see `platform` in the helpers. | + +Appium's own log is written to `logs/wdio-appium.log` in this package. + +## Adding tests for a new API + +1. **Add a spec.** Create `test/specs/.spec.ts` and use `describeApi('', …)` + with the `tauri()` helper. It is picked up automatically by the `test/specs/**/*.spec.ts` + glob. Minimal example: + + ```ts + import { expect } from '@wdio/globals' + import { tauri, describeApi } from '../helpers/index.js' + + describeApi('app', () => { + it('reports the product name', async () => { + expect(await tauri((api) => api.app.getName())).toBe('Tauri API') + }) + }) + ``` + +2. **Grant permissions if needed.** If the API calls a command that is not allowed by + default, add the permission to + [`examples/api/src-tauri/capabilities/run-app.json`](../../examples/api/src-tauri/capabilities/run-app.json). + The default permission sets are documented under + `crates/tauri/permissions/*/autogenerated/reference.md`. + +3. **Add a Rust command if needed.** If the API needs a custom backend command, add it to + [`examples/api/src-tauri/src/cmd.rs`](../../examples/api/src-tauri/src/cmd.rs), register it + in the `generate_handler!` list in + [`src/lib.rs`](../../examples/api/src-tauri/src/lib.rs), add its permission, and grant it + in `run-app.json`. + +4. **Handle environment-sensitive cases.** Use `itWm` (instead of `it`) for assertions that + depend on a real window manager, and `eventually()` to poll for state a WM applies + asynchronously. Branch on `platform` from the helpers (never `process.platform`, which is + the host running the emulator/simulator on mobile) for platform-specific behavior. + +5. **Gate what mobile does not have.** The same specs run on Android and iOS. Wrap tests of + desktop-only commands (`#[cfg(desktop)]` in the core plugins, or no-ops on mobile such as + the window title and size) in `itDesktop`, use `itOn('android', …)` / `itOn('ios', …)` for + platform-specific APIs, and pass `{ desktopOnly: true }` to `describeApi` for modules whose + plugin is not registered on mobile at all (`menu`, `tray`). Skipped tests show up as pending + rather than silently disappearing. + +### Rules for `tauri()` page functions + +The function you pass to `tauri()` runs **inside the webview**, serialized as a string: + +- It **cannot** close over anything from the spec module — pass every value it needs through + the trailing `tauri(fn, ...args)` arguments. +- It may only reference `api` (the `window.__TAURI__` object), those args, and browser + globals (`window`, `document`, `setTimeout`, `Promise`, …). +- Its return value must be JSON-serializable — return plain objects/primitives, not class + instances (call methods and return their results instead). +- Restore any app state you mutate (title, size, theme, …); tests within a spec file share + the same app instance. +- For in-page waiting, wrap logic in a `Promise` with an explicit `setTimeout` rejection so a + failure surfaces as a message rather than an opaque driver timeout. + +Use `tauriError(fn, ...args)` to assert that a call rejects; it returns the rejection message. diff --git a/packages/api-e2e/package.json b/packages/api-e2e/package.json new file mode 100644 index 0000000..0911e74 --- /dev/null +++ b/packages/api-e2e/package.json @@ -0,0 +1,33 @@ +{ + "name": "@tauri-apps/api-e2e", + "private": true, + "version": "0.0.0", + "description": "End-to-end test suite for @tauri-apps/api, driven against the examples/api app via WebdriverIO.", + "license": "Apache-2.0 OR MIT", + "type": "module", + "scripts": { + "e2e": "wdio run ./wdio.conf.ts", + "e2e:skip-build": "cross-env E2E_SKIP_BUILD=true wdio run ./wdio.conf.ts", + "e2e:android": "wdio run ./wdio.android.conf.ts", + "e2e:ios": "wdio run ./wdio.ios.conf.ts", + "ts:check": "tsc --noEmit" + }, + "devDependencies": { + "@crabnebula/tauri-driver": "^2.0.9", + "@crabnebula/test-runner-backend": "^0.3.0", + "@tauri-apps/api": "../api/dist", + "@types/node": "^22.10.0", + "@wdio/appium-service": "^9.31.9", + "@wdio/cli": "^9.31.9", + "@wdio/globals": "^9.31.3", + "@wdio/local-runner": "^9.31.9", + "@wdio/mocha-framework": "^9.31.9", + "@wdio/spec-reporter": "^9.31.2", + "appium": "^3.7.0", + "appium-uiautomator2-driver": "^8.7.0", + "appium-xcuitest-driver": "^12.12.4", + "cross-env": "^10.1.0", + "typescript": "^6.0.3", + "webdriverio": "^9.31.9" + } +} diff --git a/packages/api-e2e/tauri.e2e.conf.json b/packages/api-e2e/tauri.e2e.conf.json new file mode 100644 index 0000000..4786b44 --- /dev/null +++ b/packages/api-e2e/tauri.e2e.conf.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../crates/tauri-schema-generator/schemas/config.schema.json", + "build": { + "removeUnusedCommands": false, + "features": ["automation"] + }, + "bundle": { + "icon": [ + "../../.icons/32x32.png", + "../../.icons/128x128.png", + "../../.icons/128x128@2x.png", + "../../.icons/icon.icns", + "../../.icons/icon.ico" + ] + } +} diff --git a/packages/api-e2e/test/helpers/index.ts b/packages/api-e2e/test/helpers/index.ts new file mode 100644 index 0000000..fd2f849 --- /dev/null +++ b/packages/api-e2e/test/helpers/index.ts @@ -0,0 +1,243 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { browser } from '@wdio/globals' +import type * as TauriApi from '@tauri-apps/api' + +/** The full `@tauri-apps/api` surface, as exposed on `window.__TAURI__`. */ +export type Api = typeof TauriApi + +/** OS the app under test runs on. */ +export type Platform = NodeJS.Platform | 'android' | 'ios' + +/** + * The platform of the app under test. The desktop suite drives an app on the + * host, so it is `process.platform`; the mobile configs (`wdio.android.conf.ts`, + * `wdio.ios.conf.ts`) drive an emulator/simulator and set `E2E_PLATFORM` for + * the spec workers instead. + */ +export const platform: Platform = + (process.env.E2E_PLATFORM as Platform | undefined) ?? process.platform + +export const isMobile = platform === 'android' || platform === 'ios' + +type PageOutcome = + | { ok: true; value: T } + | { ok: false; error: string; stack?: string } + +/** Thrown when the function passed to {@link tauri} rejects inside the webview. */ +export class TauriPageError extends Error { + pageStack?: string + constructor(message: string, pageStack?: string) { + super(message) + this.name = 'TauriPageError' + this.pageStack = pageStack + } +} + +/** + * Runs `fn` inside the app's webview with `window.__TAURI__` as its first argument + * and resolves with its (JSON-serializable) return value. + * + * `fn` is serialized with `Function.prototype.toString`, so it **cannot close over + * anything in the spec module** — every value it needs must be passed through `args`, + * and it may only reference `api`, those args, and browser globals (`window`, + * `document`, `setTimeout`, `Promise`, ...). + * + * @example + * const name = await tauri((api) => api.app.getName()) + * const sum = await tauri((api, a, b) => a + b, 2, 3) + */ +export async function tauri( + fn: (api: Api, ...args: A) => R, + ...args: A +): Promise> { + // A string body (rather than passing `fn` directly) keeps this working across + // both the classic and bidi WebDriver protocols and avoids any in-page eval of + // our own — the driver injects this script itself, which is exempt from the + // app's CSP. `executeAsync` is used because promise support in `execute` is not + // uniform across the platform drivers tauri-driver proxies to. + // + // The outcome crosses the driver as a JSON string rather than an object so no + // driver gets to interpret its shape: the Selenium atoms that Appium runs + // scripts through on iOS turn any object with a numeric `length` property + // into an array. + const script = ` + var done = arguments[arguments.length - 1]; + var args = Array.prototype.slice.call(arguments, 0, arguments.length - 1); + var fn = (${fn.toString()}); + Promise.resolve() + .then(function () { return fn.apply(null, [window.__TAURI__].concat(args)); }) + .then( + function (value) { return { ok: true, value: value === undefined ? null : value }; }, + function (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined + }; + } + ) + .then(function (outcome) { + try { + done(JSON.stringify(outcome)); + } catch (error) { + done(JSON.stringify({ ok: false, error: 'result is not JSON-serializable: ' + error })); + } + }); + ` + const raw = await browser.executeAsync(script, ...args) + const outcome = ( + typeof raw === 'string' ? JSON.parse(raw) : raw + ) as PageOutcome> | null + if (!outcome || typeof outcome !== 'object' || !('ok' in outcome)) { + throw new Error( + `tauri() bridge returned an unexpected value: ${JSON.stringify(outcome)}` + ) + } + if (!outcome.ok) { + throw new TauriPageError(outcome.error, outcome.stack) + } + return outcome.value +} + +/** + * Asserts that the page-side call rejects and returns the rejection message, + * so specs can assert on it. Throws if the call unexpectedly resolves. + */ +export async function tauriError( + fn: (api: Api, ...args: A) => unknown, + ...args: A +): Promise { + try { + await tauri(fn, ...args) + } catch (error) { + if (error instanceof TauriPageError) { + return error.message + } + // Some platform drivers (notably the Linux WebKitWebDriver) surface a + // page-side `invoke` rejection as a WebDriver-level error on the + // `execute/async` command instead of letting the in-page bridge report it + // as an `{ ok: false }` outcome. Fall back to that error's message so the + // backend rejection is still assertable. This is safe for error-path specs: + // they match the message against an expected pattern, so a genuine driver + // failure (whose message won't match) still fails the test. + if (error instanceof Error) { + return error.message + } + throw error + } + throw new Error('expected the API call to reject, but it resolved') +} + +/** + * Polls `check` until it returns without throwing or `timeout` elapses. + * Use for state that a window manager applies asynchronously (focus, minimize, ...). + */ +export async function eventually( + check: () => T | Promise, + { + timeout = 10_000, + interval = 250 + }: { timeout?: number; interval?: number } = {} +): Promise { + const deadline = Date.now() + timeout + let lastError: unknown + for (;;) { + try { + return await check() + } catch (error) { + lastError = error + } + if (Date.now() > deadline) { + throw lastError instanceof Error + ? lastError + : new Error(String(lastError)) + } + await new Promise((resolve) => setTimeout(resolve, interval)) + } +} + +const skippedModules = (process.env.E2E_SKIP ?? '') + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean) + +export interface DescribeApiOptions { + /** + * The module's plugin is not registered at all on mobile (`menu`, `tray`), + * so the whole suite is skipped there. + */ + desktopOnly?: boolean +} + +/** + * `describe` wrapper keyed by API module name. Any module listed in the + * comma-separated `E2E_SKIP` env var (e.g. `E2E_SKIP=tray,menu`) is skipped. + */ +export function describeApi(module: string, fn: () => void): void +export function describeApi( + module: string, + options: DescribeApiOptions, + fn: () => void +): void +export function describeApi( + module: string, + optionsOrFn: DescribeApiOptions | (() => void), + maybeFn?: () => void +): void { + const [options, fn] = + typeof optionsOrFn === 'function' + ? [{} as DescribeApiOptions, optionsOrFn] + : [optionsOrFn, maybeFn!] + const title = `@tauri-apps/api/${module}` + if (skippedModules.includes(module) || (options.desktopOnly && isMobile)) { + describe.skip(title, fn) + } else { + describe(title, fn) + } +} + +/** + * `it` restricted to the given platform(s); skipped (as pending) elsewhere. + * Use for behavior that only exists on one OS, e.g. `activityName()` on Android. + */ +export function itOn( + platforms: Platform | Platform[], + title: string, + fn: () => void | Promise +): void { + const list = Array.isArray(platforms) ? platforms : [platforms] + if (list.includes(platform)) { + it(title, fn) + } else { + it.skip(title, fn) + } +} + +/** + * `it` for desktop-only APIs. The mobile builds do not register the commands + * behind them (`#[cfg(desktop)]` in the core plugins) or implement them as + * no-ops (window title, size, resizability), so they are skipped on mobile. + */ +export function itDesktop(title: string, fn: () => void | Promise): void { + if (isMobile) { + it.skip(title, fn) + } else { + it(title, fn) + } +} + +/** + * `it` for assertions that depend on a real window manager (minimize, maximize, + * focus, ...). Skipped entirely when `E2E_SKIP_WM` is set (e.g. bare headless CI), + * and on mobile, which has no window manager (nor those commands). + */ +export function itWm(title: string, fn: () => void | Promise): void { + if (process.env.E2E_SKIP_WM || isMobile) { + it.skip(title, fn) + } else { + it(title, fn) + } +} diff --git a/packages/api-e2e/test/specs/app.spec.ts b/packages/api-e2e/test/specs/app.spec.ts new file mode 100644 index 0000000..7b7ddca --- /dev/null +++ b/packages/api-e2e/test/specs/app.spec.ts @@ -0,0 +1,64 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { + tauri, + eventually, + describeApi, + platform, + isMobile +} from '../helpers/index.js' + +describeApi('app', () => { + it('getName returns the configured product name', async () => { + expect(await tauri((api) => api.app.getName())).toBe('Tauri API') + }) + + it('getVersion returns the configured version', async () => { + expect(await tauri((api) => api.app.getVersion())).toBe('1.0.0') + }) + + it('getTauriVersion returns a semver string', async () => { + expect(await tauri((api) => api.app.getTauriVersion())).toMatch( + /^\d+\.\d+\.\d+/ + ) + }) + + it('getIdentifier returns the bundle identifier', async () => { + expect(await tauri((api) => api.app.getIdentifier())).toBe('com.tauri.api') + }) + + it('defaultWindowIcon returns an image with a positive size', async () => { + const size = await tauri(async (api) => { + const icon = await api.app.defaultWindowIcon() + return icon ? await icon.size() : null + }) + expect(size).not.toBeNull() + expect(size!.width).toBeGreaterThan(0) + expect(size!.height).toBeGreaterThan(0) + }) + + it('setTheme applies a theme and can be reset to the system default', async () => { + await tauri((api) => api.app.setTheme('dark')) + // On Linux the app-level theme is not observable through `window.theme()`: + // tao's event-loop `set_theme` only flips the GTK `prefer-dark` setting and + // never updates the window's stored theme, so `theme()` keeps reporting the + // portal/system value (and blocks on a 5s dbus timeout when no portal runs, + // as in headless CI). On mobile `set_theme` is a no-op and `theme()` always + // reports `light`. Elsewhere the change is reflected on the window. + if (platform !== 'linux' && !isMobile) { + await eventually(async () => { + const theme = await tauri((api) => + api.window.getCurrentWindow().theme() + ) + if (theme !== 'dark') { + throw new Error(`expected theme "dark", got "${theme}"`) + } + }) + } + // restore + await tauri((api) => api.app.setTheme(null)) + }) +}) diff --git a/packages/api-e2e/test/specs/core.spec.ts b/packages/api-e2e/test/specs/core.spec.ts new file mode 100644 index 0000000..4507d81 --- /dev/null +++ b/packages/api-e2e/test/specs/core.spec.ts @@ -0,0 +1,109 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, tauriError, describeApi, itOn } from '../helpers/index.js' + +describeApi('core', () => { + it('isTauri reports true inside the app', async () => { + expect(await tauri((api) => api.core.isTauri())).toBe(true) + }) + + it('invoke round-trips a raw request through the echo command', async () => { + const echoed = await tauri((api) => + api.core.invoke('echo', { some: 'payload', n: 42 }) + ) + expect(echoed).toEqual({ some: 'payload', n: 42 }) + }) + + it('invoke rejects for a command that is not registered', async () => { + // `removeUnusedCommands` strips ungranted commands from the app entirely. + const message = await tauriError((api) => + api.core.invoke('this_command_does_not_exist') + ) + expect(message).toMatch(/not found|not allowed|denied/i) + }) + + it('command scopes gate log_operation by event', async () => { + // The run-app capability scopes `log_operation` to the `tauri-click` event. + await tauri((api) => + api.core.invoke('log_operation', { + event: 'tauri-click', + payload: 'from e2e' + }) + ) + const message = await tauriError((api) => + api.core.invoke('log_operation', { event: 'out-of-scope-event' }) + ) + expect(message).toMatch(/not allowed|denied/i) + }) + + it('Channel receives every message streamed from the backend in order', async () => { + // `spam` sends 1..=1000 over the channel. + const result = await tauri( + (api, count) => + new Promise<{ length: number; ordered: boolean; last: number }>( + (resolve, reject) => { + const received: number[] = [] + const channel = new api.core.Channel() + channel.onmessage = (value: number) => { + received.push(value) + if (received.length === count) { + let ordered = true + for (let i = 0; i < received.length; i++) { + if (received[i] !== i + 1) { + ordered = false + break + } + } + resolve({ + length: received.length, + ordered, + last: received[received.length - 1] + }) + } + } + api.core.invoke('spam', { channel }).catch(reject) + setTimeout( + () => + reject( + new Error( + `only received ${received.length} of ${count} messages` + ) + ), + 15000 + ) + } + ), + 1000 + ) + expect(result.length).toBe(1000) + expect(result.ordered).toBe(true) + expect(result.last).toBe(1000) + }) + + itOn( + ['android', 'ios'], + 'a mobile plugin resolves JSArray and JSObject values as JSON arrays and objects', + async () => { + // On Android, the sample plugin's `jsValues` goes through + // `Invoke.resolveObject`, i.e. the plugin JSON mapper rather than + // org.json's own `toString`. + const values = await tauri((api) => + api.core.invoke('plugin:sample|js_values') + ) + expect(values).toEqual({ + array: ['a', 1, true], + obj: { kind: 'object', nested: [1, 2] } + }) + } + ) + + it('convertFileSrc builds a platform-appropriate asset URL', async () => { + const url = await tauri((api) => + api.core.convertFileSrc('/path/to/file.png') + ) + expect(url).toMatch(/(asset:\/\/localhost\/|https?:\/\/asset\.localhost\/)/) + }) +}) diff --git a/packages/api-e2e/test/specs/dpi.spec.ts b/packages/api-e2e/test/specs/dpi.spec.ts new file mode 100644 index 0000000..393958f --- /dev/null +++ b/packages/api-e2e/test/specs/dpi.spec.ts @@ -0,0 +1,54 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, describeApi } from '../helpers/index.js' + +describeApi('dpi', () => { + it('LogicalSize.toPhysical scales up by the factor', async () => { + const physical = await tauri((api) => { + const size = new api.dpi.LogicalSize(100, 50).toPhysical(2) + return { width: size.width, height: size.height } + }) + expect(physical).toEqual({ width: 200, height: 100 }) + }) + + it('PhysicalSize.toLogical scales down by the factor', async () => { + const logical = await tauri((api) => { + const size = new api.dpi.PhysicalSize(200, 100).toLogical(2) + return { width: size.width, height: size.height } + }) + expect(logical).toEqual({ width: 100, height: 50 }) + }) + + it('LogicalPosition.toPhysical and PhysicalPosition.toLogical round-trip', async () => { + const result = await tauri((api) => { + const physical = new api.dpi.LogicalPosition(10, 20).toPhysical(2) + const logical = new api.dpi.PhysicalPosition(10, 20).toLogical(2) + return { + physical: { x: physical.x, y: physical.y }, + logical: { x: logical.x, y: logical.y } + } + }) + expect(result.physical).toEqual({ x: 20, y: 40 }) + expect(result.logical).toEqual({ x: 5, y: 10 }) + }) + + it('Size and Position wrappers delegate conversions', async () => { + const result = await tauri((api) => { + const size = new api.dpi.Size( + new api.dpi.LogicalSize(100, 50) + ).toPhysical(2) + const position = new api.dpi.Position( + new api.dpi.PhysicalPosition(20, 40) + ).toLogical(2) + return { + size: { width: size.width, height: size.height }, + position: { x: position.x, y: position.y } + } + }) + expect(result.size).toEqual({ width: 200, height: 100 }) + expect(result.position).toEqual({ x: 10, y: 20 }) + }) +}) diff --git a/packages/api-e2e/test/specs/event.spec.ts b/packages/api-e2e/test/specs/event.spec.ts new file mode 100644 index 0000000..16dd8c9 --- /dev/null +++ b/packages/api-e2e/test/specs/event.spec.ts @@ -0,0 +1,125 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, describeApi } from '../helpers/index.js' + +describeApi('event', () => { + it('listen receives an emitted payload', async () => { + const payload = await tauri( + (api) => + new Promise<{ hello: string }>((resolve, reject) => { + api.event + .listen<{ hello: string }>('e2e://ping', (event) => + resolve(event.payload) + ) + .then((unlisten) => { + api.event.emit('e2e://ping', { hello: 'world' }).catch(reject) + setTimeout(() => { + unlisten() + reject(new Error('event was not received')) + }, 5000) + }) + .catch(reject) + }) + ) + expect(payload).toEqual({ hello: 'world' }) + }) + + it('once fires exactly once even if the event is emitted twice', async () => { + const count = await tauri( + (api) => + new Promise((resolve, reject) => { + let calls = 0 + api.event + .once('e2e://once', () => { + calls++ + }) + .then(async () => { + await api.event.emit('e2e://once') + await api.event.emit('e2e://once') + setTimeout(() => resolve(calls), 1000) + }) + .catch(reject) + }) + ) + expect(count).toBe(1) + }) + + it('unlisten stops further delivery', async () => { + const count = await tauri( + (api) => + new Promise((resolve, reject) => { + let calls = 0 + api.event + .listen('e2e://off', () => { + calls++ + }) + .then(async (unlisten) => { + await api.event.emit('e2e://off') + setTimeout(async () => { + await unlisten() + await api.event.emit('e2e://off') + setTimeout(() => resolve(calls), 500) + }, 500) + }) + .catch(reject) + }) + ) + expect(count).toBe(1) + }) + + it('emitTo delivers only to the targeted label', async () => { + const result = await tauri( + (api) => + new Promise<{ mainCount: number }>((resolve, reject) => { + let mainCount = 0 + api.event + // The listener must be scoped to the `main` label: a default + // `listen()` registers the `Any` target, a catch-all that receives + // every emit regardless of the `emitTo` label (so it would also see + // the `does-not-exist` emit). + .listen( + 'e2e://target', + () => { + mainCount++ + }, + { target: 'main' } + ) + .then(async (unlisten) => { + await api.event.emitTo('main', 'e2e://target') // delivered + await api.event.emitTo('does-not-exist', 'e2e://target') // dropped + setTimeout(async () => { + await unlisten() + resolve({ mainCount }) + }, 800) + }) + .catch(reject) + }) + ) + expect(result.mainCount).toBe(1) + }) + + it('bridges events to and from the Rust backend', async () => { + // examples/api listens for `js-event` and replies with `rust-event { data }`. + const reply = await tauri( + (api) => + new Promise<{ data: string }>((resolve, reject) => { + api.event + .listen<{ data: string }>('rust-event', (event) => + resolve(event.payload) + ) + .then((unlisten) => { + api.event.emit('js-event', 'hello from e2e').catch(reject) + setTimeout(() => { + unlisten() + reject(new Error('no rust-event reply received')) + }, 5000) + }) + .catch(reject) + }) + ) + expect(reply).toEqual({ data: 'something else' }) + }) +}) diff --git a/packages/api-e2e/test/specs/image.spec.ts b/packages/api-e2e/test/specs/image.spec.ts new file mode 100644 index 0000000..f4eafad --- /dev/null +++ b/packages/api-e2e/test/specs/image.spec.ts @@ -0,0 +1,31 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, describeApi } from '../helpers/index.js' + +describeApi('image', () => { + it('Image.new round-trips raw rgba pixels', async () => { + // 2x1 image: red pixel, green pixel. + const rgba = [255, 0, 0, 255, 0, 255, 0, 255] + const result = await tauri( + async (api, pixels, width, height) => { + const image = await api.image.Image.new(pixels, width, height) + const size = await image.size() + const bytes = await image.rgba() + return { + width: size.width, + height: size.height, + byteLength: bytes.length + } + }, + rgba, + 2, + 1 + ) + expect(result.width).toBe(2) + expect(result.height).toBe(1) + expect(result.byteLength).toBe(rgba.length) + }) +}) diff --git a/packages/api-e2e/test/specs/menu.spec.ts b/packages/api-e2e/test/specs/menu.spec.ts new file mode 100644 index 0000000..a281c91 --- /dev/null +++ b/packages/api-e2e/test/specs/menu.spec.ts @@ -0,0 +1,88 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, describeApi } from '../helpers/index.js' + +// The menu commands are all allowed by default. These tests only build and +// inspect menu resources; they deliberately do not attach the menu to the app +// or window, to avoid disturbing the live example app. The menu plugin is +// desktop-only, so the whole module is skipped on mobile. +describeApi('menu', { desktopOnly: true }, () => { + it('Menu.new builds a menu with the given items', async () => { + const result = await tauri(async (api) => { + const item = await api.menu.MenuItem.new({ text: 'Item A' }) + const check = await api.menu.CheckMenuItem.new({ + text: 'Item B', + checked: true + }) + const separator = await api.menu.PredefinedMenuItem.new({ + item: 'Separator' + }) + const submenu = await api.menu.Submenu.new({ + text: 'More', + items: [await api.menu.MenuItem.new({ text: 'Nested' })] + }) + const menu = await api.menu.Menu.new({ + items: [item, check, separator, submenu] + }) + const items = await menu.items() + return { count: items.length } + }) + expect(result.count).toBe(4) + }) + + it('MenuItem text and enabled state can be read and updated', async () => { + const result = await tauri(async (api) => { + const item = await api.menu.MenuItem.new({ + text: 'Original', + enabled: true + }) + const before = await item.text() + await item.setText('Updated') + const after = await item.text() + const enabledBefore = await item.isEnabled() + await item.setEnabled(false) + const enabledAfter = await item.isEnabled() + return { before, after, enabledBefore, enabledAfter } + }) + expect(result.before).toBe('Original') + expect(result.after).toBe('Updated') + expect(result.enabledBefore).toBe(true) + expect(result.enabledAfter).toBe(false) + }) + + it('CheckMenuItem tracks its checked state', async () => { + const result = await tauri(async (api) => { + const item = await api.menu.CheckMenuItem.new({ + text: 'Toggle', + checked: false + }) + // On Windows the checked state lives in the native menu the item belongs + // to, so it must be attached to a menu to be readable. + await api.menu.Menu.new({ items: [item] }) + const before = await item.isChecked() + await item.setChecked(true) + const after = await item.isChecked() + return { before, after } + }) + expect(result.before).toBe(false) + expect(result.after).toBe(true) + }) + + it('append and removeAt mutate the item list', async () => { + const result = await tauri(async (api) => { + const menu = await api.menu.Menu.new({ + items: [await api.menu.MenuItem.new({ text: 'First' })] + }) + await menu.append(await api.menu.MenuItem.new({ text: 'Second' })) + const afterAppend = (await menu.items()).length + await menu.removeAt(0) + const afterRemove = (await menu.items()).length + return { afterAppend, afterRemove } + }) + expect(result.afterAppend).toBe(2) + expect(result.afterRemove).toBe(1) + }) +}) diff --git a/packages/api-e2e/test/specs/path.spec.ts b/packages/api-e2e/test/specs/path.spec.ts new file mode 100644 index 0000000..a0e8cbf --- /dev/null +++ b/packages/api-e2e/test/specs/path.spec.ts @@ -0,0 +1,62 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import nodePath from 'node:path' +import { expect } from '@wdio/globals' +import { tauri, describeApi, platform } from '../helpers/index.js' + +// Node's path module for the platform the *app* runs on. That is the host for +// the desktop suite, but the mobile suites drive an Android/iOS app (POSIX +// paths) from whatever OS hosts the emulator/simulator. +const path = platform === 'win32' ? nodePath.win32 : nodePath.posix + +describeApi('path', () => { + it('sep and delimiter match the target platform', async () => { + const separators = await tauri((api) => ({ + sep: api.path.sep(), + delimiter: api.path.delimiter() + })) + expect(separators.sep).toBe(path.sep) + expect(separators.delimiter).toBe(path.delimiter) + }) + + it('join, dirname, basename and extname behave like the platform', async () => { + const result = await tauri(async (api) => ({ + joined: await api.path.join('a', 'b', 'c.txt'), + dir: await api.path.dirname(await api.path.join('a', 'b', 'c.txt')), + base: await api.path.basename(await api.path.join('a', 'b', 'c.txt')), + ext: await api.path.extname(await api.path.join('a', 'b', 'c.txt')) + })) + expect(result.joined).toBe(path.join('a', 'b', 'c.txt')) + expect(result.base).toBe('c.txt') + expect(result.ext).toBe('txt') + expect(result.dir.endsWith(path.join('a', 'b'))).toBe(true) + }) + + it('normalize and isAbsolute work', async () => { + const result = await tauri(async (api) => ({ + normalized: await api.path.normalize('a/b/../c'), + absoluteIsAbsolute: await api.path.isAbsolute( + await api.path.resolve('x') + ), + relativeIsAbsolute: await api.path.isAbsolute('a/b') + })) + expect(result.normalized).toContain('c') + expect(result.absoluteIsAbsolute).toBe(true) + expect(result.relativeIsAbsolute).toBe(false) + }) + + it('app directories are absolute and scoped to the identifier', async () => { + const dirs = await tauri(async (api) => ({ + appData: await api.path.appDataDir(), + appConfig: await api.path.appConfigDir(), + home: await api.path.homeDir(), + temp: await api.path.tempDir() + })) + expect(dirs.appData).toContain('com.tauri.api') + expect(dirs.appConfig.length).toBeGreaterThan(0) + expect(dirs.home.length).toBeGreaterThan(0) + expect(dirs.temp.length).toBeGreaterThan(0) + }) +}) diff --git a/packages/api-e2e/test/specs/tray.spec.ts b/packages/api-e2e/test/specs/tray.spec.ts new file mode 100644 index 0000000..d99918c --- /dev/null +++ b/packages/api-e2e/test/specs/tray.spec.ts @@ -0,0 +1,43 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, describeApi } from '../helpers/index.js' + +// Tray commands are allowed by default, but a tray host is not always available +// in headless environments — set `E2E_SKIP=tray` there. The tray plugin is +// desktop-only, so the whole module is skipped on mobile. +describeApi('tray', { desktopOnly: true }, () => { + it('creates a tray icon, looks it up and removes it', async () => { + const created = await tauri(async (api) => { + const icon = await api.app.defaultWindowIcon() + const tray = await api.tray.TrayIcon.new({ + id: 'e2e-tray', + icon: icon ?? undefined, + tooltip: 'e2e tray' + }) + return tray.id + }) + expect(created).toBe('e2e-tray') + + const foundById = await tauri(async (api) => { + const tray = await api.tray.TrayIcon.getById('e2e-tray') + return tray !== null + }) + expect(foundById).toBe(true) + + await tauri(async (api) => { + const tray = await api.tray.TrayIcon.getById('e2e-tray') + await tray?.setVisible(true) + return null + }) + + const removed = await tauri(async (api) => { + await api.tray.TrayIcon.removeById('e2e-tray') + const tray = await api.tray.TrayIcon.getById('e2e-tray') + return tray === null + }) + expect(removed).toBe(true) + }) +}) diff --git a/packages/api-e2e/test/specs/webview.spec.ts b/packages/api-e2e/test/specs/webview.spec.ts new file mode 100644 index 0000000..c42a99a --- /dev/null +++ b/packages/api-e2e/test/specs/webview.spec.ts @@ -0,0 +1,44 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, describeApi, itDesktop } from '../helpers/index.js' + +describeApi('webview', () => { + it('getCurrentWebview reports the main label', async () => { + expect(await tauri((api) => api.webview.getCurrentWebview().label)).toBe( + 'main' + ) + }) + + it('getAllWebviews includes the main webview', async () => { + const labels = await tauri(async (api) => + (await api.webview.getAllWebviews()).map((w) => w.label) + ) + expect(labels).toContain('main') + }) + + // `webview_position`/`webview_size` are desktop-only commands: on mobile the + // webview always fills the window. + itDesktop('exposes a physical position and size', async () => { + const rect = await tauri(async (api) => { + const wv = api.webview.getCurrentWebview() + const pos = await wv.position() + const size = await wv.size() + return { x: pos.x, y: pos.y, width: size.width, height: size.height } + }) + expect(rect.width).toBeGreaterThan(0) + expect(rect.height).toBeGreaterThan(0) + }) + + itDesktop('setZoom adjusts and restores the zoom level', async () => { + // Requires `core:webview:allow-set-webview-zoom` in the run-app capability. + await tauri(async (api) => { + const wv = api.webview.getCurrentWebview() + await wv.setZoom(1.5) + await wv.setZoom(1) + return null + }) + }) +}) diff --git a/packages/api-e2e/test/specs/webviewWindow.spec.ts b/packages/api-e2e/test/specs/webviewWindow.spec.ts new file mode 100644 index 0000000..37a1ac3 --- /dev/null +++ b/packages/api-e2e/test/specs/webviewWindow.spec.ts @@ -0,0 +1,64 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { tauri, eventually, describeApi, itDesktop } from '../helpers/index.js' + +describeApi('webviewWindow', () => { + it('getCurrentWebviewWindow reports the main label', async () => { + expect( + await tauri((api) => api.webviewWindow.getCurrentWebviewWindow().label) + ).toBe('main') + }) + + // On mobile a new window is a new Android activity / iOS scene that takes + // over the foreground (and the automation session's webview context), so + // this is only exercised on desktop. + itDesktop('creates, finds and closes a new webview window', async () => { + // The label must match `main-*` to inherit the run-app capability. + const created = await tauri( + (api) => + new Promise((resolve, reject) => { + const w = new api.webviewWindow.WebviewWindow('main-e2e', { + width: 400, + height: 300 + }) + w.once('tauri://created', () => resolve(true)).catch(reject) + w.once('tauri://error', (event) => + reject( + new Error(`window creation failed: ${String(event.payload)}`) + ) + ).catch(reject) + setTimeout(() => reject(new Error('window creation timed out')), 8000) + }) + ) + expect(created).toBe(true) + + const found = await tauri(async (api) => { + const byLabel = + await api.webviewWindow.WebviewWindow.getByLabel('main-e2e') + const all = (await api.webviewWindow.getAllWebviewWindows()).map( + (w) => w.label + ) + return { exists: byLabel !== null, all } + }) + expect(found.exists).toBe(true) + expect(found.all).toContain('main-e2e') + + await tauri(async (api) => { + const w = await api.webviewWindow.WebviewWindow.getByLabel('main-e2e') + await w?.close() + return null + }) + + await eventually(async () => { + const labels = await tauri(async (api) => + (await api.webviewWindow.getAllWebviewWindows()).map((w) => w.label) + ) + if (labels.includes('main-e2e')) { + throw new Error('window is still present after close') + } + }) + }) +}) diff --git a/packages/api-e2e/test/specs/window.spec.ts b/packages/api-e2e/test/specs/window.spec.ts new file mode 100644 index 0000000..937c67c --- /dev/null +++ b/packages/api-e2e/test/specs/window.spec.ts @@ -0,0 +1,295 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { expect } from '@wdio/globals' +import { + tauri, + eventually, + describeApi, + itWm, + itDesktop, + itOn +} from '../helpers/index.js' + +describeApi('window', () => { + it('getCurrentWindow reports the main label', async () => { + expect(await tauri((api) => api.window.getCurrentWindow().label)).toBe( + 'main' + ) + }) + + it('getAllWindows includes the main window', async () => { + const labels = await tauri(async (api) => + (await api.window.getAllWindows()).map((w) => w.label) + ) + expect(labels).toContain('main') + }) + + // Mobile windows have no title bar: `title()` is always empty and + // `setTitle` a no-op there. + itDesktop('title can be read and set', async () => { + const original = await tauri((api) => api.window.getCurrentWindow().title()) + await tauri((api) => api.window.getCurrentWindow().setTitle('e2e title')) + expect(await tauri((api) => api.window.getCurrentWindow().title())).toBe( + 'e2e title' + ) + // restore + await tauri( + (api, title) => api.window.getCurrentWindow().setTitle(title), + original + ) + }) + + it('reports a positive scale factor and sizes', async () => { + const metrics = await tauri(async (api) => { + const w = api.window.getCurrentWindow() + const scale = await w.scaleFactor() + const inner = await w.innerSize() + const outer = await w.outerSize() + return { + scale, + inner: { width: inner.width, height: inner.height }, + outer: { width: outer.width, height: outer.height } + } + }) + expect(metrics.scale).toBeGreaterThan(0) + expect(metrics.inner.width).toBeGreaterThan(0) + expect(metrics.inner.height).toBeGreaterThan(0) + expect(metrics.outer.width).toBeGreaterThanOrEqual(metrics.inner.width) + }) + + itOn( + 'android', + 'activityName reports the activity hosting the window', + async () => { + const name = await tauri((api) => + api.window.getCurrentWindow().activityName() + ) + expect(name).toMatch(/MainActivity/) + } + ) + + // The window is the whole screen on mobile: `set_size` is a no-op. + itDesktop('setSize resizes the window', async () => { + const scale = await tauri((api) => + api.window.getCurrentWindow().scaleFactor() + ) + const original = await tauri(async (api) => { + const size = await api.window.getCurrentWindow().innerSize() + return { width: size.width, height: size.height } + }) + + await tauri( + (api, width, height) => + api.window + .getCurrentWindow() + .setSize(new api.dpi.LogicalSize(width, height)), + 600, + 400 + ) + + await eventually(async () => { + const inner = await tauri(async (api) => { + const size = await api.window.getCurrentWindow().innerSize() + return { width: size.width, height: size.height } + }) + const tolerance = Math.ceil(scale) * 4 + if (Math.abs(inner.width - 600 * scale) > tolerance) { + throw new Error(`width ${inner.width} not near ${600 * scale}`) + } + if (Math.abs(inner.height - 400 * scale) > tolerance) { + throw new Error(`height ${inner.height} not near ${400 * scale}`) + } + }) + + // restore the original physical size + await tauri( + (api, width, height) => + api.window + .getCurrentWindow() + .setSize(new api.dpi.PhysicalSize(width, height)), + original.width, + original.height + ) + }) + + // Mobile windows are never resizable (`is_resizable` is always `false`). + itDesktop('setResizable toggles resizability', async () => { + await tauri((api) => api.window.getCurrentWindow().setResizable(false)) + expect( + await tauri((api) => api.window.getCurrentWindow().isResizable()) + ).toBe(false) + await tauri((api) => api.window.getCurrentWindow().setResizable(true)) + expect( + await tauri((api) => api.window.getCurrentWindow().isResizable()) + ).toBe(true) + }) + + // Desktop-only commands (`#[cfg(desktop)]` in the window plugin). + itDesktop('assorted setters resolve without throwing', async () => { + await tauri(async (api) => { + const w = api.window.getCurrentWindow() + await w.setAlwaysOnTop(true) + await w.setAlwaysOnTop(false) + await w.setShadow(true) + await w.setContentProtected(false) + await w.setCursorVisible(true) + await w.requestUserAttention(null) + return null + }) + }) + + itWm('setPosition moves the window', async () => { + const original = await tauri(async (api) => { + const pos = await api.window.getCurrentWindow().outerPosition() + return { x: pos.x, y: pos.y } + }) + + await tauri( + (api, x, y) => + api.window + .getCurrentWindow() + .setPosition(new api.dpi.PhysicalPosition(x, y)), + original.x + 40, + original.y + 40 + ) + + await eventually(async () => { + const pos = await tauri(async (api) => { + const p = await api.window.getCurrentWindow().outerPosition() + return { x: p.x, y: p.y } + }) + // Window managers can nudge the final position; allow a generous tolerance. + if ( + Math.abs(pos.x - (original.x + 40)) > 30 + || Math.abs(pos.y - (original.y + 40)) > 30 + ) { + throw new Error( + `position ${pos.x},${pos.y} not near ${original.x + 40},${original.y + 40}` + ) + } + }) + + await tauri( + (api, x, y) => + api.window + .getCurrentWindow() + .setPosition(new api.dpi.PhysicalPosition(x, y)), + original.x, + original.y + ) + }) + + itWm('minimize and unminimize toggle the minimized state', async () => { + await tauri((api) => api.window.getCurrentWindow().minimize()) + await eventually(async () => { + if ( + !(await tauri((api) => api.window.getCurrentWindow().isMinimized())) + ) { + throw new Error('window is not minimized') + } + }) + await tauri((api) => api.window.getCurrentWindow().unminimize()) + await eventually(async () => { + if (await tauri((api) => api.window.getCurrentWindow().isMinimized())) { + throw new Error('window is still minimized') + } + }) + }) + + itWm('maximize and unmaximize toggle the maximized state', async () => { + await tauri((api) => api.window.getCurrentWindow().maximize()) + await eventually(async () => { + if ( + !(await tauri((api) => api.window.getCurrentWindow().isMaximized())) + ) { + throw new Error('window is not maximized') + } + }) + await tauri((api) => api.window.getCurrentWindow().unmaximize()) + await eventually(async () => { + if (await tauri((api) => api.window.getCurrentWindow().isMaximized())) { + throw new Error('window is still maximized') + } + }) + }) + + itWm('setFullscreen toggles fullscreen', async () => { + await tauri((api) => api.window.getCurrentWindow().setFullscreen(true)) + await eventually(async () => { + if ( + !(await tauri((api) => api.window.getCurrentWindow().isFullscreen())) + ) { + throw new Error('window is not fullscreen') + } + }) + await tauri((api) => api.window.getCurrentWindow().setFullscreen(false)) + await eventually(async () => { + if (await tauri((api) => api.window.getCurrentWindow().isFullscreen())) { + throw new Error('window is still fullscreen') + } + }) + }) + + itWm('setFullscreenOnMonitor goes fullscreen on that monitor', async () => { + // Use the monitor the window is on, so the assertion holds on single-monitor CI. + const origin = await tauri(async (api) => { + const monitor = await api.window.currentMonitor() + return monitor ? { x: monitor.position.x, y: monitor.position.y } : null + }) + expect(origin).not.toBeNull() + + await tauri( + (api, origin) => + api.window + .getCurrentWindow() + .setFullscreenOnMonitor( + new api.dpi.PhysicalPosition(origin.x, origin.y) + ), + origin! + ) + await eventually(async () => { + const state = await tauri(async (api) => { + const w = api.window.getCurrentWindow() + const position = await w.outerPosition() + return { + fullscreen: await w.isFullscreen(), + x: position.x, + y: position.y + } + }) + if (!state.fullscreen) { + throw new Error('window is not fullscreen') + } + // a fullscreen window sits at its monitor's origin + if (state.x !== origin!.x || state.y !== origin!.y) { + throw new Error( + `window is at (${state.x}, ${state.y}), expected (${origin!.x}, ${origin!.y})` + ) + } + }) + + await tauri((api) => api.window.getCurrentWindow().setFullscreen(false)) + await eventually(async () => { + if (await tauri((api) => api.window.getCurrentWindow().isFullscreen())) { + throw new Error('window is still fullscreen') + } + }) + }) + + itWm('hide and show toggle visibility', async () => { + await tauri((api) => api.window.getCurrentWindow().hide()) + await eventually(async () => { + if (await tauri((api) => api.window.getCurrentWindow().isVisible())) { + throw new Error('window is still visible') + } + }) + await tauri((api) => api.window.getCurrentWindow().show()) + await eventually(async () => { + if (!(await tauri((api) => api.window.getCurrentWindow().isVisible()))) { + throw new Error('window is not visible') + } + }) + }) +}) diff --git a/packages/api-e2e/test/types/global.d.ts b/packages/api-e2e/test/types/global.d.ts new file mode 100644 index 0000000..9438475 --- /dev/null +++ b/packages/api-e2e/test/types/global.d.ts @@ -0,0 +1,17 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// The examples/api app is built with `withGlobalTauri: true`, so the whole +// `@tauri-apps/api` surface is available on `window.__TAURI__` inside the webview. +// This mirrors that for the functions we serialize and run in the page. + +import type * as TauriApi from '@tauri-apps/api' + +declare global { + interface Window { + __TAURI__: typeof TauriApi + } +} + +export {} diff --git a/packages/api-e2e/tsconfig.json b/packages/api-e2e/tsconfig.json new file mode 100644 index 0000000..65f99f9 --- /dev/null +++ b/packages/api-e2e/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + // The specs run through tsx (esbuild, pulled in by @wdio/cli), and + // @tauri-apps/api's published type definitions use extensionless relative + // imports, so bundler-style resolution is what matches the runtime and + // resolves the api types. + "moduleResolution": "Bundler", + "lib": ["ES2023", "DOM"], + "types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"], + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["wdio.*.ts", "test/**/*.ts"] +} diff --git a/packages/api-e2e/wdio.android.conf.ts b/packages/api-e2e/wdio.android.conf.ts new file mode 100644 index 0000000..34747cb --- /dev/null +++ b/packages/api-e2e/wdio.android.conf.ts @@ -0,0 +1,10 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// `@tauri-apps/api` e2e suite against the examples/api app on an Android +// device or emulator, through Appium's UiAutomator2 driver. See wdio.mobile.ts. + +import { mobileConfig } from './wdio.mobile.js' + +export const config = mobileConfig('android') diff --git a/packages/api-e2e/wdio.conf.ts b/packages/api-e2e/wdio.conf.ts new file mode 100644 index 0000000..27c4254 --- /dev/null +++ b/packages/api-e2e/wdio.conf.ts @@ -0,0 +1,296 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import path from 'node:path' +import fs from 'node:fs' +import { spawn, spawnSync, type ChildProcess } from 'node:child_process' +import { fileURLToPath } from 'node:url' +import { waitTauriDriverReady } from '@crabnebula/tauri-driver' + +const dirname = path.dirname(fileURLToPath(import.meta.url)) +const repoRoot = path.resolve(dirname, '..', '..') +const appDir = path.join(repoRoot, 'examples', 'api') +const targetDir = process.env.CARGO_TARGET_DIR ?? path.join(repoRoot, 'target') + +// macOS has no native WebDriver for WKWebView, so the CrabNebula Webdriver +// (backed by tauri-plugin-automation + the test-runner-backend) is required there. +// It can be opted into on the other platforms via E2E_CN_WEBDRIVER for parity. +const useCrabNebulaWebdriver = + process.platform === 'darwin' || !!process.env.E2E_CN_WEBDRIVER + +// Path passed to the driver as `tauri:options.application`. +const application = + process.env.E2E_APP_PATH + ?? (process.platform === 'darwin' + ? path.join(targetDir, 'debug', 'bundle', 'macos', 'Tauri API.app') + : process.platform === 'win32' + ? path.join(targetDir, 'debug', 'api.exe') + : path.join(targetDir, 'debug', 'api')) + +let tauriDriver: ChildProcess | undefined +let killedTauriDriver = false +let testRunnerBackend: ChildProcess | undefined +let killedTestRunnerBackend = false + +export const config: WebdriverIO.Config = { + hostname: '127.0.0.1', + port: 4444, + specs: ['./test/specs/**/*.spec.ts'], + // The driver spawns a single app instance and speaks WebDriver to it, so the + // suite must run serially. + maxInstances: 1, + capabilities: [ + { + maxInstances: 1, + // `tauri:options` is understood by tauri-driver, not by the WebdriverIO types. + 'tauri:options': { application } + } as unknown as WebdriverIO.Capabilities + ], + reporters: ['spec'], + framework: 'mocha', + mochaOpts: { + ui: 'bdd', + timeout: 120000 + }, + connectionRetryCount: 0, + specFileRetries: Number(process.env.E2E_SPEC_RETRIES ?? 0), + + onPrepare: async () => { + // The example resolves `@tauri-apps/api` from `packages/api/dist`, and its + // frontend build (vite) needs it too. Fail early with a clear message. + if ( + !fs.existsSync(path.join(repoRoot, 'packages', 'api', 'dist', 'index.js')) + ) { + throw new Error( + 'packages/api/dist is missing — run `pnpm build:api` at the repo root before the e2e suite.' + ) + } + + if (!process.env.E2E_SKIP_BUILD && !process.env.E2E_APP_PATH) { + // `examples/api`'s `tauri` script is `node ../../packages/cli/tauri.js`, + // which requires the native CLI to be built (`pnpm build:cli`). + // + // The override config does two things for this (debug, test-only) build: + // - enables the example's off-by-default `automation` feature, which + // registers tauri-plugin-automation; + // - disables `removeUnusedCommands`, which the example otherwise sets. + // It strips command handlers that no static capability grants, and the + // plugin adds its `resolve` command through a *runtime* capability, so + // it would be stripped — breaking the CrabNebula Webdriver on macOS. + // Passed as an appDir-relative path to sidestep shell quoting. + const overrideConfig = path.relative( + appDir, + path.join(dirname, 'tauri.e2e.conf.json') + ) + const buildArgs = [ + 'tauri', + 'build', + '--debug', + '--config', + overrideConfig, + ...(process.platform === 'darwin' + ? ['--bundles', 'app'] // the .app bundle is needed for tauri:options + : ['--no-bundle']) + ] + const build = spawnSync('pnpm', buildArgs, { + cwd: appDir, + stdio: 'inherit', + shell: true + }) + if (build.status !== 0) { + throw new Error( + `\`pnpm ${buildArgs.join(' ')}\` failed with status ${build.status}` + ) + } + } + + if (!fs.existsSync(application)) { + throw new Error( + `app not found at ${application} — build it (unset E2E_SKIP_BUILD) or point E2E_APP_PATH at an existing build.` + ) + } + + if (useCrabNebulaWebdriver) { + if (!process.env.CN_API_KEY) { + throw new Error( + 'CN_API_KEY is required for the CrabNebula Webdriver (mandatory on macOS, or when E2E_CN_WEBDRIVER=1).' + ) + } + testRunnerBackend = spawn('pnpm', ['exec', 'test-runner-backend'], { + cwd: dirname, + stdio: 'inherit', + shell: true, + // Lead a new process group so the whole tree (sh -> pnpm -> + // test-runner-backend) can be torn down together. See killProcessTree. + detached: process.platform !== 'win32' + }) + testRunnerBackend.on('error', (error) => { + console.error('test-runner-backend error:', error) + process.exit(1) + }) + testRunnerBackend.on('exit', (code) => { + if (!killedTestRunnerBackend) { + console.error('test-runner-backend exited with code:', code) + process.exit(1) + } + }) + const { waitTestRunnerBackendReady } = + await import('@crabnebula/test-runner-backend') + await waitTestRunnerBackendReady() + process.env.REMOTE_WEBDRIVER_URL = 'http://127.0.0.1:3000' + } + }, + + // A fresh tauri-driver (and therefore a fresh app instance) per spec file, + // so each module's suite runs in isolation. + beforeSession: async () => { + const args = ['exec', 'tauri-driver'] + if (process.env.E2E_NATIVE_DRIVER) { + args.push('--native-driver', process.env.E2E_NATIVE_DRIVER) + } + // Reset before each (re)spawn so the `exit` handler below still treats an + // unexpected driver crash as fatal on retried spec files. + killedTauriDriver = false + tauriDriver = spawn('pnpm', args, { + cwd: dirname, + stdio: [null, process.stdout, process.stderr], + shell: true, + // Lead a new process group so the whole tree (sh -> pnpm -> tauri-driver + // -> native webdriver) can be torn down together. See killProcessTree. + detached: process.platform !== 'win32' + }) + tauriDriver.on('error', (error) => { + console.error('tauri-driver error:', error) + process.exit(1) + }) + tauriDriver.on('exit', (code) => { + if (!killedTauriDriver) { + console.error('tauri-driver exited with code:', code) + process.exit(1) + } + }) + await waitTauriDriverReady() + }, + + // The session is created as soon as the app's window exists, which can be + // before the webview has navigated to the app's page: WebView2 on a cold start + // (the first launches on a fresh Windows runner) still shows `about:blank` + // for a few seconds, so the first spec's scripts ran in a page without + // `window.__TAURI__`. Every spec goes through that global, so block until + // it exists. + before: async (_capabilities, _specs, browser) => { + await browser.waitUntil( + async () => { + try { + return (await browser.executeAsync( + 'var done = arguments[arguments.length - 1]; done(typeof window.__TAURI__ !== "undefined");' + )) as boolean + } catch { + // A command issued mid-navigation can fail on a stale execution + // context; that just means "not ready yet". + return false + } + }, + { + timeout: 30_000, + interval: 250, + timeoutMsg: + 'window.__TAURI__ never became available — the app did not load its page.' + } + ) + }, + + // Awaited so the driver (and its port) is fully gone before the next spec's + // beforeSession spawns a new one on the same port. + afterSession: async () => { + await closeTauriDriver() + }, + + onComplete: () => { + closeAll() + } +} + +/** + * Kills a shell-spawned child and everything it started. `child.kill()` only + * signals the `sh`/`cmd` wrapper, orphaning the real process (which keeps + * holding the WebDriver port and poisons every later spec), so we take down the + * whole process group/tree instead. + */ +function killProcessTree( + child: ChildProcess | undefined, + signal: NodeJS.Signals = 'SIGTERM' +): void { + const pid = child?.pid + if (!pid || child?.exitCode !== null) return + if (process.platform === 'win32') { + spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { + stdio: 'ignore' + }) + } else { + try { + // Negative pid targets the whole process group (see `detached` above). + process.kill(-pid, signal) + } catch { + try { + child!.kill(signal) + } catch { + // already gone + } + } + } +} + +async function closeTauriDriver(): Promise { + killedTauriDriver = true + const driver = tauriDriver + tauriDriver = undefined + if (!driver || driver.exitCode !== null) return + const exited = new Promise((resolve) => + driver.once('exit', () => resolve()) + ) + killProcessTree(driver) + // Give it a moment to release the port, then escalate to SIGKILL. + let timer: ReturnType | undefined + await Promise.race([ + exited, + new Promise((resolve) => { + timer = setTimeout(() => { + killProcessTree(driver, 'SIGKILL') + resolve() + }, 5000) + }) + ]) + if (timer) clearTimeout(timer) +} + +function closeAll() { + killedTauriDriver = true + killProcessTree(tauriDriver) + tauriDriver = undefined + killedTestRunnerBackend = true + killProcessTree(testRunnerBackend) + testRunnerBackend = undefined +} + +function onShutdown(fn: () => void) { + const cleanup = () => { + try { + fn() + } finally { + process.exit() + } + } + for (const signal of [ + 'exit', + 'SIGINT', + 'SIGTERM', + 'SIGHUP', + 'SIGBREAK' + ] as const) { + process.on(signal, cleanup) + } +} + +onShutdown(closeAll) diff --git a/packages/api-e2e/wdio.ios.conf.ts b/packages/api-e2e/wdio.ios.conf.ts new file mode 100644 index 0000000..6f049cd --- /dev/null +++ b/packages/api-e2e/wdio.ios.conf.ts @@ -0,0 +1,10 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// `@tauri-apps/api` e2e suite against the examples/api app on an iOS +// simulator, through Appium's XCUITest driver. See wdio.mobile.ts. + +import { mobileConfig } from './wdio.mobile.js' + +export const config = mobileConfig('ios') diff --git a/packages/api-e2e/wdio.mobile.ts b/packages/api-e2e/wdio.mobile.ts new file mode 100644 index 0000000..2e7fc69 --- /dev/null +++ b/packages/api-e2e/wdio.mobile.ts @@ -0,0 +1,425 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// Shared WebdriverIO configuration for the mobile suites (`wdio.android.conf.ts` +// and `wdio.ios.conf.ts`). Instead of tauri-driver, the app is driven through +// Appium: the UiAutomator2 driver (Android; chromedriver attaches to the +// WebView) or the XCUITest driver (iOS simulator; WebKit remote inspector). +// Debug builds enable webview debugging on both platforms, which is what makes +// the `WEBVIEW_*` context — and `browser.executeAsync` inside it — available. + +import path from 'node:path' +import fs from 'node:fs' +import { spawnSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' + +export type MobilePlatform = 'android' | 'ios' + +const dirname = path.dirname(fileURLToPath(import.meta.url)) +const repoRoot = path.resolve(dirname, '..', '..') +const appDir = path.join(repoRoot, 'examples', 'api') +const tauriDir = path.join(appDir, 'src-tauri') + +/** `identifier` in examples/api's tauri.conf.json. */ +const appId = 'com.tauri.api' + +/** Where Appium looks for drivers; they are devDependencies of this package. */ +process.env.APPIUM_HOME ??= dirname + +export function mobileConfig(platform: MobilePlatform): WebdriverIO.Config { + const ios = platform === 'ios' ? iosTarget() : undefined + + // Path passed to the driver as `appium:app`. + const application = + process.env.E2E_APP_PATH + ?? (ios + ? path.join( + tauriDir, + 'gen', + 'apple', + 'build', + ios.outputArch, + 'Tauri API.app' + ) + : path.join( + tauriDir, + 'gen', + 'android', + 'app', + 'build', + 'outputs', + 'apk', + 'universal', + 'debug', + 'app-universal-debug.apk' + )) + + return { + specs: ['./test/specs/**/*.spec.ts'], + // One device/simulator, one app instance: the suite runs serially. + maxInstances: 1, + capabilities: [ + platform === 'android' + ? androidCapabilities(application) + : iosCapabilities(application) + ], + services: [ + [ + 'appium', + { + args: { + address: '127.0.0.1', + // Appium 3 requires insecure features to be scoped to a driver. + // chromedriver_autodownload lets the UiAutomator2 driver fetch a + // chromedriver matching the device's WebView (see E2E_CHROMEDRIVER). + allowInsecure: 'uiautomator2:chromedriver_autodownload' + }, + // First-session setup (chromedriver download, WebDriverAgent build) + // can be slow, hence the generous timeout. + appiumStartTimeout: 120_000, + logPath: path.join(dirname, 'logs') + } + ] + ], + reporters: ['spec'], + framework: 'mocha', + mochaOpts: { + ui: 'bdd', + timeout: 120000 + }, + connectionRetryCount: 0, + // The first session boots the simulator/emulator, installs the app and, on + // iOS, compiles WebDriverAgent — well over the 120s default that the + // request to create the session would otherwise be cut at (the driver + // timeouts in the capabilities below are what actually bound it). + connectionRetryTimeout: 600_000, + specFileRetries: Number(process.env.E2E_SPEC_RETRIES ?? 0), + // Tells the specs (which run in worker processes) what the app runs on; + // `process.platform` there is the host. See `platform` in test/helpers. + runnerEnv: { E2E_PLATFORM: platform }, + + onPrepare: async () => { + if ( + !fs.existsSync( + path.join(repoRoot, 'packages', 'api', 'dist', 'index.js') + ) + ) { + throw new Error( + 'packages/api/dist is missing — run `pnpm build:api` at the repo root before the e2e suite.' + ) + } + + if (!process.env.E2E_SKIP_BUILD && !process.env.E2E_APP_PATH) { + // `examples/api`'s `tauri` script is `node ../../packages/cli/tauri.js`, + // which requires the native CLI to be built (`pnpm build:cli`). + // + // The generated Android Studio / Xcode project lives in `src-tauri/gen` + // (gitignored), so it is initialized on a fresh checkout. The build + // installs the Rust target it needs itself, so init skips that. + if ( + !fs.existsSync(path.join(tauriDir, 'gen', ios ? 'apple' : 'android')) + ) { + tauriCli([platform, 'init', '--ci', '--skip-targets-install']) + } + // A debug build, so wry turns on webview debugging (Android + // `setWebContentsDebuggingEnabled`, iOS `isInspectable`), which is what + // lets Appium reach the page. Only the target that the device/emulator + // actually runs is compiled. + tauriCli( + ios + ? [ + 'ios', + 'build', + '--debug', + '--target', + ios.name, + // Simulator builds are not code signed (see + // `additionalWebviewBundleIds` in `iosCapabilities`). + '--no-sign' + ] + : [ + 'android', + 'build', + '--debug', + '--apk', + '--target', + androidTarget() + ] + ) + } + + if (!fs.existsSync(application)) { + throw new Error( + `app not found at ${application} — build it (unset E2E_SKIP_BUILD) or point E2E_APP_PATH at an existing build.` + ) + } + }, + + // The session starts in the native (`NATIVE_APP`) context. Every spec goes + // through `window.__TAURI__`, so switch to the app's webview as soon as it + // is attachable and then block until the page has loaded (as the desktop + // config does). + before: async (_capabilities, _specs, browser: WebdriverIO.Browser) => { + let webview: string | undefined + await browser.waitUntil( + async () => { + const contexts = (await browser.getAppiumContexts()) + // Detailed objects are only returned with `appium:fullContextList`. + .map((context) => + typeof context === 'string' ? context : context.id + ) + // Android names the context after the package, and lists every + // debuggable WebView on the device (other apps included), so match + // ours exactly. iOS names it `WEBVIEW_.` and only lists the + // app under test's webviews, so the first one is it. + webview = + contexts.find((name) => name === `WEBVIEW_${appId}`) + ?? (platform === 'ios' + ? contexts.find((name) => name.startsWith('WEBVIEW_')) + : undefined) + return webview !== undefined + }, + { + // Covers a cold app start plus, on Android, the on-demand chromedriver + // download for the first session. + timeout: 120_000, + interval: 1000, + timeoutMsg: + 'no WEBVIEW context appeared — is the app a debug build (webview debugging enabled)?' + } + ) + await browser.switchAppiumContext(webview!) + // The specs run the page through `executeAsync`, and the XCUITest driver + // starts with a script timeout of 0 (every async script times out at + // once) rather than the 30s the other drivers default to. + await browser.setTimeout({ script: 30_000 }) + + await browser.waitUntil( + async () => { + try { + return (await browser.executeAsync( + 'var done = arguments[arguments.length - 1]; done(typeof window.__TAURI__ !== "undefined");' + )) as boolean + } catch { + // A command issued mid-navigation can fail on a stale execution + // context; that just means "not ready yet". + return false + } + }, + { + timeout: 30_000, + interval: 250, + timeoutMsg: + 'window.__TAURI__ never became available — the app did not load its page.' + } + ) + } + } +} + +/** Runs `pnpm tauri ` in examples/api, failing loudly. */ +function tauriCli(args: string[]): void { + const result = spawnSync('pnpm', ['tauri', ...args], { + cwd: appDir, + stdio: 'inherit', + shell: true + }) + if (result.status !== 0) { + throw new Error( + `\`pnpm tauri ${args.join(' ')}\` failed with status ${result.status}` + ) + } +} + +// --- Android ----------------------------------------------------------------- + +function androidCapabilities(app: string): WebdriverIO.Capabilities { + return { + platformName: 'Android', + 'appium:automationName': 'UiAutomator2', + 'appium:app': app, + 'appium:appPackage': appId, + 'appium:appActivity': '.MainActivity', + // Which device/emulator to use; Appium picks the first connected one + // otherwise. E2E_ANDROID_AVD instead boots that AVD. + ...(process.env.E2E_ANDROID_DEVICE + ? { 'appium:udid': process.env.E2E_ANDROID_DEVICE } + : {}), + ...(process.env.E2E_ANDROID_AVD + ? { 'appium:avd': process.env.E2E_ANDROID_AVD } + : {}), + // chromedriver must match the WebView's Chrome version: either a specific + // binary, or one Appium downloads on demand. + ...(process.env.E2E_CHROMEDRIVER + ? { 'appium:chromedriverExecutable': process.env.E2E_CHROMEDRIVER } + : { 'appium:chromedriverAutodownload': true }), + 'appium:autoGrantPermissions': true, + // Every build keeps the same version, so without this Appium sees the app + // as already installed and runs the previous build instead of this one. + 'appium:enforceAppInstall': true, + // Emulators in CI are slow; give the UiAutomator2 server and adb room. + 'appium:uiautomator2ServerInstallTimeout': 120_000, + 'appium:uiautomator2ServerLaunchTimeout': 120_000, + 'appium:adbExecTimeout': 60_000, + 'appium:newCommandTimeout': 300 + } as WebdriverIO.Capabilities +} + +const androidTargets: Record = { + // `ro.product.cpu.abi` -> `tauri android build --target` + 'arm64-v8a': 'aarch64', + 'armeabi-v7a': 'armv7', + x86_64: 'x86_64', + x86: 'i686' +} + +/** + * The Rust target to build the APK for: `E2E_ANDROID_TARGET`, else the ABI of + * the connected device/emulator (via adb), else the host's (emulators run the + * host architecture). + */ +function androidTarget(): string { + if (process.env.E2E_ANDROID_TARGET) { + return process.env.E2E_ANDROID_TARGET + } + const sdk = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT + const adb = sdk ? path.join(sdk, 'platform-tools', 'adb') : 'adb' + const abi = spawnSync( + adb, + [ + ...(process.env.E2E_ANDROID_DEVICE + ? ['-s', process.env.E2E_ANDROID_DEVICE] + : []), + 'shell', + 'getprop', + 'ro.product.cpu.abi' + ], + { encoding: 'utf8', timeout: 10_000 } + ) + const detected = + abi.status === 0 ? androidTargets[abi.stdout.trim()] : undefined + return detected ?? (process.arch === 'arm64' ? 'aarch64' : 'x86_64') +} + +// --- iOS --------------------------------------------------------------------- + +function iosCapabilities(app: string): WebdriverIO.Capabilities { + return { + platformName: 'iOS', + 'appium:automationName': 'XCUITest', + 'appium:app': app, + 'appium:bundleId': appId, + // The driver looks the app up in the simulator's Web Inspector listing by + // bundle identifier. The inspector identifies an app by the + // `application-identifier` entitlement Xcode embeds when it code signs a + // simulator build (`__TEXT,__entitlements`); `--no-sign` skips signing + // altogether (`CODE_SIGNING_ALLOWED=NO`), so the entitlement is missing and + // the inspector falls back to `process-`. Match that too. + 'appium:additionalWebviewBundleIds': [ + `process-${path.basename(app, '.app')}` + ], + 'appium:udid': iosSimulator(), + // As on Android: the version never changes between builds. + 'appium:enforceAppInstall': true, + // No Simulator.app window. Besides not needing one, the driver otherwise + // shuts a simulator that is booted without a visible UI down to relaunch it + // with one, on every session — and that shutdown regularly outlasts the + // driver's 15s limit on it. + 'appium:isHeadless': true, + // A prebuilt WebDriverAgent (Appium's release build for the simulator, see + // `appium driver run xcuitest download-wda -- --kind sim`) is installed and + // launched as is, instead of being compiled with xcodebuild on the first + // session, which takes minutes on a CI runner. + ...(process.env.E2E_IOS_WDA + ? { + 'appium:usePreinstalledWDA': true, + 'appium:prebuiltWDAPath': process.env.E2E_IOS_WDA + } + : {}), + // Without one, WebDriverAgent is compiled on the first session, which takes + // minutes on a CI runner. + 'appium:wdaLaunchTimeout': 240_000, + 'appium:wdaStartupRetries': 3, + 'appium:simulatorStartupTimeout': 240_000, + 'appium:newCommandTimeout': 300 + } as WebdriverIO.Capabilities +} + +/** + * The Rust target to build the simulator app for (`E2E_IOS_TARGET`, else the + * host's architecture) and the directory the CLI exports it to. + */ +function iosTarget(): { name: string; outputArch: string } { + const name = + process.env.E2E_IOS_TARGET + ?? (process.arch === 'arm64' ? 'aarch64-sim' : 'x86_64') + // `tauri ios build` writes to gen/apple/build// (cargo-mobile2's + // `arch` for the target). + const outputArch = name === 'aarch64-sim' ? 'arm64-sim' : name + return { name, outputArch } +} + +interface SimctlDevice { + name: string + udid: string + state: string + isAvailable: boolean + deviceTypeIdentifier?: string +} + +/** + * UDID of the simulator to run on: `E2E_IOS_DEVICE` (a UDID or device name), + * else an already-booted iPhone, else the iPhone on the newest installed + * runtime. Resolved through `simctl` so nothing has to be hardcoded per Xcode + * version. + */ +function iosSimulator(): string { + const requested = process.env.E2E_IOS_DEVICE + if (requested && /^[0-9A-F-]{36}$/i.test(requested)) { + return requested + } + const list = spawnSync( + 'xcrun', + ['simctl', 'list', 'devices', 'available', '--json'], + { encoding: 'utf8' } + ) + if (list.status !== 0) { + throw new Error( + `\`xcrun simctl list\` failed — is Xcode installed?\n${list.stderr}` + ) + } + const runtimes = JSON.parse(list.stdout).devices as Record< + string, + SimctlDevice[] + > + const iphones = Object.entries(runtimes) + // e.g. `com.apple.CoreSimulator.SimRuntime.iOS-18-2` + .filter(([runtime]) => runtime.includes('.iOS-')) + .sort(([a], [b]) => runtimeVersion(b) - runtimeVersion(a)) + .flatMap(([, devices]) => + devices.filter( + (device) => + device.isAvailable + && (device.deviceTypeIdentifier ?? device.name).includes('iPhone') + ) + ) + const device = requested + ? iphones.find((candidate) => candidate.name === requested) + : (iphones.find((candidate) => candidate.state === 'Booted') ?? iphones[0]) + if (!device) { + throw new Error( + requested + ? `no available iPhone simulator named "${requested}" (E2E_IOS_DEVICE)` + : 'no available iPhone simulator found — install an iOS runtime in Xcode.' + ) + } + return device.udid +} + +function runtimeVersion(runtime: string): number { + const [major = '0', minor = '0'] = (runtime.split('.iOS-')[1] ?? '').split( + '-' + ) + return Number(major) * 100 + Number(minor) +} diff --git a/packages/api/.gitignore b/packages/api/.gitignore new file mode 100644 index 0000000..849e0ce --- /dev/null +++ b/packages/api/.gitignore @@ -0,0 +1,2 @@ +# Build output +/dist/ diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md new file mode 100644 index 0000000..1478871 --- /dev/null +++ b/packages/api/CHANGELOG.md @@ -0,0 +1,1056 @@ +# Changelog + +## \[2.11.1] + +### Enhancements + +- [`916782601`](https://www.github.com/tauri-apps/tauri/commit/9167826011cc3d114bf12dfb301968fae479891f) ([#15520](https://www.github.com/tauri-apps/tauri/pull/15520) by [@polw1](https://www.github.com/tauri-apps/tauri/../../polw1)) Document that `Monitor.size`, `Monitor.position` and `Monitor.workArea` are in physical pixels, with examples showing how to convert them to the logical pixels expected by window creation options via `toLogical(monitor.scaleFactor)`. + +## \[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 + +- Re-release of `2.10.0` with working assets. + +## \[2.10.0] + +### Dependencies + +- [`517b81e97`](https://www.github.com/tauri-apps/tauri/commit/517b81e97005d087ca5fc4538d954982fce0f4ac) ([#14876](https://www.github.com/tauri-apps/tauri/pull/14876)) Upgraded to `tauri@2.10` + +## \[2.9.1] + +### Bug Fixes + +- [`ad1dec2e2`](https://www.github.com/tauri-apps/tauri/commit/ad1dec2e2488fe5c0a004b69f1bd290dfc593bf8) ([#14464](https://www.github.com/tauri-apps/tauri/pull/14464) by [@funnydino](https://www.github.com/tauri-apps/tauri/../../funnydino)) Fix `addPluginListener` fallback added in https://github.com/tauri-apps/tauri/pull/14132 didn't work properly + +## \[2.9.0] + +### New Features + +- [`f5851ee00`](https://www.github.com/tauri-apps/tauri/commit/f5851ee00d6d1f4d560a220ca5a728fedd525092) ([#14089](https://www.github.com/tauri-apps/tauri/pull/14089)) Adds the `scrollBarStyle` option to the Webview and WebviewBuilder constructors. +- [`3397fd9bf`](https://www.github.com/tauri-apps/tauri/commit/3397fd9bfe5f6b1337110149f6c34731b8a44bb3) ([#14133](https://www.github.com/tauri-apps/tauri/pull/14133)) Added `app > onBackButtonPress` for Android back button handling. + +### Enhancements + +- [`59089723f`](https://www.github.com/tauri-apps/tauri/commit/59089723fc20d66f3f305f2008adeb279bf87462) ([#14091](https://www.github.com/tauri-apps/tauri/pull/14091)) Added a config to set a data_directory relative to the app-specific data dir in JavaScript and `tauri.conf.json`. + +### Bug Fixes + +- [`08bda64c2`](https://www.github.com/tauri-apps/tauri/commit/08bda64c25008bd45c5b58d06ff14649081a2f5d) ([#14132](https://www.github.com/tauri-apps/tauri/pull/14132)) Fix `core > addPluginListener` failing on command permission check. + +## \[2.8.0] + +### New Features + +- [`68874c68c`](https://www.github.com/tauri-apps/tauri/commit/68874c68c566638b4c21a3aa67844d1bdaeb6dab) ([#13564](https://www.github.com/tauri-apps/tauri/pull/13564) by [@robertrpf](https://www.github.com/tauri-apps/tauri/../../robertrpf)) Add window focusable attribute and set_focusable API. +- [`5110a762e`](https://www.github.com/tauri-apps/tauri/commit/5110a762e9db978a28a15400bf76e3c864da2a86) ([#13830](https://www.github.com/tauri-apps/tauri/pull/13830) by [@Sky-walkerX](https://www.github.com/tauri-apps/tauri/../../Sky-walkerX)) Added `Window::setSimpleFullscreen`. + +### Enhancements + +- [`5ba1c3faa`](https://www.github.com/tauri-apps/tauri/commit/5ba1c3faa468073512bdb5035a01f7f99720fcf0) ([#13722](https://www.github.com/tauri-apps/tauri/pull/13722) by [@s00d](https://www.github.com/tauri-apps/tauri/../../s00d)) Added icon (icon and nativeIcon) support for Submenu: + + - In the Rust API (`tauri`), you can now set an icon for submenus via the builder and dedicated methods. + - In the JS/TS API (`@tauri-apps/api`), `SubmenuOptions` now has an `icon` field, and the `Submenu` class provides `setIcon` and `setNativeIcon` methods. + - Usage examples are added to the documentation and demo app. + + This is a backwards-compatible feature. Submenus can now display icons just like regular menu items. + +## \[2.7.0] + +### New Features + +- [`232265c70`](https://www.github.com/tauri-apps/tauri/commit/232265c70e1c213bbb3f84b5541ddc07d330fce1) ([#13209](https://www.github.com/tauri-apps/tauri/pull/13209) by [@kandrelczyk](https://www.github.com/tauri-apps/tauri/../../kandrelczyk)) Added `getBundleType` to the app module. + +### Enhancements + +- [`96391467e`](https://www.github.com/tauri-apps/tauri/commit/96391467e967c1e3c6475ce75166c58a326116a3) ([#13783](https://www.github.com/tauri-apps/tauri/pull/13783) by [@JosephBrooksbank](https://www.github.com/tauri-apps/tauri/../../JosephBrooksbank)) Allow events emitted with `emit` to be handled correctly by `listen` callbacks when in a mocked environment + +### Bug Fixes + +- [`152d971bc`](https://www.github.com/tauri-apps/tauri/commit/152d971bcd6c1fdc5716f7d5417dd4df5ce7479f) ([#13744](https://www.github.com/tauri-apps/tauri/pull/13744) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Expose `unregisterCallback`, `runCallback`, `callbacks` in `mockIPC` +- [`b821796ad`](https://www.github.com/tauri-apps/tauri/commit/b821796add33cca4de72f48882684af487936d02) ([#13810](https://www.github.com/tauri-apps/tauri/pull/13810) by [@asdolo](https://www.github.com/tauri-apps/tauri/../../asdolo)) Add missing `trafficLightPosition` TypeScript type definition + +## \[2.6.0] + +### New Features + +- [`50ebddaa2`](https://www.github.com/tauri-apps/tauri/commit/50ebddaa2d83033a393a176ba07ef28352b98210) ([#13319](https://www.github.com/tauri-apps/tauri/pull/13319) by [@kingsword09](https://www.github.com/tauri-apps/tauri/../../kingsword09)) Expose the `setAutoResize` API for webviews in `@tauri-apps/api`. +- [`267368fd4`](https://www.github.com/tauri-apps/tauri/commit/267368fd4f83e0a71dfb1b72a66d56592a2066bc) ([#13276](https://www.github.com/tauri-apps/tauri/pull/13276) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `Monitor.workArea` field. + +### Bug Fixes + +- [`23b9da75b`](https://www.github.com/tauri-apps/tauri/commit/23b9da75b91379cca9520bc53b10fdf39ebae241) ([#13324](https://www.github.com/tauri-apps/tauri/pull/13324) by [@kingsword09](https://www.github.com/tauri-apps/tauri/../../kingsword09)) Fixed path joining behavior where `path.join('', 'a')` incorrectly returns "/a" instead of "a". +- [`b985eaf0a`](https://www.github.com/tauri-apps/tauri/commit/b985eaf0a231ea570e36d686c665cddbc76ab4f6) ([#13306](https://www.github.com/tauri-apps/tauri/pull/13306) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Immediately unregister event listener when the unlisten function is called. + +### What's Changed + +- [`b5c549d18`](https://www.github.com/tauri-apps/tauri/commit/b5c549d1898ecdb712822c02dc665cc6771fbd07) ([#13325](https://www.github.com/tauri-apps/tauri/pull/13325) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) `transformCallback` now registers the callbacks inside `window.__TAURI_INTERNALS__.callbacks` instead of directly on `window['_{id}']` + +## \[2.5.0] + +### New Features + +- [`66e6325f4`](https://www.github.com/tauri-apps/tauri/commit/66e6325f43efa49ec2165c45afec911a1a14ecfb) ([#13136](https://www.github.com/tauri-apps/tauri/pull/13136)) Allow passing the callback as the parameter of constructor of `Channel` so you can use it like this `new Channel((message) => console.log(message))` +- [`ea36294cb`](https://www.github.com/tauri-apps/tauri/commit/ea36294cbca98f7725c91d1464fd92e77c89698a) ([#13208](https://www.github.com/tauri-apps/tauri/pull/13208)) Added `disableInputAccessoryView: bool` config for iOS. +- [`c1cd0a2dd`](https://www.github.com/tauri-apps/tauri/commit/c1cd0a2ddb5bc3e99451cbe399b5fc9f0035f571) ([#13090](https://www.github.com/tauri-apps/tauri/pull/13090)) macOS/iOS: add option to disable or enable link previews when building a webview (the webkit api has it enabled by default) + + - `WindowOptions::allowLinkPreview` + - `WebviewOptions::allowLinkPreview` +- [`b072e2b29`](https://www.github.com/tauri-apps/tauri/commit/b072e2b2967640ae4fa1af466ae878c156551edd) ([#9687](https://www.github.com/tauri-apps/tauri/pull/9687)) Add `preventOverflow` config option to prevent the window from overflowing the monitor size on creation +- [`dd4f13ce4`](https://www.github.com/tauri-apps/tauri/commit/dd4f13ce4b3cd89cde2fa3f18a063c272f215621) ([#13185](https://www.github.com/tauri-apps/tauri/pull/13185)) Added `app.setDockVisibility` for macOS. + +### Enhancements + +- [`b8f86669a`](https://www.github.com/tauri-apps/tauri/commit/b8f86669ab05f7dbdd15839a20999e63dc43bda6) ([#13145](https://www.github.com/tauri-apps/tauri/pull/13145)) `core.isTauri` now leverages `globalThis` instead of `window` in order to be used in unit tests. + +### Bug Fixes + +- [`66e6325f4`](https://www.github.com/tauri-apps/tauri/commit/66e6325f43efa49ec2165c45afec911a1a14ecfb) ([#13136](https://www.github.com/tauri-apps/tauri/pull/13136)) Fix `Channel`'s callback attached to `window` never cleaned up + +## \[2.4.1] + +### Enhancements + +- [`dd1372833`](https://www.github.com/tauri-apps/tauri/commit/dd137283341ce0e6aabfd158d07d77c6feeb920e) ([#13066](https://www.github.com/tauri-apps/tauri/pull/13066) by [@ahaoboy](https://www.github.com/tauri-apps/tauri/../../ahaoboy)) Add a generic to `emit` and `emitTo` functions for the `payload` instead of the previously used type (`unknown`). + +## \[2.4.0] + +### New Features + +- [`d8059bad3`](https://www.github.com/tauri-apps/tauri/commit/d8059bad3cc922dc369c39ca1cfa49aaec31322e) ([#12900](https://www.github.com/tauri-apps/tauri/pull/12900) by [@Simon-Laux](https://www.github.com/tauri-apps/tauri/../../Simon-Laux)) add `AppHandle.fetch_data_store_identifiers` and `AppHandle.remove_data_store` (macOS and iOS only) +- [`20c190691`](https://www.github.com/tauri-apps/tauri/commit/20c19069125c89b2d45a2127278c9ffc2df35fc2) ([#12821](https://www.github.com/tauri-apps/tauri/pull/12821) by [@Simon-Laux](https://www.github.com/tauri-apps/tauri/../../Simon-Laux)) Added `WindowOptions::javascriptDisabled` and `WebviewOptions::javascriptDisabled`. +- [`060de5bbd`](https://www.github.com/tauri-apps/tauri/commit/060de5bbdddca384e3965a8938d89840f27c581d) ([#12837](https://www.github.com/tauri-apps/tauri/pull/12837) by [@niladrix719](https://www.github.com/tauri-apps/tauri/../../niladrix719)) Added `getIdentifier()` function to get the application identifier configured in tauri.conf.json +- [`be2e6b85f`](https://www.github.com/tauri-apps/tauri/commit/be2e6b85fed226732b4a98f68cc5d72b4f8f5a13) ([#12944](https://www.github.com/tauri-apps/tauri/pull/12944) by [@Simon-Laux](https://www.github.com/tauri-apps/tauri/../../Simon-Laux)) Added `Window#isAlwaysOnTop` and `WebviewWindow#isAlwaysOnTop` methods. +- [`bcdd51025`](https://www.github.com/tauri-apps/tauri/commit/bcdd510254ebe37827e22a5ffeb944321361e97c) ([#13012](https://www.github.com/tauri-apps/tauri/pull/13012) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) The `path` basename and extname APIs now accept Android content URIs, such as the paths returned by the dialog plugin. + +### Bug Fixes + +- [`3a74dc8f3`](https://www.github.com/tauri-apps/tauri/commit/3a74dc8f3421112b1d0a32b6a432606b1f33cc25) ([#12935](https://www.github.com/tauri-apps/tauri/pull/12935) by [@tk103331](https://www.github.com/tauri-apps/tauri/../../tk103331)) Fix `Webview.close` always fail with command not found + +## \[2.3.0] + +### Enhancements + +- [`a2d36b8c3`](https://www.github.com/tauri-apps/tauri/commit/a2d36b8c34a8dcfc6736797ca5cd4665faf75e7e) ([#12181](https://www.github.com/tauri-apps/tauri/pull/12181) by [@bastiankistner](https://www.github.com/tauri-apps/tauri/../../bastiankistner)) Add an option to change the default background throttling policy (currently for WebKit only). + +## \[2.2.0] + +### New Features + +- [`020ea0556`](https://www.github.com/tauri-apps/tauri/commit/020ea05561348dcd6d2a7df358f8a5190f661ba2) ([#11661](https://www.github.com/tauri-apps/tauri/pull/11661) by [@ahqsoftwares](https://www.github.com/tauri-apps/tauri/../../ahqsoftwares)) Add badging APIs: + + - `Window/WebviewWindow::set_badge_count` for Linux, macOS and IOS. + - `Window/WebviewWindow::set_overlay_icon` for Windows Only. + - `Window/WebviewWindow::set_badge_label`for macOS Only. +- [`fc30b20be`](https://www.github.com/tauri-apps/tauri/commit/fc30b20bea125f647db00ca824663f8e1da4d61f) ([#11726](https://www.github.com/tauri-apps/tauri/pull/11726) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `TrayIcon.setShowMenuOnLeftClick` method and deprecate `TrayIcon.setMenuOnLeftClick` to match the Rust API. +- [`fc30b20be`](https://www.github.com/tauri-apps/tauri/commit/fc30b20bea125f647db00ca824663f8e1da4d61f) ([#11726](https://www.github.com/tauri-apps/tauri/pull/11726) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `TrayIconOptions.showMenuOnLeftClick` field and deprecate `TrayIconOptions.menuOnLeftClick` to match the Rust API. + +### Enhancements + +- [`fc30b20be`](https://www.github.com/tauri-apps/tauri/commit/fc30b20bea125f647db00ca824663f8e1da4d61f) ([#11726](https://www.github.com/tauri-apps/tauri/pull/11726) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add support for `TrayIconOptions.menuOnLeftClick` option and `TrayIcon.setMenuOnLeftClick` on Windows. + +### Bug Fixes + +- [`a16796a55`](https://www.github.com/tauri-apps/tauri/commit/a16796a55592cf5be80043edfbb630dd2e32efab) ([#12069](https://www.github.com/tauri-apps/tauri/pull/12069) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix `Channel` never calls `onmessage` in some cases +- [`12a48d1e2`](https://www.github.com/tauri-apps/tauri/commit/12a48d1e26a83c3915eaa0687b196fbc8f2d457a) ([#11741](https://www.github.com/tauri-apps/tauri/pull/11741) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Fix error when calling `PredefinedMenuItem.new` to create an `About` menu item that uses an `Image` instance for the about icon. +- [`12a48d1e2`](https://www.github.com/tauri-apps/tauri/commit/12a48d1e26a83c3915eaa0687b196fbc8f2d457a) ([#11741](https://www.github.com/tauri-apps/tauri/pull/11741) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Fix error when calling `IconMenuItem.new` using an `Image` instance for the icon. +- [`b63262cd4`](https://www.github.com/tauri-apps/tauri/commit/b63262cd4d6a3667ca1664607a0a5444ad79fe0e) ([#11724](https://www.github.com/tauri-apps/tauri/pull/11724) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Removed the generic in the type of the callback function argument in `mockIPC` which prevented its proper use in tests using TypeScript. +- [`a6e84f7d2`](https://www.github.com/tauri-apps/tauri/commit/a6e84f7d2c1d5fdc65901fce683502be3f47833f) ([#11835](https://www.github.com/tauri-apps/tauri/pull/11835) by [@ilittlebig](https://www.github.com/tauri-apps/tauri/../../ilittlebig)) Fix error where using `isAbsolute` would return `Command not found`. + +## \[2.1.1] + +### Bug Fixes + +- [`7f81f0523`](https://www.github.com/tauri-apps/tauri/commit/7f81f052365675721312aafba297a7b67fb872d2) Fix regression in `toLogical` and `toPhysical` for position types in `dpi` module returning incorrect `y` value. +- [`e8a50f6d7`](https://www.github.com/tauri-apps/tauri/commit/e8a50f6d760fad4529e7abb400302a1b487f11dd) ([#11645](https://www.github.com/tauri-apps/tauri/pull/11645)) Fix integer values of `BasDirectory.Home` and `BaseDirectory.Font` regression which broke path APIs in JS. + +## \[2.1.0] + +### New Features + +- [`5c4b83084`](https://www.github.com/tauri-apps/tauri/commit/5c4b830843ab085f8ff9db9e08d832223b027e4e) ([#11191](https://www.github.com/tauri-apps/tauri/pull/11191) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Improved support for `dpi` module types to allow these types to be used without manual conversions with `invoke`: + + - Added `SERIALIZE_TO_IPC_FN` const in `core` module which can be used to implement custom IPC serialization for types passed to `invoke`. + - Added `Size` and `Position` classes in `dpi` module. + - Implementd `SERIALIZE_TO_IPC_FN` method on `PhysicalSize`, `PhysicalPosition`, `LogicalSize` and `LogicalPosition` to convert it into a valid IPC-compatible value that can be deserialized correctly on the Rust side into its equivalent struct. +- [`4d545ab3c`](https://www.github.com/tauri-apps/tauri/commit/4d545ab3ca228c8a21b966b709f84a0da2864479) ([#11486](https://www.github.com/tauri-apps/tauri/pull/11486) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Added `Webview::setBackgroundColor`, `WebviewWindow::setBackgroundColor` APIs to set the window background color dynamically + and a `backgroundColor` window option to set the background color on window creation. +- [`cbc095ec5`](https://www.github.com/tauri-apps/tauri/commit/cbc095ec5fe7de29b5c9265576d4e071ec159c1c) ([#11451](https://www.github.com/tauri-apps/tauri/pull/11451) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `app > windows > devtools` config option and when creating the webview from JS, to enable or disable devtools for a specific webview. +- [`2a75c64b5`](https://www.github.com/tauri-apps/tauri/commit/2a75c64b5431284e7340e8743d4ea56a62c75466) ([#11469](https://www.github.com/tauri-apps/tauri/pull/11469) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Added `windowClassname` option, when constructing a `Webview` or `WebviewWindow`, to specify the name of the window class on Windows. + +### Bug Fixes + +- [`54cbf59b5`](https://www.github.com/tauri-apps/tauri/commit/54cbf59b5a572570a47237a3b5e6505f2a9e5d5d) ([#11441](https://www.github.com/tauri-apps/tauri/pull/11441) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Fix submenu created as a menu item instead of a submenu when created by using an object in the `items` field in the options object passed to `Menu.new` or `Submenu.new`. + +## \[2.0.3] + +### Bug Fixes + +- [`fbb45c674`](https://www.github.com/tauri-apps/tauri/commit/fbb45c674ca92fbbe04f1a8360e5f2e477dd4297) ([#11423](https://www.github.com/tauri-apps/tauri/pull/11423) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fixes `addPluginListener` not working. + +### What's Changed + +- [`2e88633ba`](https://www.github.com/tauri-apps/tauri/commit/2e88633ba4da8fc289c6d8a29c36f3327f9b576e) ([#11369](https://www.github.com/tauri-apps/tauri/pull/11369) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Remove references to no longer used `__TAURI_INTERNALS__.metadata.windows` and `__TAURI_INTERNALS__.metadata.webviews`. + +## \[2.0.2] + +### What's Changed + +- [`e968b3d25`](https://www.github.com/tauri-apps/tauri/commit/e968b3d2527b8edf7653e6cf7284dc4a8889b5fe) ([#11219](https://www.github.com/tauri-apps/tauri/pull/11219) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Actually publish package with the latest tag. + +## \[2.0.1] + +### What's Changed + +- [`be683e2ac`](https://www.github.com/tauri-apps/tauri/commit/be683e2ac36df9c51a5c050d9d500247bd019090) ([#11199](https://www.github.com/tauri-apps/tauri/pull/11199) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Publish package with the latest NPM tag. + +## \[2.0.0] + +### What's Changed + +- [`637285790`](https://www.github.com/tauri-apps/tauri/commit/6372857905ae9c0aedb7f482ddf6cf9f9836c9f2) Promote to v2 stable! + +## \[2.0.0-rc.6] + +### New Features + +- [`9014a3f17`](https://www.github.com/tauri-apps/tauri/commit/9014a3f1765ca406ea5c3e5224267a79c52cd53d) ([#11066](https://www.github.com/tauri-apps/tauri/pull/11066) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `WebviewWindow.clearAllBrowsingData` and `Webview.clearAllBrowsingData` to clear the webview browsing data. +- [`95df53a2e`](https://www.github.com/tauri-apps/tauri/commit/95df53a2ed96873cd35a4b14a5e312d07e4e3004) ([#11143](https://www.github.com/tauri-apps/tauri/pull/11143) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Add the ability to set theme dynamically using `Window.setTheme` or `setTheme` function from the `app` module +- [`d9d2502b4`](https://www.github.com/tauri-apps/tauri/commit/d9d2502b41e39efde679e30c8955006e2ba9ea64) ([#11140](https://www.github.com/tauri-apps/tauri/pull/11140) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `Webview.hide` and `Webview.show` methods. +- [`de7414aab`](https://www.github.com/tauri-apps/tauri/commit/de7414aab935e45540594ea930eb60bae4dbc979) ([#11154](https://www.github.com/tauri-apps/tauri/pull/11154) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `Window::setEnabled` and `Window::isEnabled` methods + +### Bug Fixes + +- [`948772a65`](https://www.github.com/tauri-apps/tauri/commit/948772a657eb3caf20843628abac9109e3b67d41) ([#11114](https://www.github.com/tauri-apps/tauri/pull/11114) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Change the `button_state` tray event field to camelCase `buttonState`. + +### Breaking Changes + +- [`0b4495996`](https://www.github.com/tauri-apps/tauri/commit/0b4495996d3131a5ee80fbb2c71a28203e491ee7) ([#11121](https://www.github.com/tauri-apps/tauri/pull/11121) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Simplified emitted tray event JS value and updated `TrayIconEvent` type definition to match it. + +## \[2.0.0-rc.5] + +### New Features + +- [`ddf69157b`](https://www.github.com/tauri-apps/tauri/commit/ddf69157b54249f3321ca72db6703812019f1ab9) ([#11031](https://www.github.com/tauri-apps/tauri/pull/11031) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add `toPhysical` method on `LogicalPositon` and `LogicalSize` classes. + +## \[2.0.0-rc.4] + +### Enhancements + +- [`f81929e25`](https://www.github.com/tauri-apps/tauri/commit/f81929e25104aa1091e464bd012c80649dedf9e5) ([#10799](https://www.github.com/tauri-apps/tauri/pull/10799) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Added `PermissionState`, `checkPermissions` and `requestPermissions` base APIs to the core module, designed for plugin authors to extend. + +### Bug Fixes + +- [`fbe76a955`](https://www.github.com/tauri-apps/tauri/commit/fbe76a955a63af9fb33f66d5f747caf858cf179b) ([#10797](https://www.github.com/tauri-apps/tauri/pull/10797) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Uint8Arrays and ArrayBuffers are now properly serialized as an array of numbers. + +## \[2.0.0-rc.3] + +### What's Changed + +- [`f4d5241b3`](https://www.github.com/tauri-apps/tauri/commit/f4d5241b377d0f7a1b58100ee19f7843384634ac) ([#10731](https://www.github.com/tauri-apps/tauri/pull/10731) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Update documentation icon path. + +## \[2.0.0-rc.2] + +### Bug Fixes + +- [`c689521a7`](https://www.github.com/tauri-apps/tauri/commit/c689521a7674b6562b5dfd4f5cacd12138d99d85) ([#10681](https://www.github.com/tauri-apps/tauri/pull/10681) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix tslib path in dist. + +## \[2.0.0-rc.1] + +### Breaking Changes + +- [`b6dca99ff`](https://www.github.com/tauri-apps/tauri/commit/b6dca99fff73816a39380b288c299b47b493cfdb) ([#10630](https://www.github.com/tauri-apps/tauri/pull/10630) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Changed `WebviewWindow.getAll`, `WebviewWindow.getByLabel`, `getAllWebviewWindows`, + `Window.getAll`, `Window.getByLabel`, `getAllWindows`, + `Webview.getAll`, `Webview.getByLabel`, `getAllWebviews` + to be async so their return value are synchronized with the state from the Rust side, + meaning new and destroyed windows are reflected. + +## \[2.0.0-rc.0] + +### Changes + +- Promoted to RC! + +## \[2.0.0-beta.16] + +### New Features + +- [`da25f7353`](https://www.github.com/tauri-apps/tauri/commit/da25f7353070477ba969851e974379d7666d6806) ([#10242](https://www.github.com/tauri-apps/tauri/pull/10242) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Add APIs to enable setting window size constraints separately: + + - Added `WindowSizeConstraints` interface in `window` and `webviewWindow` modules. + - Added `Window.setSizeConstraints` and `WebviewWindow.setSizeConstraints` + +### Bug Fixes + +- [`3c17fb64f`](https://www.github.com/tauri-apps/tauri/commit/3c17fb64fd822597d5cc16ee7e7b3f9e1023637b) ([#10277](https://www.github.com/tauri-apps/tauri/pull/10277) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix `Webview.reparent` pointing to `set_webview_focus` instead of `reparent` Rust API +- [`da25f7353`](https://www.github.com/tauri-apps/tauri/commit/da25f7353070477ba969851e974379d7666d6806) ([#10242](https://www.github.com/tauri-apps/tauri/pull/10242) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Apply `minWidth`, `minHieght`, `maxWidth` and `maxHeight` constraints separately, which fixes a long standing bug where these constraints were never applied unless width and height were constrained together. + +## \[2.0.0-beta.15] + +### New Features + +- [`7bc6a2a1d`](https://www.github.com/tauri-apps/tauri/commit/7bc6a2a1d6d2c5406d91cac94d33bce76443c28f) ([#9788](https://www.github.com/tauri-apps/tauri/pull/9788) by [@pewsheen](https://www.github.com/tauri-apps/tauri/../../pewsheen)) Add a new method to set title bar style dynamically on macOS. + +### Enhancements + +- [`080b6e127`](https://www.github.com/tauri-apps/tauri/commit/080b6e12720b89d839c686d7067cc94d276ed7e4) ([#10246](https://www.github.com/tauri-apps/tauri/pull/10246) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Use `EventName` on `Window`, `Webview` and `WebviewWindow`'s `once` so you can get auto complete for tauri's built-in events + +### Bug Fixes + +- [`080b6e127`](https://www.github.com/tauri-apps/tauri/commit/080b6e12720b89d839c686d7067cc94d276ed7e4) ([#10246](https://www.github.com/tauri-apps/tauri/pull/10246) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix `once` doesn't detached after one callback if event handler throws + +### Breaking Changes + +- [`261c9f942`](https://www.github.com/tauri-apps/tauri/commit/261c9f942de9a598b5c6cc504de6bddd1306113b) ([#10170](https://www.github.com/tauri-apps/tauri/pull/10170) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Renamed drag and drop events in `TauriEvent` enum to better convey when they are triggered: + + - `TauriEvent.DRAG` -> `TauriEvent.DRAG_ENTER` + - `TauriEvent.DROP_OVER` -> `TauriEvent.DRAG_OVER` + - `TauriEvent.DROP` -> `TauriEvent.DRAG_DROP` + - `TauriEvent.DROP_CANCELLED` -> `TauriEvent::DRAG_LEAVE` + + Also the `type` field values in `Window/Webview/WebviewWindow.onDropEvent` and `DragDropEvent` have changed: + + - `dragged` -> `enter` + - `dragOver` -> `over` + - `dropped` -> `drop` + - `cancelled` -> `leave` +- [`2b1ceb40d`](https://www.github.com/tauri-apps/tauri/commit/2b1ceb40d345aef42dd79438fa69ca7989ee0194) ([#10229](https://www.github.com/tauri-apps/tauri/pull/10229) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Renamed the JS `getCurrent` and `getAll` functions to a clearer name to avoid ambiguity: + + - `getCurrent` in `window` module has been renamed to `getCurrentWindow` + - `getCurrent` in `webview` module has been renamed to `getCurrentWebview` + - `getCurrent` in `webviewWindow` module has been renamed to `getCurrentWebviewWindow` + - `getAll` in `window` module has been renamed to `getAllWindows` + - `getAll` in `webview` module has been renamed to `getAllWebviews` + - `getAll` in `webviewWindow` module has been renamed to `getAllWebviewWindows` + +## \[2.0.0-beta.14] + +### New Features + +- [`148f04887`](https://www.github.com/tauri-apps/tauri/commit/148f048871caee21498b236c058b8890f2b66cc7) ([#9979](https://www.github.com/tauri-apps/tauri/pull/9979)) Add `defaultWindowIcon` to the JS `app` module to retrieve the default window icon in JS. + +### Bug Fixes + +- [`c98f385cb`](https://www.github.com/tauri-apps/tauri/commit/c98f385cb5da4d72968df24b1fc0b58212d59653) ([#10044](https://www.github.com/tauri-apps/tauri/pull/10044)) Export `mocks` module in `@tauri-apps/api` npm package. + +## \[2.0.0-beta.13] + +### Breaking Changes + +- [`c4410daa8`](https://www.github.com/tauri-apps/tauri/commit/c4410daa85616340e911c8243fdaa69e6906fd49)([#9777](https://www.github.com/tauri-apps/tauri/pull/9777)) This release contains breaking changes to the tray event structure because of newly added events: + + - Changed `TrayIconEvent` to be an enum instead of a struct. + - Added `MouseButtonState` and `MouseButton` enums. + - Removed `ClickType` enum and replaced it with `MouseButton` enum. + - Added `MouseButtonState` enum. + +## \[2.0.0-beta.12] + +### New Features + +- [`ec0e092ec`](https://www.github.com/tauri-apps/tauri/commit/ec0e092ecd23b547c756c7476f23a0d95be6db80)([#9770](https://www.github.com/tauri-apps/tauri/pull/9770)) Add `monitorFromPoint` function in `window` module to get the monitor from a given point. + +## \[2.0.0-beta.11] + +### Bug Fixes + +- [`aa080696e`](https://www.github.com/tauri-apps/tauri/commit/aa080696e0952abff416dd9088d519eaf2587a3a)([#9618](https://www.github.com/tauri-apps/tauri/pull/9618)) Fix `isTauri` incorrect return type. + +## \[2.0.0-beta.10] + +### New Features + +- [`477bb8cd4`](https://www.github.com/tauri-apps/tauri/commit/477bb8cd4ea88ade3f6c1f268ad1701a68150161)([#9297](https://www.github.com/tauri-apps/tauri/pull/9297)) Add `cursorPosition` function in `window` module to get the current cursor position. + +## \[2.0.0-beta.9] + +### New Features + +- [`70c51371e`](https://www.github.com/tauri-apps/tauri/commit/70c51371e01184223312de3dba8030394a5a9406)([#9539](https://www.github.com/tauri-apps/tauri/pull/9539)) Add `isTauri` function in `core` module to check whether running inside tauri or not. + +### Bug Fixes + +- [`be7eab209`](https://www.github.com/tauri-apps/tauri/commit/be7eab209c60c45e140f7bcb4bab1037d62d4c03)([#9486](https://www.github.com/tauri-apps/tauri/pull/9486)) Set the `exports > types` package.json field. +- [`cf615e8e4`](https://www.github.com/tauri-apps/tauri/commit/cf615e8e4d5008ee1ac3f77e530ba26fb91e8977)([#9463](https://www.github.com/tauri-apps/tauri/pull/9463)) Fixes a bug when processing channel messages out of order. +- [`35b25f7e5`](https://www.github.com/tauri-apps/tauri/commit/35b25f7e5c0fe03af4ed3582e22a626863f035f0)([#9530](https://www.github.com/tauri-apps/tauri/pull/9530)) Do not use JS optional chaining to prevent script errors on older webviews such as macOS 10.14. + +## \[2.0.0-beta.8] + +### New Features + +- [`58a7a552d`](https://www.github.com/tauri-apps/tauri/commit/58a7a552d739b77b71d61af11c53f7f2dc7a6e7e)([#9378](https://www.github.com/tauri-apps/tauri/pull/9378)) Added the `set_zoom` function to the webview API. +- [`58a7a552d`](https://www.github.com/tauri-apps/tauri/commit/58a7a552d739b77b71d61af11c53f7f2dc7a6e7e)([#9378](https://www.github.com/tauri-apps/tauri/pull/9378)) Add `zoom_hotkeys_enabled` to enable browser native zoom controls on creating webviews. + +### Bug Fixes + +- [`48a7a78f8`](https://www.github.com/tauri-apps/tauri/commit/48a7a78f8094d08e5e403e88050391642d29151b)([#9376](https://www.github.com/tauri-apps/tauri/pull/9376)) Fix `Window/Webview/WebviewWindow.setSize`, `Window/Webview/WebviewWindow.setPostion`, `Window/WebviewWindow.setMinSize`, `Window/WebviewWindow.setMaxSize`, `Window/WebviewWindow.setCursorPosition` and `Menu/Submenu.popup` methods failing with invalid args. + +## \[2.0.0-beta.7] + +### Bug Fixes + +- [`c33f6e6cf`](https://www.github.com/tauri-apps/tauri/commit/c33f6e6cf35a0d34b5598875a2e5b642a01c8b38)([#9211](https://www.github.com/tauri-apps/tauri/pull/9211)) Re-added the `TauriEvent.WINDOW_CREATED` (`tauri://window-created`) event. + +### Breaking Changes + +- [`06833f4fa`](https://www.github.com/tauri-apps/tauri/commit/06833f4fa8e63ecc55fe3fc874a9e397e77a5709)([#9100](https://www.github.com/tauri-apps/tauri/pull/9100)) Rename `FileDrop` to `DragDrop` on structs, enums and enum variants. Also renamed `file_drop` to `drag_drop` on fields and function names. + +## \[2.0.0-beta.6] + +### New Features + +- [`acdd76833`](https://www.github.com/tauri-apps/tauri/commit/acdd76833db6d81f4012418133d0042220de100b)([#9155](https://www.github.com/tauri-apps/tauri/pull/9155)) Add `TrayIcon.getById` and `TrayIcon.removeById` static methods. + +### Enhancements + +- [`ea0242db4`](https://www.github.com/tauri-apps/tauri/commit/ea0242db4aa6c127d2bb4a2e275000ba47c9e68c)([#9179](https://www.github.com/tauri-apps/tauri/pull/9179)) The `Image` constructor is now public (for internal use only). + +### Bug Fixes + +- [`379cc2b35`](https://www.github.com/tauri-apps/tauri/commit/379cc2b3547395474d4b66b4222679cf4538428d)([#9165](https://www.github.com/tauri-apps/tauri/pull/9165)) Fix `basename(path, 'ext')` JS API when removing all occurances of `ext` where it should only remove the last one. + +### Breaking Changes + +- [`ea0242db4`](https://www.github.com/tauri-apps/tauri/commit/ea0242db4aa6c127d2bb4a2e275000ba47c9e68c)([#9179](https://www.github.com/tauri-apps/tauri/pull/9179)) `Image::rgba()` now returns `Promise`. +- [`ea0242db4`](https://www.github.com/tauri-apps/tauri/commit/ea0242db4aa6c127d2bb4a2e275000ba47c9e68c)([#9179](https://www.github.com/tauri-apps/tauri/pull/9179)) Removed `width` and `height` methods on the JS `Image` class, use `size` instead. + +## \[2.0.0-beta.5] + +### Breaking Changes + +- [`db0a24a97`](https://www.github.com/tauri-apps/tauri/commit/db0a24a973191752aeecfbd556faa254b0f17e79)([#9132](https://www.github.com/tauri-apps/tauri/pull/9132)) Remove the `Image.fromPngBytes` and `Image.fromIcoBytes` APIs. Use `Image.fromBytes` instead. + +## \[2.0.0-beta.4] + +### New Features + +- [`d1e77acd8`](https://www.github.com/tauri-apps/tauri/commit/d1e77acd8dfdf554b90b542513a58a2de1ef2360)([#9011](https://www.github.com/tauri-apps/tauri/pull/9011)) Add a new `Image` type in Rust and JS. + +### Enhancements + +- [`e62ca4ee9`](https://www.github.com/tauri-apps/tauri/commit/e62ca4ee95f4308a6ad128d0f100c85634e28223)([#9070](https://www.github.com/tauri-apps/tauri/pull/9070)) Added a mechanism to preserve channel message order. + +## \[2.0.0-beta.3] + +### New Features + +- [`fdcaf935`](https://www.github.com/tauri-apps/tauri/commit/fdcaf935fa75ecfa2806939c4faad4fe9e880386)([#8939](https://www.github.com/tauri-apps/tauri/pull/8939)) Added the `reparent` function to the webview API. + +## \[2.0.0-beta.2] + +### Breaking Changes + +- [`361ec37f`](https://www.github.com/tauri-apps/tauri/commit/361ec37fd4a5caa5b6630b9563ef079f53c6c336)([#8932](https://www.github.com/tauri-apps/tauri/pull/8932)) Removed the `unityUri` option from the progress bar state, no longer required. + +## \[2.0.0-beta.1] + +### New Features + +- [`16e550ec`](https://www.github.com/tauri-apps/tauri/commit/16e550ec1503765158cdc3bb2a20e70ec710e981)([#8844](https://www.github.com/tauri-apps/tauri/pull/8844)) Add a new `webviewWindow` module that exports `WebviewWindow` class and related methods such as `getCurrent` and `getAll`. +- [`16e550ec`](https://www.github.com/tauri-apps/tauri/commit/16e550ec1503765158cdc3bb2a20e70ec710e981)([#8844](https://www.github.com/tauri-apps/tauri/pull/8844)) Add `Window.onFileDropEvent` method. + +### Breaking Changes + +- [`16e550ec`](https://www.github.com/tauri-apps/tauri/commit/16e550ec1503765158cdc3bb2a20e70ec710e981)([#8844](https://www.github.com/tauri-apps/tauri/pull/8844)) Renamed the following enum variants of `TauriEvent` enum: + + - `TauriEvent.WEBVIEW_FILE_DROP` -> `TauriEvent.FILE_DROP` + - `TauriEvent.WEBVIEW_FILE_DROP_HOVER` -> `TauriEvent.FILE_DROP_HOVER` + - `TauriEvent.WEBVIEW_FILE_DROP_CANCELLED` -> `TauriEvent.FILE_DROP_CANCELLED` +- [`16e550ec`](https://www.github.com/tauri-apps/tauri/commit/16e550ec1503765158cdc3bb2a20e70ec710e981)([#8844](https://www.github.com/tauri-apps/tauri/pull/8844)) Move `WebviewWindow` class from `webview` module to a new `webviewWindow` module. + +## \[2.0.0-beta.0] + +### New Features + +- [`74a2a603`](https://www.github.com/tauri-apps/tauri/commit/74a2a6036a5e57462f161d728cbd8a6f121028ca)([#8661](https://www.github.com/tauri-apps/tauri/pull/8661)) Implement access control list for IPC usage. +- [`a093682d`](https://www.github.com/tauri-apps/tauri/commit/a093682d2df7169b024bb4f736c7f1fd2ea8b327)([#8621](https://www.github.com/tauri-apps/tauri/pull/8621)) Added `emitTo` api to `event` module which is equivalent to the rust `emit_to` method. Also added `emitTo` method on `Window`, `Webivew` and `WebviewWindow` classes. +- [`a2fc3a63`](https://www.github.com/tauri-apps/tauri/commit/a2fc3a63579ca739646d696870cbecbb3a169d33)([#8657](https://www.github.com/tauri-apps/tauri/pull/8657)) Add `visibleOnAllWorkspaces` option when creating the window in JS and `Window.setVisibleOnAllWorkspaces` method. +- [`7f033f6d`](https://www.github.com/tauri-apps/tauri/commit/7f033f6dcd54c69a4193765a5c1584755ba92c61)([#8537](https://www.github.com/tauri-apps/tauri/pull/8537)) Add `Window.startResizeDragging`. +- [`9eaeb5a8`](https://www.github.com/tauri-apps/tauri/commit/9eaeb5a8cd95ae24b5e66205bdc2763cb7f965ce)([#8622](https://www.github.com/tauri-apps/tauri/pull/8622)) Add `parent` option when creating a window. +- [`af610232`](https://www.github.com/tauri-apps/tauri/commit/af6102327376884364b2075b468bdf08ee0d02aa)([#8710](https://www.github.com/tauri-apps/tauri/pull/8710)) Added `Window::destroy` to force close a window. +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) Added support to multiwebview via the new `window` and `webview` modules. + +### Breaking Changes + +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) Removed event callback's `windowLabel`. +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) The event target is now an object so you can target either a window or a webview. +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) Moved webview-specific APIs from the `Window` class to the `Webview` class. +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) Renamed `TauriEvent.WINDOW_FILE_DROP` to `TauriEvent.WEBVIEW_FILE_DROP`, `TauriEvent.WINDOW_FILE_DROP_HOVER` to `TauriEvent.WEBVIEW_FILE_DROP_HOVER` and `TauriEvent.WINDOW_FILE_DROP_CANCELLED` to `TauriEvent.WEBVIEW_FILE_DROP_CANCELLED`. +- [`c77b4032`](https://www.github.com/tauri-apps/tauri/commit/c77b40324ea9bf580871fc11aed69ba0c9b6b8cf)([#8280](https://www.github.com/tauri-apps/tauri/pull/8280)) Added back the `WebviewWindow` API that exposes functionality of a window that hosts a single webview. The dedicated `Window` and `Webview` types are exposed for multiwebview features. +- [`af610232`](https://www.github.com/tauri-apps/tauri/commit/af6102327376884364b2075b468bdf08ee0d02aa)([#8710](https://www.github.com/tauri-apps/tauri/pull/8710)) `Window::close` now triggers a close requested event instead of forcing the window to be closed. + +## \[2.0.0-alpha.14] + +- [`97e33412`](https://www.github.com/tauri-apps/tauri/commit/97e334129956159bbd60e1c531b6acd3bc6139a6)([#8534](https://www.github.com/tauri-apps/tauri/pull/8534)) `mockIPC` and `mockWindows` no longer crash if `window.__TAURI_INTERNALS__` is undefined. + +## \[2.0.0-alpha.13] + +### New Features + +- [`428ea652`](https://www.github.com/tauri-apps/tauri/commit/428ea6524c70545be33aac96d7c22b21f25caa4c)([#8370](https://www.github.com/tauri-apps/tauri/pull/8370)) Exposed `Resource` class which should be extended for Rust-backed resources created through `tauri::Manager::resources_table`. + +### Bug Fixes + +- [`ef21b681`](https://www.github.com/tauri-apps/tauri/commit/ef21b681e237a80592c9118b9c023c1d57231bac)([#8391](https://www.github.com/tauri-apps/tauri/pull/8391)) Fix a regression where typescript could not find types when using `"moduleResolution": "node"` +- [`46451aee`](https://www.github.com/tauri-apps/tauri/commit/46451aee1318f63a6cd861a12b63929b38c64eb6)([#8268](https://www.github.com/tauri-apps/tauri/pull/8268)) Add top-level `main`, `module` and `types` fields in `package.json` to be compliant with typescripts's `"moduleResolution": "node"` + +### Breaking Changes + +- [`c2ad4d28`](https://www.github.com/tauri-apps/tauri/commit/c2ad4d28c481b2d7ed643458db56210cd44a2e0c)([#8273](https://www.github.com/tauri-apps/tauri/pull/8273)) Changed former `tauri` module from `primitives` to `core`. + +## \[2.0.0-alpha.12] + +### New Features + +- [`f93148ea`](https://www.github.com/tauri-apps/tauri/commit/f93148eac05a1428e038bd9351a8149b2464ff4c)([#7709](https://www.github.com/tauri-apps/tauri/pull/7709)) Add `tray` and `menu` modules to create and manage tray icons and menus from Javascript. + +### Enhancements + +- [`b7add750`](https://www.github.com/tauri-apps/tauri/commit/b7add750ef9f32d959de613ab35063ff240281c2)([#8204](https://www.github.com/tauri-apps/tauri/pull/8204)) Added `position` field to the `FileDropEvent` payload. + +## \[2.0.0-alpha.11] + +### Bug Fixes + +- [`822bf15d`](https://www.github.com/tauri-apps/tauri/commit/822bf15d6b258556b689ca55ac2ac224897e913a)([#8130](https://www.github.com/tauri-apps/tauri/pull/8130)) Fix tslib missing in the distributed api package. + +## \[2.0.0-alpha.10] + +### Enhancements + +- [`c6c59cf2`](https://www.github.com/tauri-apps/tauri/commit/c6c59cf2373258b626b00a26f4de4331765dd487) Pull changes from Tauri 1.5 release. + +### Bug Fixes + +- [`287066b2`](https://www.github.com/tauri-apps/tauri/commit/287066b279f503dd09bfd43d5da37d1f471451fb)([#8071](https://www.github.com/tauri-apps/tauri/pull/8071)) No longer crashing in tests without mocks when `clearMocks` is defined in `afterEach` hook. + +## \[2.0.0-alpha.9] + +### New Features + +- [`c1ec0f15`](https://www.github.com/tauri-apps/tauri/commit/c1ec0f155118527361dd5645d920becbc8afd569)([#7933](https://www.github.com/tauri-apps/tauri/pull/7933)) Added `setAlwaysOnBottom` function on `Window` and the `alwaysOnBottom` option when creating a window. +- [`fb10b879`](https://www.github.com/tauri-apps/tauri/commit/fb10b87970a43320ef4d14564f45e7579b774eaf)([#8039](https://www.github.com/tauri-apps/tauri/pull/8039)) Add the `app` module back. +- [`ed32257d`](https://www.github.com/tauri-apps/tauri/commit/ed32257d044f90b5eb15053efd1667125def2d2b)([#7794](https://www.github.com/tauri-apps/tauri/pull/7794)) On Windows, add `Effect.Tabbed`,`Effect.TabbedDark` and `Effect.TabbedLight` effects. +- [`c9a9246c`](https://www.github.com/tauri-apps/tauri/commit/c9a9246c37bdf190661355c8ee406dac6c427344)([#8007](https://www.github.com/tauri-apps/tauri/pull/8007)) Add the `window` module back. +- [`c085adda`](https://www.github.com/tauri-apps/tauri/commit/c085addab58ba851398373c6fd13f9cb026d71e8)([#8009](https://www.github.com/tauri-apps/tauri/pull/8009)) Added the `setProgressBar` API on the `Window` class. + +### What's Changed + +- [`5c0eeb40`](https://www.github.com/tauri-apps/tauri/commit/5c0eeb40c1003583290ff3aebfa02e2b5f5b9c41)([#7638](https://www.github.com/tauri-apps/tauri/pull/7638)) Updated minimum Node.js version to 18. + +### Breaking Changes + +- [`a63e71f9`](https://www.github.com/tauri-apps/tauri/commit/a63e71f9799e9bbc82521d2f17b5238fbf690e89)([#7942](https://www.github.com/tauri-apps/tauri/pull/7942)) Changed `tauri` module to `primitives` and removed the undocumented `invoke` export from the root module. + +## \[2.0.0-alpha.8] + +### Breaking Changes + +- [`d5074af5`](https://www.github.com/tauri-apps/tauri/commit/d5074af562b2b5cb6c5711442097c4058af32db6)([#7801](https://www.github.com/tauri-apps/tauri/pull/7801)) The custom protocol on Android now uses the `http` scheme instead of `https`. + +## \[2.0.0-alpha.7] + +### Breaking Changes + +- [`4cb51a2d`](https://www.github.com/tauri-apps/tauri/commit/4cb51a2d56cfcae0749062c79ede5236bd8c02c2)([#7779](https://www.github.com/tauri-apps/tauri/pull/7779)) The custom protocol on Windows now uses the `http` scheme instead of `https`. + +## \[2.0.0-alpha.6] + +### New Features + +- [`4af5c5a8`](https://www.github.com/tauri-apps/tauri/commit/4af5c5a8293263c16f8a65e8d232f2de52f41701)([#7170](https://www.github.com/tauri-apps/tauri/pull/7170)) Change the IPC call to align with the new format for the custom protocol based API. + +## \[2.0.0-alpha.5] + +### New Features + +- [`e0f0dce2`](https://www.github.com/tauri-apps/tauri/commit/e0f0dce220730e2822fc202463aedf0166145de7)([#6442](https://www.github.com/tauri-apps/tauri/pull/6442)) Added the `windowEffects` option when creating a window and `setWindowEffects` method to change it at runtime. + +### Enhancements + +- [`9e3a18e0`](https://www.github.com/tauri-apps/tauri/commit/9e3a18e04672edad15d0ec654bd8632544871967)([#7132](https://www.github.com/tauri-apps/tauri/pull/7132)) Expose the window target option on event APIs. +- [`6d3f3138`](https://www.github.com/tauri-apps/tauri/commit/6d3f3138b9e2f41cda712c7d9caba0f0e65dfd3c)([#7160](https://www.github.com/tauri-apps/tauri/pull/7160)) Changed `sep` and `delimiter` from `path` module into functions to fix import in frameworks like `next.js` +- [`4652c446`](https://www.github.com/tauri-apps/tauri/commit/4652c446b361a801252bcf45e9da39813bf85482)([#7144](https://www.github.com/tauri-apps/tauri/pull/7144)) Add `tempDir` function to `path` module + +## \[2.0.0-alpha.4] + +- [`0ab5f40d`](https://www.github.com/tauri-apps/tauri/commit/0ab5f40d3a4207f20e4440587b41c4e78f91d233)([#6813](https://www.github.com/tauri-apps/tauri/pull/6813)) Add channel API for sending data across the IPC. +- [`3245d14b`](https://www.github.com/tauri-apps/tauri/commit/3245d14b9eb256a5c5675c7030bac7082855df47)([#6895](https://www.github.com/tauri-apps/tauri/pull/6895)) Moved the `app` feature to its own plugin in the plugins-workspace repository. +- [`09376af5`](https://www.github.com/tauri-apps/tauri/commit/09376af59424cc27803fa2820d2ac0d4cdc90a6d)([#6704](https://www.github.com/tauri-apps/tauri/pull/6704)) Moved the `cli` feature to its own plugin in the plugins-workspace repository. +- [`2d5378bf`](https://www.github.com/tauri-apps/tauri/commit/2d5378bfc1ba817ee2f331b41738a90e5997e5e8)([#6717](https://www.github.com/tauri-apps/tauri/pull/6717)) Moved the dialog APIs to its own plugin in the plugins-workspace repository. +- [`39f1b04f`](https://www.github.com/tauri-apps/tauri/commit/39f1b04f7be4966488484829cd54c8ce72a04200)([#6943](https://www.github.com/tauri-apps/tauri/pull/6943)) Moved the `event` JS APIs to a plugin. +- [`fc4d687e`](https://www.github.com/tauri-apps/tauri/commit/fc4d687ef0ef2ea069ed73c40916da733b5dcb8f)([#6716](https://www.github.com/tauri-apps/tauri/pull/6716)) Moved the file system APIs to its own plugin in the plugins-workspace repository. +- [`f78a3783`](https://www.github.com/tauri-apps/tauri/commit/f78a378344bbec48533641661d865920a8f46f8f)([#6742](https://www.github.com/tauri-apps/tauri/pull/6742)) Moved the `http` feature to its own plugin in the plugins-workspace repository. +- [`29ce9ce2`](https://www.github.com/tauri-apps/tauri/commit/29ce9ce2ce7dfb260d556d5cffd075e8fe06660c)([#6902](https://www.github.com/tauri-apps/tauri/pull/6902)) Moved the `os` feature to its own plugin in the plugins-workspace repository. +- [`60cf9ed2`](https://www.github.com/tauri-apps/tauri/commit/60cf9ed2fcd7be4df41e86cf18735efe9b6cb254)([#6905](https://www.github.com/tauri-apps/tauri/pull/6905)) Moved the `process` feature to its own plugin in the plugins-workspace repository. +- [`96639ca2`](https://www.github.com/tauri-apps/tauri/commit/96639ca239c9e4f75142fc07868ac46822111cff)([#6749](https://www.github.com/tauri-apps/tauri/pull/6749)) Moved the `shell` functionality to its own plugin in the plugins-workspace repository. +- [`b072daa3`](https://www.github.com/tauri-apps/tauri/commit/b072daa3bd3e38b808466666619ddb885052c5b2)([#6919](https://www.github.com/tauri-apps/tauri/pull/6919)) Moved the `updater` feature to its own plugin in the plugins-workspace repository. +- [`cebd7526`](https://www.github.com/tauri-apps/tauri/commit/cebd75261ac71b98976314a450cb292eeeec1515)([#6728](https://www.github.com/tauri-apps/tauri/pull/6728)) Moved the `clipboard` feature to its own plugin in the plugins-workspace repository. +- [`3f17ee82`](https://www.github.com/tauri-apps/tauri/commit/3f17ee82f6ff21108806edb7b00500b8512b8dc7)([#6737](https://www.github.com/tauri-apps/tauri/pull/6737)) Moved the `global-shortcut` feature to its own plugin in the plugins-workspace repository. +- [`9a79dc08`](https://www.github.com/tauri-apps/tauri/commit/9a79dc085870e0c1a5df13481ff271b8c6cc3b78)([#6947](https://www.github.com/tauri-apps/tauri/pull/6947)) Moved the `window` JS APIs to its own plugin in the plugins-workspace repository. + +## \[2.0.0-alpha.3] + +- Overload the dialog `open` function to have better TS result types. + - [1eacd51d](https://www.github.com/tauri-apps/tauri/commit/1eacd51d185ba69a3c3cb2cc93c792e2d5929843) overloaded the open function for convenient type inference ([#5619](https://www.github.com/tauri-apps/tauri/pull/5619)) on 2023-04-07 + +## \[2.0.0-alpha.2] + +- Added `raw` encoding option to read stdout and stderr raw bytes. + - [f992e7f5](https://www.github.com/tauri-apps/tauri/commit/f992e7f58bf975c654a3daf36780b31a32bac064) chore(changes): readd change file on 2023-04-03 +- Removed shell's `Command` constructor and added the `Command.create` static function instead. + - [509d4678](https://www.github.com/tauri-apps/tauri/commit/509d4678b12816c1dd08a9a5efa71ba556d91c27) Support sending raw byte data to the "data" event for child command's stdout and stderr ([#5789](https://www.github.com/tauri-apps/tauri/pull/5789)) on 2023-03-31 + +## \[2.0.0-alpha.1] + +- Added the `shadow` option when creating a window and `setShadow` function. + - [a81750d7](https://www.github.com/tauri-apps/tauri/commit/a81750d779bc72f0fdb7de90b7fbddfd8049b328) feat(core): add shadow APIs ([#6206](https://www.github.com/tauri-apps/tauri/pull/6206)) on 2023-02-08 + +## \[2.0.0-alpha.0] + +- First mobile alpha release! + - [fa3a1098](https://www.github.com/tauri-apps/tauri/commit/fa3a10988a03aed1b66fb17d893b1a9adb90f7cd) feat(ci): prepare 2.0.0-alpha.0 ([#5786](https://www.github.com/tauri-apps/tauri/pull/5786)) on 2022-12-08 + +## \[1.5.3] + +### Bug Fixes + +- [`1c582a94`](https://www.github.com/tauri-apps/tauri/commit/1c582a942e345a066b65620e4db9f688ec142bb9)([#8392](https://www.github.com/tauri-apps/tauri/pull/8392)) Fix a regression where typescript could not find types when using `"moduleResolution": "node"` + +## \[1.5.2] + +### Bug Fixes + +- [`50462702`](https://www.github.com/tauri-apps/tauri/commit/504627027303ef5a0e855aab2abea64c6964223b)([#8267](https://www.github.com/tauri-apps/tauri/pull/8267)) Add top-level `main`, `module` and `types` fields in `package.json` to be compliant with typescripts's `"moduleResolution": "node"` +- [`14544e4b`](https://www.github.com/tauri-apps/tauri/commit/14544e4b87269c06c89fed3647d80f492e0a1d34)([#8219](https://www.github.com/tauri-apps/tauri/pull/8219)) Avoid crashing in `clearMocks` + +## \[1.5.1] + +### New Features + +- [`2b0212af`](https://www.github.com/tauri-apps/tauri/commit/2b0212af49c386e52bb2357381813d6d435ec4af)([#7961](https://www.github.com/tauri-apps/tauri/pull/7961)) Add `mockConvertFileSrc` in `mocks` module, to mock `convertFileSrc` function. + +## \[1.5.0] + +### New Features + +- [`6c408b73`](https://www.github.com/tauri-apps/tauri/commit/6c408b736c7aa2a0a91f0a40d45a2b7a7dedfe78)([#7269](https://www.github.com/tauri-apps/tauri/pull/7269)) Add option to specify notification sound. + +### Enhancements + +- [`58d6b899`](https://www.github.com/tauri-apps/tauri/commit/58d6b899e21d37bb42810890d289deb57f2273bd)([#7636](https://www.github.com/tauri-apps/tauri/pull/7636)) Add `append` option to `FsOptions` in the `fs` JS module, used in `writeTextFile` and `writeBinaryFile`, to be able to append to existing files instead of overwriting it. + +### Bug Fixes + +- [`2eab1505`](https://www.github.com/tauri-apps/tauri/commit/2eab1505632ff71431d4c31c49b5afc78fa5b9dd)([#7394](https://www.github.com/tauri-apps/tauri/pull/7394)) Fix `Body.form` static not reading and sending entries of type `Blob` (including subclasses such as `File`) + +## \[1.4.0] + +### New Features + +- [`359058ce`](https://www.github.com/tauri-apps/tauri/commit/359058cecca44a9c30b65140c44a8bb3a6dd3be8)([#5939](https://www.github.com/tauri-apps/tauri/pull/5939)) Add `locale` function in the `os` module to get the system locale. +- [`c4d6fb4b`](https://www.github.com/tauri-apps/tauri/commit/c4d6fb4b1ea8acf02707a9fe5dcab47c1c5bae7b)([#2353](https://www.github.com/tauri-apps/tauri/pull/2353)) Added the `maximizable`, `minimizable` and `closable` fields on `WindowOptions`. +- [`c4d6fb4b`](https://www.github.com/tauri-apps/tauri/commit/c4d6fb4b1ea8acf02707a9fe5dcab47c1c5bae7b)([#2353](https://www.github.com/tauri-apps/tauri/pull/2353)) Added the `setMaximizable`, `setMinimizable`, `setClosable`, `isMaximizable`, `isMinimizable` and `isClosable` methods. +- [`000104bc`](https://www.github.com/tauri-apps/tauri/commit/000104bc3bc0c9ff3d20558ab9cf2080f126e9e0)([#6472](https://www.github.com/tauri-apps/tauri/pull/6472)) Add `WebviewWindow.is_focused` and `WebviewWindow.getFocusedWindow` getters. + +## \[1.3.0] + +- Return correct type for `event.payload` in `onResized` and `onMoved` window event handlers. + - [0b46637e](https://www.github.com/tauri-apps/tauri/commit/0b46637ebaba54403afa32a1cb466f09df2db999) fix(api): construct correct object for onResized and onMoved, closes [#6507](https://www.github.com/tauri-apps/tauri/pull/6507) ([#6509](https://www.github.com/tauri-apps/tauri/pull/6509)) on 2023-04-03 +- Added the `WindowOptions::contentProtected` option and `WebviewWindow#setContentProtected` to change it at runtime. + - [4ab5545b](https://www.github.com/tauri-apps/tauri/commit/4ab5545b7a831c549f3c65e74de487ede3ab7ce5) feat: add content protection api, closes [#5132](https://www.github.com/tauri-apps/tauri/pull/5132) ([#5513](https://www.github.com/tauri-apps/tauri/pull/5513)) on 2022-12-13 +- Allow setting the text of the dialog buttons. + - [00e1efaa](https://www.github.com/tauri-apps/tauri/commit/00e1efaa9b33876d41dd360624b69971e70d3856) feat: customize button texts of message dialog ([#4383](https://www.github.com/tauri-apps/tauri/pull/4383)) on 2022-12-28 +- Add `is_minimized()` window method. + - [62144ef3](https://www.github.com/tauri-apps/tauri/commit/62144ef3be63b237869e511826edfb938e2c7174) feat: add is_minimized (fix [#3878](https://www.github.com/tauri-apps/tauri/pull/3878)) ([#5618](https://www.github.com/tauri-apps/tauri/pull/5618)) on 2022-12-13 +- Add `title` getter on window. + - [233e43b0](https://www.github.com/tauri-apps/tauri/commit/233e43b0c34fada1ca025378533a0b76931a6540) feat: add `title` getter on window, closes [#5023](https://www.github.com/tauri-apps/tauri/pull/5023) ([#5515](https://www.github.com/tauri-apps/tauri/pull/5515)) on 2022-12-13 + +## \[1.2.0] + +- Added the `acceptFirstMouse` window option. + - [95f467ad](https://www.github.com/tauri-apps/tauri/commit/95f467add51448319983c54e2f382c7c09fb72d6) feat(core): add window `accept_first_mouse` option, closes [#5347](https://www.github.com/tauri-apps/tauri/pull/5347) ([#5374](https://www.github.com/tauri-apps/tauri/pull/5374)) on 2022-10-17 +- Fix incorrect return type on `fs/exists` + - [ca3cd8b3](https://www.github.com/tauri-apps/tauri/commit/ca3cd8b3d11beb9b6102da40b7d27f6dbe6cd2d0) fix(api): fs/exists return type previously set to void when it should be boolean ([#5252](https://www.github.com/tauri-apps/tauri/pull/5252)) on 2022-09-29 +- Initialize `Monitor` instances with the correct classes for `position` and `size` fields instead of plain object. + - [6f41a271](https://www.github.com/tauri-apps/tauri/commit/6f41a2712445ac41a5ed84bbcd40af3b76c8b1d8) fix(api.js): fix `Monitor` initialization, closes [#4672](https://www.github.com/tauri-apps/tauri/pull/4672) ([#5314](https://www.github.com/tauri-apps/tauri/pull/5314)) on 2022-09-30 +- **Breaking change:** Node.js v12 is no longer supported. + - [1129f4f5](https://www.github.com/tauri-apps/tauri/commit/1129f4f575dd02f746abe8e66472c88c8f9fe63d) refactor: simplify api.js bundling ([#4277](https://www.github.com/tauri-apps/tauri/pull/4277)) on 2022-10-04 +- Add new app-specific `BaseDirectory` enum variants `AppConfig`, `AppData`, `AppLocalData`, `AppCache` and `AppLog` along with equivalent functions in `path` module and deprecated ambiguous variants `Log` and `App` along with their equivalent functions in `path` module. + - [5d89905e](https://www.github.com/tauri-apps/tauri/commit/5d89905e39ce0e6eaaec50a693679335449edb32) feat(api): add app-specific directory APIs, closes [#5263](https://www.github.com/tauri-apps/tauri/pull/5263) ([#5272](https://www.github.com/tauri-apps/tauri/pull/5272)) on 2022-09-28 +- Fix `dialog.save` return type + - [8357ce5b](https://www.github.com/tauri-apps/tauri/commit/8357ce5b2efdd6f92c7944822542e48ba0e303ce) Fix dialog.save return type ([#5373](https://www.github.com/tauri-apps/tauri/pull/5373)) on 2022-10-08 +- Added support to `FormData` on the `Body.form` function. + - [aa119f28](https://www.github.com/tauri-apps/tauri/commit/aa119f28364f8ffbc64c6bcdfc77483613076a20) feat(api): add FormData support on Body.form, closes [#5545](https://www.github.com/tauri-apps/tauri/pull/5545) ([#5546](https://www.github.com/tauri-apps/tauri/pull/5546)) on 2022-11-04 +- Added `show` and `hide` methods on the `app` module. + - [39bf895b](https://www.github.com/tauri-apps/tauri/commit/39bf895b73ec6b53f5758815396ba85dda6b9c67) feat(macOS): Add application `show` and `hide` methods ([#3689](https://www.github.com/tauri-apps/tauri/pull/3689)) on 2022-10-03 +- Added `tabbingIdentifier` window option for macOS. + - [4137ab44](https://www.github.com/tauri-apps/tauri/commit/4137ab44a81d739556cbc7583485887e78952bf1) feat(macos): add `tabbing_identifier` option, closes [#2804](https://www.github.com/tauri-apps/tauri/pull/2804), [#3912](https://www.github.com/tauri-apps/tauri/pull/3912) ([#5399](https://www.github.com/tauri-apps/tauri/pull/5399)) on 2022-10-19 +- Added `tabbing_identifier` to the window builder on macOS. + - [4137ab44](https://www.github.com/tauri-apps/tauri/commit/4137ab44a81d739556cbc7583485887e78952bf1) feat(macos): add `tabbing_identifier` option, closes [#2804](https://www.github.com/tauri-apps/tauri/pull/2804), [#3912](https://www.github.com/tauri-apps/tauri/pull/3912) ([#5399](https://www.github.com/tauri-apps/tauri/pull/5399)) on 2022-10-19 +- Added the `user_agent` option when creating a window. + - [a6c94119](https://www.github.com/tauri-apps/tauri/commit/a6c94119d8545d509723b147c273ca5edfe3729f) feat(core): expose user_agent to window config ([#5317](https://www.github.com/tauri-apps/tauri/pull/5317)) on 2022-10-02 + +## \[1.1.0] + +- Update `mockIPC()` handler signature to allow async handler functions. + - [4fa968dc](https://www.github.com/tauri-apps/tauri/commit/4fa968dc0e74b5206bfcd54e704d180c16b67b08) fix(api): add async `mockIPC()` handler signature ([#5056](https://www.github.com/tauri-apps/tauri/pull/5056)) on 2022-08-26 +- Improve shell's `Command`, `Command.stdout` and `Command.stderr` events with new `once`, `off`, `listenerCount`, `prependListener`, `prependOnceListener` and `removeAllListeners` functions. + - [aa9f1243](https://www.github.com/tauri-apps/tauri/commit/aa9f1243e6c1629972a82e469f20c8399741740e) Improved EventEmitter for tauri api shell ([#4697](https://www.github.com/tauri-apps/tauri/pull/4697)) on 2022-07-26 +- Added the `encoding` option to the `Command` options. + - [d8cf9f9f](https://www.github.com/tauri-apps/tauri/commit/d8cf9f9fcd617ac24fa418952fd4a32c08804f5c) Command support for specified character encoding, closes [#4644](https://www.github.com/tauri-apps/tauri/pull/4644) ([#4772](https://www.github.com/tauri-apps/tauri/pull/4772)) on 2022-07-28 +- Add `exists` function to the fs module. + - [3c62dbc9](https://www.github.com/tauri-apps/tauri/commit/3c62dbc902c904d35a7472ce72a969084c95fbbe) feat(api): Add `exists` function to the fs module. ([#5060](https://www.github.com/tauri-apps/tauri/pull/5060)) on 2022-09-15 + +## \[1.0.2] + +- Added helper functions to listen to updater and window events. + - [b02fc90f](https://www.github.com/tauri-apps/tauri/commit/b02fc90f450ff9e9d8a35ee55dc1beced4957869) feat(api): add abstractions to updater and window event listeners ([#4569](https://www.github.com/tauri-apps/tauri/pull/4569)) on 2022-07-05 +- Add support to `ArrayBuffer` in `Body.bytes` and `writeBinaryFile`. + - [92aca55a](https://www.github.com/tauri-apps/tauri/commit/92aca55a6f1f899d5c0c3a6aae9ac9cb0a7e9a86) feat(api): add support to ArrayBuffer ([#4579](https://www.github.com/tauri-apps/tauri/pull/4579)) on 2022-07-05 +- Use `toString()` on message/confirm/ask dialogs title and message values. + - [b8cd2a79](https://www.github.com/tauri-apps/tauri/commit/b8cd2a7993cd2aa5b71b30c545b3307245d254bf) feat(api): call `toString()` on dialog title and message, closes [#4583](https://www.github.com/tauri-apps/tauri/pull/4583) ([#4588](https://www.github.com/tauri-apps/tauri/pull/4588)) on 2022-07-04 +- Remove the `type-fest` dependency, changing the OS types to the specific enum instead of allowing any string. + - [d5e910eb](https://www.github.com/tauri-apps/tauri/commit/d5e910ebcc6c8d7f055ab0691286722b140ffcd4) chore(api): remove `type-fest` ([#4605](https://www.github.com/tauri-apps/tauri/pull/4605)) on 2022-07-06 + +## \[1.0.1] + +- Fixes the `writeBinaryFile` sending an empty file contents when only the first argument is passed. + - [ea43cf52](https://www.github.com/tauri-apps/tauri/commit/ea43cf52db8541d20a6397ef3ecd40f0f2bd6113) fix(api): `writeBinaryFile` sends an empty contents with only one arg ([#4368](https://www.github.com/tauri-apps/tauri/pull/4368)) on 2022-06-16 + +## \[1.0.0] + +- Allow choosing multiple folders in `dialog.open`. + - [4e51dce6](https://www.github.com/tauri-apps/tauri/commit/4e51dce6ca21c7664de779bc78a04be1051371f7) fix: dialog open supports multiple dirs, fixes [#4091](https://www.github.com/tauri-apps/tauri/pull/4091) ([#4354](https://www.github.com/tauri-apps/tauri/pull/4354)) on 2022-06-15 +- Upgrade to `stable`! + - [f4bb30cc](https://www.github.com/tauri-apps/tauri/commit/f4bb30cc73d6ba9b9ef19ef004dc5e8e6bb901d3) feat(covector): prepare for v1 ([#4351](https://www.github.com/tauri-apps/tauri/pull/4351)) on 2022-06-15 + +## \[1.0.0-rc.7] + +- Fix `FilePart` usage in `http.Body.form` by renaming the `value` property to `file`. + - [55f89d5f](https://www.github.com/tauri-apps/tauri/commit/55f89d5f9d429252ad3fd557b1d6233b256495e0) fix(api): Rename FormPart `value` to `file` to match docs and endpoint ([#4307](https://www.github.com/tauri-apps/tauri/pull/4307)) on 2022-06-09 +- Fixes a memory leak in the command system. + - [f72cace3](https://www.github.com/tauri-apps/tauri/commit/f72cace36821dc675a6d25268ae85a21bdbd6296) fix: never remove ipc callback & mem never be released ([#4274](https://www.github.com/tauri-apps/tauri/pull/4274)) on 2022-06-05 +- The notification's `isPermissionGranted` function now returns `boolean` instead of `boolean | null`. The response is never `null` because we won't check the permission for now, always returning `true` instead. + - [f482b094](https://www.github.com/tauri-apps/tauri/commit/f482b0942276e9402ab3725957535039bacb4fef) fix: remove notification permission prompt ([#4302](https://www.github.com/tauri-apps/tauri/pull/4302)) on 2022-06-09 +- Added the `resolveResource` API to the path module. + - [7bba8db8](https://www.github.com/tauri-apps/tauri/commit/7bba8db83ead92e9bd9c4be7863742e71ac47513) feat(api): add `resolveResource` API to the path module ([#4234](https://www.github.com/tauri-apps/tauri/pull/4234)) on 2022-05-29 +- Renamed `writeFile` to `writeTextFile` but kept the original function for backwards compatibility. + - [3f998ca2](https://www.github.com/tauri-apps/tauri/commit/3f998ca29445a349489078a74dd068e157a4d68e) feat(api): add `writeTextFile` and `(path, contents, options)` overload ([#4228](https://www.github.com/tauri-apps/tauri/pull/4228)) on 2022-05-29 +- Added `(path, contents[, options])` overload to the `writeTextFile` and `writeBinaryFile` APIs. + - [3f998ca2](https://www.github.com/tauri-apps/tauri/commit/3f998ca29445a349489078a74dd068e157a4d68e) feat(api): add `writeTextFile` and `(path, contents, options)` overload ([#4228](https://www.github.com/tauri-apps/tauri/pull/4228)) on 2022-05-29 + +## \[1.0.0-rc.6] + +- Expose option to set the dialog type. + - [f46175d5](https://www.github.com/tauri-apps/tauri/commit/f46175d5d46fa3eae66ad2415a0eb1efb7d31da2) feat(core): expose option to set dialog type, closes [#4183](https://www.github.com/tauri-apps/tauri/pull/4183) ([#4187](https://www.github.com/tauri-apps/tauri/pull/4187)) on 2022-05-21 +- Expose `title` option in the message dialog API. + - [ae99f991](https://www.github.com/tauri-apps/tauri/commit/ae99f991674d77c322a2240d10ed4b78ed2f4d4b) feat(core): expose message dialog's title option, ref [#4183](https://www.github.com/tauri-apps/tauri/pull/4183) ([#4186](https://www.github.com/tauri-apps/tauri/pull/4186)) on 2022-05-21 + +## \[1.0.0-rc.5] + +- Fixes the type of `http > connectTimeout`. + - [f3c5ca89](https://www.github.com/tauri-apps/tauri/commit/f3c5ca89e79d429183c4e15a9e7cebada2b493a0) fix(core): http api `connect_timeout` deserialization, closes [#4004](https://www.github.com/tauri-apps/tauri/pull/4004) ([#4006](https://www.github.com/tauri-apps/tauri/pull/4006)) on 2022-04-29 + +## \[1.0.0-rc.4] + +- Encode the file path in the `convertFileSrc` function. + - [42e8d9cf](https://www.github.com/tauri-apps/tauri/commit/42e8d9cf925089e9ad591198ee04b0cc0a0eed48) fix(api): encode file path in `convertFileSrc` function, closes [#3841](https://www.github.com/tauri-apps/tauri/pull/3841) ([#3846](https://www.github.com/tauri-apps/tauri/pull/3846)) on 2022-04-02 +- Added `theme` getter to `WebviewWindow`. + - [4cebcf6d](https://www.github.com/tauri-apps/tauri/commit/4cebcf6da7cad1953e0f01b426afac3b5ef1f81e) feat: expose theme APIs, closes [#3903](https://www.github.com/tauri-apps/tauri/pull/3903) ([#3937](https://www.github.com/tauri-apps/tauri/pull/3937)) on 2022-04-21 +- Added `theme` field to `WindowOptions`. + - [4cebcf6d](https://www.github.com/tauri-apps/tauri/commit/4cebcf6da7cad1953e0f01b426afac3b5ef1f81e) feat: expose theme APIs, closes [#3903](https://www.github.com/tauri-apps/tauri/pull/3903) ([#3937](https://www.github.com/tauri-apps/tauri/pull/3937)) on 2022-04-21 +- Added the `setCursorGrab`, `setCursorVisible`, `setCursorIcon` and `setCursorPosition` methods to the `WebviewWindow` class. + - [c54ddfe9](https://www.github.com/tauri-apps/tauri/commit/c54ddfe9338e7eb90b4d5b02dfde687d432d5bc1) feat: expose window cursor APIs, closes [#3888](https://www.github.com/tauri-apps/tauri/pull/3888) [#3890](https://www.github.com/tauri-apps/tauri/pull/3890) ([#3935](https://www.github.com/tauri-apps/tauri/pull/3935)) on 2022-04-21 +- **Breaking change:** The process Command API stdio lines now includes the trailing `\r`. + - [b5622882](https://www.github.com/tauri-apps/tauri/commit/b5622882cf3748e1e5a90915f415c0cd922aaaf8) fix(cli): exit on non-compilation Cargo errors, closes [#3930](https://www.github.com/tauri-apps/tauri/pull/3930) ([#3942](https://www.github.com/tauri-apps/tauri/pull/3942)) on 2022-04-22 +- Added the `tauri://theme-changed` event. + - [4cebcf6d](https://www.github.com/tauri-apps/tauri/commit/4cebcf6da7cad1953e0f01b426afac3b5ef1f81e) feat: expose theme APIs, closes [#3903](https://www.github.com/tauri-apps/tauri/pull/3903) ([#3937](https://www.github.com/tauri-apps/tauri/pull/3937)) on 2022-04-21 + +## \[1.0.0-rc.3] + +- Properly define the `appWindow` type. + - [1deeb03e](https://www.github.com/tauri-apps/tauri/commit/1deeb03ef6c7cbea8cf585864424a3d66f184a02) fix(api.js): appWindow shown as type `any`, fixes [#3747](https://www.github.com/tauri-apps/tauri/pull/3747) ([#3772](https://www.github.com/tauri-apps/tauri/pull/3772)) on 2022-03-24 +- Added `Temp` to the `BaseDirectory` enum. + - [266156a0](https://www.github.com/tauri-apps/tauri/commit/266156a0b08150b21140dd552c8bc252fe413cdd) feat(core): add `BaseDirectory::Temp` and `$TEMP` variable ([#3763](https://www.github.com/tauri-apps/tauri/pull/3763)) on 2022-03-24 + +## \[1.0.0-rc.2] + +- Do not crash if `__TAURI_METADATA__` is not set, log an error instead. + - [9cb1059a](https://www.github.com/tauri-apps/tauri/commit/9cb1059aa3f81521ccc6da655243acfe0327cd98) fix(api): do not throw an exception if **TAURI_METADATA** is not set, fixes [#3554](https://www.github.com/tauri-apps/tauri/pull/3554) ([#3572](https://www.github.com/tauri-apps/tauri/pull/3572)) on 2022-03-03 +- Reimplement endpoint to read file as string for performance. + - [834ccc51](https://www.github.com/tauri-apps/tauri/commit/834ccc51539401d36a7dfa1c0982623c9c446a4c) feat(core): reimplement `readTextFile` for performance ([#3631](https://www.github.com/tauri-apps/tauri/pull/3631)) on 2022-03-07 +- Fixes a regression on the `unlisten` command. + - [76c791bd](https://www.github.com/tauri-apps/tauri/commit/76c791bd2b836d2055410e37e71716172a3f81ef) fix(core): regression on the unlisten function ([#3623](https://www.github.com/tauri-apps/tauri/pull/3623)) on 2022-03-06 + +## \[1.0.0-rc.1] + +- Provide functions to mock IPC calls during testing and static site generation. + - [7e04c072](https://www.github.com/tauri-apps/tauri/commit/7e04c072c4ee2278c648f44575c6c4710ac047f3) feat: add mock functions for testing and SSG ([#3437](https://www.github.com/tauri-apps/tauri/pull/3437)) on 2022-02-14 + - [6f5ed2e6](https://www.github.com/tauri-apps/tauri/commit/6f5ed2e69cb7ffa0d5c8eb5a744fbf94ed6010d4) fix: change file on 2022-02-14 + +## \[1.0.0-rc.0] + +- Add `fileDropEnabled` property to `WindowOptions` so you can now disable it when creating windows from js. + + - [1bfc32a3](https://www.github.com/tauri-apps/tauri/commit/1bfc32a3b2f31b962ce8a5c611b60cb008360923) fix(api.js): add `fileDropEnabled` to `WindowOptions`, closes [#2968](https://www.github.com/tauri-apps/tauri/pull/2968) ([#2989](https://www.github.com/tauri-apps/tauri/pull/2989)) on 2021-12-09 + +- Add `logDir` function to the `path` module to access the suggested log directory. + Add `BaseDirectory.Log` to the `fs` module. + + - [acbb3ae7](https://www.github.com/tauri-apps/tauri/commit/acbb3ae7bb0165846b9456aea103269f027fc548) feat: add Log directory ([#2736](https://www.github.com/tauri-apps/tauri/pull/2736)) on 2021-10-16 + - [62c7a8ad](https://www.github.com/tauri-apps/tauri/commit/62c7a8ad30fd3031b8679960590e5ef3eef8e4da) chore(covector): prepare for `rc` release ([#3376](https://www.github.com/tauri-apps/tauri/pull/3376)) on 2022-02-10 + +- Expose `ask`, `message` and `confirm` APIs on the dialog module. + + - [e98c1af4](https://www.github.com/tauri-apps/tauri/commit/e98c1af44279a5ff6c8a6f0a506ecc219c9f77af) feat(core): expose message dialog APIs, fix window.confirm, implement HasRawWindowHandle for Window, closes [#2535](https://www.github.com/tauri-apps/tauri/pull/2535) ([#2700](https://www.github.com/tauri-apps/tauri/pull/2700)) on 2021-10-02 + +- Event `emit` now automatically serialize non-string types. + + - [06000996](https://www.github.com/tauri-apps/tauri/commit/060009969627890fa9018e2f1105bad13299394c) feat(api): support unknown types for event emit payload, closes [#2929](https://www.github.com/tauri-apps/tauri/pull/2929) ([#2964](https://www.github.com/tauri-apps/tauri/pull/2964)) on 2022-01-07 + +- Fix `http.fetch` throwing error if the response is successful but the body is empty. + + - [50c63900](https://www.github.com/tauri-apps/tauri/commit/50c63900c7313064037e2ceb798a6432fcd1bcda) fix(api.js): fix `http.fetch` throwing error if response body is empty, closes [#2831](https://www.github.com/tauri-apps/tauri/pull/2831) ([#3008](https://www.github.com/tauri-apps/tauri/pull/3008)) on 2021-12-09 + +- Add `title` option to file open/save dialogs. + + - [e1d6a6e6](https://www.github.com/tauri-apps/tauri/commit/e1d6a6e6445637723e2331ca799a662e720e15a8) Create api-file-dialog-title.md ([#3235](https://www.github.com/tauri-apps/tauri/pull/3235)) on 2022-01-16 + - [62c7a8ad](https://www.github.com/tauri-apps/tauri/commit/62c7a8ad30fd3031b8679960590e5ef3eef8e4da) chore(covector): prepare for `rc` release ([#3376](https://www.github.com/tauri-apps/tauri/pull/3376)) on 2022-02-10 + +- Fix `os.platform` returning `macos` and `windows` instead of `darwin` and `win32`. + + - [3924c3d8](https://www.github.com/tauri-apps/tauri/commit/3924c3d85365df30b376a1ec6c2d933460d66af0) fix(api.js): fix `os.platform` return on macos and windows, closes [#2698](https://www.github.com/tauri-apps/tauri/pull/2698) ([#2699](https://www.github.com/tauri-apps/tauri/pull/2699)) on 2021-10-02 + +- The `formatCallback` helper function now returns a number instead of a string. + + - [a48b8b18](https://www.github.com/tauri-apps/tauri/commit/a48b8b18d428bcc404d489daa690bbefe1f57311) feat(core): validate callbacks and event names \[TRI-038] \[TRI-020] ([#21](https://www.github.com/tauri-apps/tauri/pull/21)) on 2022-01-09 + +- Added `rawHeaders` to `http > Response`. + + - [b7a2345b](https://www.github.com/tauri-apps/tauri/commit/b7a2345b06ca0306988b4ba3d3deadd449e65af9) feat(core): add raw headers to HTTP API, closes [#2695](https://www.github.com/tauri-apps/tauri/pull/2695) ([#3053](https://www.github.com/tauri-apps/tauri/pull/3053)) on 2022-01-07 + +- Removed the `currentDir` API from the `path` module. + + - [a08509c6](https://www.github.com/tauri-apps/tauri/commit/a08509c641f43695e25944a2dd47697b18cd83e2) fix(api): remove `currentDir` API from the `path` module on 2022-02-04 + +- Remove `.ts` files on the published package. + + - [0f321ac0](https://www.github.com/tauri-apps/tauri/commit/0f321ac08d56412edd5bc9d166201fbc95d887d8) fix(api): do not ship TS files, closes [#2598](https://www.github.com/tauri-apps/tauri/pull/2598) ([#2645](https://www.github.com/tauri-apps/tauri/pull/2645)) on 2021-09-23 + +- **Breaking change:** Replaces all usages of `number[]` with `Uint8Array` to be closer aligned with the wider JS ecosystem. + + - [9b19a805](https://www.github.com/tauri-apps/tauri/commit/9b19a805aa8efa64b22f2dfef193a144b8e0cee3) fix(api.js) Replace `number[]`with `Uint8Array`. fixes [#3306](https://www.github.com/tauri-apps/tauri/pull/3306) ([#3305](https://www.github.com/tauri-apps/tauri/pull/3305)) on 2022-02-05 + +- `WindowManager` methods `innerPosition` `outerPosition` now correctly return instance of `PhysicalPosition`. + `WindowManager` methods `innerSize` `outerSize` now correctly return instance of `PhysicalSize`. + + - [cc8b1468](https://www.github.com/tauri-apps/tauri/commit/cc8b1468c821df53ceb771061c919409a9c80978) Fix(api): Window size and position returning wrong class (fix: [#2599](https://www.github.com/tauri-apps/tauri/pull/2599)) ([#2621](https://www.github.com/tauri-apps/tauri/pull/2621)) on 2021-09-22 + +- Change the `event` field of the `Event` interface to type `EventName` instead of `string`. + + - [b5d9bcb4](https://www.github.com/tauri-apps/tauri/commit/b5d9bcb402380abc86ae1fa1a77c629af2275f9d) Consistent event name usage ([#3228](https://www.github.com/tauri-apps/tauri/pull/3228)) on 2022-01-15 + - [62c7a8ad](https://www.github.com/tauri-apps/tauri/commit/62c7a8ad30fd3031b8679960590e5ef3eef8e4da) chore(covector): prepare for `rc` release ([#3376](https://www.github.com/tauri-apps/tauri/pull/3376)) on 2022-02-10 + +- Now `resolve()`, `join()` and `normalize()` from the `path` module, won't throw errors if the path doesn't exist, which matches NodeJS behavior. + + - [fe381a0b](https://www.github.com/tauri-apps/tauri/commit/fe381a0bde86ebf4014007f6e21af4c1a9e58cef) fix: `join` no longer cares if path doesn't exist, closes [#2499](https://www.github.com/tauri-apps/tauri/pull/2499) ([#2548](https://www.github.com/tauri-apps/tauri/pull/2548)) on 2021-09-21 + +- Fixes the dialog `defaultPath` usage on Linux. + + - [2212bd5d](https://www.github.com/tauri-apps/tauri/commit/2212bd5d75146f5a2df27cc2157a057642f626da) fix: dialog default path on Linux, closes [#3091](https://www.github.com/tauri-apps/tauri/pull/3091) ([#3123](https://www.github.com/tauri-apps/tauri/pull/3123)) on 2021-12-27 + +- Fixes `window.label` property returning null instead of the actual label. + + - [f5109e0c](https://www.github.com/tauri-apps/tauri/commit/f5109e0c962e3d25404995194968bade1be33b16) fix(api): window label null instead of actual value, closes [#3295](https://www.github.com/tauri-apps/tauri/pull/3295) ([#3332](https://www.github.com/tauri-apps/tauri/pull/3332)) on 2022-02-04 + +- Remove the `BaseDirectory::Current` enum variant for security reasons. + + - [696dca58](https://www.github.com/tauri-apps/tauri/commit/696dca58a9f8ee127a1cf857eb848e09f5845d18) refactor(core): remove `BaseDirectory::Current` variant on 2022-01-26 + +- Change `WindowLabel` type to `string`. + + - [f68603ae](https://www.github.com/tauri-apps/tauri/commit/f68603aee4e16500dff9e385b217f5dd8b1b39e8) chore(docs): simplify event system documentation on 2021-09-27 + +- When building Universal macOS Binaries through the virtual target `universal-apple-darwin`: + +- Expect a universal binary to be created by the user + +- Ensure that binary is bundled and accessed correctly at runtime + +- [3035e458](https://www.github.com/tauri-apps/tauri/commit/3035e4581c161ec7f0bd6d9b42e9015cf1dd1d77) Remove target triple from sidecar bin paths, closes [#3355](https://www.github.com/tauri-apps/tauri/pull/3355) ([#3356](https://www.github.com/tauri-apps/tauri/pull/3356)) on 2022-02-07 + +## \[1.0.0-beta.8] + +- Revert target back to ES5. + - [657c7dac](https://www.github.com/tauri-apps/tauri/commit/657c7dac734661956b87d021ff531ba530dd92a3) fix(api): revert ES2021 target on 2021-08-23 + +## \[1.0.0-beta.7] + +- Fix missing asset protocol path.Now the protocol is `https://asset.localhost/path/to/file` on Windows. Linux and macOS + is still `asset://path/to/file`. + - [994b5325](https://www.github.com/tauri-apps/tauri/commit/994b5325dd385f564b37fe1530c5d798dc925fff) fix: missing asset protocol path ([#2484](https://www.github.com/tauri-apps/tauri/pull/2484)) on 2021-08-23 + +## \[1.0.0-beta.6] + +- `bundle` now exports `clipboard` module so you can `import { clipboard } from "@tauri-apps/api"`. + - [4f88c3fb](https://www.github.com/tauri-apps/tauri/commit/4f88c3fb94286f3daafb906e3513c9210ecfa76b) fix(api.js): `bundle` now exports `clipboard` mod, closes [#2243](https://www.github.com/tauri-apps/tauri/pull/2243) ([#2244](https://www.github.com/tauri-apps/tauri/pull/2244)) on 2021-07-19 +- Fix double window creation + - [9fbcc024](https://www.github.com/tauri-apps/tauri/commit/9fbcc024542d87f71afd364acdcf2302cf82912c) fix(api.js): fix double window creation, closes [#2284](https://www.github.com/tauri-apps/tauri/pull/2284) ([#2285](https://www.github.com/tauri-apps/tauri/pull/2285)) on 2021-07-23 +- Add `os` module which exports `EOL`, `platform()`, `version()`, `type()`, `arch()`, `tempdir()` + - [05e679a6](https://www.github.com/tauri-apps/tauri/commit/05e679a6d2aca5642c780052bcf1384c49a462de) feat(api.js): add `os` module ([#2299](https://www.github.com/tauri-apps/tauri/pull/2299)) on 2021-07-28 +- - Add new nodejs-inspired functions which are `join`, `resolve`, `normalize`, `dirname`, `basename` and `extname`. +- Add `sep` and `delimiter` constants. +- Removed `resolvePath` API, use `resolve` instead. +- [05b9d81e](https://www.github.com/tauri-apps/tauri/commit/05b9d81ee6bcc920defca76cff00178b301fffe8) feat(api.js): add nodejs-inspired functions in `path` module ([#2310](https://www.github.com/tauri-apps/tauri/pull/2310)) on 2021-08-02 +- Change target to ES2021. + - [97bc52ee](https://www.github.com/tauri-apps/tauri/commit/97bc52ee03dec0b67cc1cced23305a4c53e9eb62) Tooling: \[API] Changed target in tsconfig to es6 ([#2362](https://www.github.com/tauri-apps/tauri/pull/2362)) on 2021-08-09 +- Add `toggleMaximize()` function to the `WebviewWindow` class. + - [1a510066](https://www.github.com/tauri-apps/tauri/commit/1a510066732d5f61c88c0ceed1c5f5cc559faf7d) fix(core): `data-tauri-drag-region` didn't respect resizable, closes [#2314](https://www.github.com/tauri-apps/tauri/pull/2314) ([#2316](https://www.github.com/tauri-apps/tauri/pull/2316)) on 2021-08-02 +- Fix `@ts-expect` error usage + - [dd52e738](https://www.github.com/tauri-apps/tauri/commit/dd52e738f1fd323bd8d185d6e650f412eb031200) fix(api.js): fix `@ts-expect-error` usage, closes [#2249](https://www.github.com/tauri-apps/tauri/pull/2249) ([#2250](https://www.github.com/tauri-apps/tauri/pull/2250)) on 2021-07-20 +- Fixes file drop events being swapped (`file-drop-hover` on drop and `file-drop` on hover). + - [c2b0fe1c](https://www.github.com/tauri-apps/tauri/commit/c2b0fe1ce58e54dbcfdb63162ad17d7e6d8774d9) fix(core): fix wrong file drop events ([#2300](https://www.github.com/tauri-apps/tauri/pull/2300)) on 2021-07-31 +- Fixes the global bundle UMD code. + - [268450b1](https://www.github.com/tauri-apps/tauri/commit/268450b1329a4b55f2043890c565a8563f890c3a) fix(api): global bundle broken code, closes [#2289](https://www.github.com/tauri-apps/tauri/pull/2289) ([#2297](https://www.github.com/tauri-apps/tauri/pull/2297)) on 2021-07-26 +- - Fixes monitor api not working. +- Fixes window.print() not working on macOS. +- [0f63f5e7](https://www.github.com/tauri-apps/tauri/commit/0f63f5e757873f1787a1ae07ca531340d0d45ec3) fix(api): Fix monitor functions, closes [#2294](https://www.github.com/tauri-apps/tauri/pull/2294) ([#2301](https://www.github.com/tauri-apps/tauri/pull/2301)) on 2021-07-29 +- Improve `EventName` type using `type-fest`'s `LiteralUnion`. + - [8e480297](https://www.github.com/tauri-apps/tauri/commit/8e48029790857b38988da4d291aa7458f51bb265) feat(api): improve `EventName` type definition ([#2379](https://www.github.com/tauri-apps/tauri/pull/2379)) on 2021-08-10 +- Update protocol url path with wry 0.12.1 on Windows. + - [88382fe1](https://www.github.com/tauri-apps/tauri/commit/88382fe147ebcb3f59308cc529e5562a04970876) chore(api): update protocol url path with wry 0.12.1 on Windows ([#2409](https://www.github.com/tauri-apps/tauri/pull/2409)) on 2021-08-13 + +## \[1.0.0-beta.5] + +- Adds `convertFileSrc` helper to the `tauri` module, simplifying the process of using file paths as webview source (`img`, `video`, etc). + - [51a5cfe4](https://www.github.com/tauri-apps/tauri/commit/51a5cfe4b5e9890fb6f639c9c929657fd747a595) feat(api): add `convertFileSrc` helper ([#2138](https://www.github.com/tauri-apps/tauri/pull/2138)) on 2021-07-02 +- You can now use `emit`, `listen` and `once` using the `appWindow` exported by the window module. + - [5d7626f8](https://www.github.com/tauri-apps/tauri/commit/5d7626f89781a6ebccceb9ab3b2e8335aa7a0392) feat(api): WindowManager extends WebviewWindowHandle, add events docs ([#2146](https://www.github.com/tauri-apps/tauri/pull/2146)) on 2021-07-03 +- Allow manipulating a spawned window directly using `WebviewWindow`, which now extends `WindowManager`. + - [d69b1cf6](https://www.github.com/tauri-apps/tauri/commit/d69b1cf6d7c13297073073d753e30fe1a22a09cb) feat(api): allow managing windows created on JS ([#2154](https://www.github.com/tauri-apps/tauri/pull/2154)) on 2021-07-05 + +## \[1.0.0-beta.4] + +- Add asset custom protocol to access local file system. + - [ee60e424](https://www.github.com/tauri-apps/tauri/commit/ee60e424221559d3d725716b0003c5566ef2b5cd) feat: asset custom protocol to access local file system ([#2104](https://www.github.com/tauri-apps/tauri/pull/2104)) on 2021-06-28 + +## \[1.0.0-beta.3] + +- Export `Response` and `ResponseType` as value instead of type. + - [394b6e05](https://www.github.com/tauri-apps/tauri/commit/394b6e0572e7a0a92e103e462a7f603f7d569319) fix(api): http `ResponseType` export type error ([#2065](https://www.github.com/tauri-apps/tauri/pull/2065)) on 2021-06-24 + +## \[1.0.0-beta.2] + +- Export `BaseDirectory` in `path` module + - [277f5ca5](https://www.github.com/tauri-apps/tauri/commit/277f5ca5a8ae227bbdccee1ad52bdd88b4a5b11b) feat(api): export `BaseDirectory` in `path` module ([#1885](https://www.github.com/tauri-apps/tauri/pull/1885)) on 2021-05-30 +- Use `export type` to export TS types, enums and interfaces. + - [9a662d26](https://www.github.com/tauri-apps/tauri/commit/9a662d2601b01d712c6bd205f8db1b674f56dfa7) fix: Monitor if --isolatedModules is enabled ([#1825](https://www.github.com/tauri-apps/tauri/pull/1825)) on 2021-05-13 + - [612cd8ec](https://www.github.com/tauri-apps/tauri/commit/612cd8ecb8e02954f3696b9e138cbc7d2c228fad) feat(api): finalize `export type` usage ([#1847](https://www.github.com/tauri-apps/tauri/pull/1847)) on 2021-05-17 +- Adds `focus?: boolean` to the WindowOptions interface. + - [5f351622](https://www.github.com/tauri-apps/tauri/commit/5f351622c7812ad1bb56ddb37364ccaa4124c24b) feat(core): add focus API to the WindowBuilder and WindowOptions, [#1737](https://www.github.com/tauri-apps/tauri/pull/1737) on 2021-05-30 +- Adds `isDecorated` getter on the window API. + - [f58a2114](https://www.github.com/tauri-apps/tauri/commit/f58a2114fbfd5307c349f05c88f2e08fd8baa8aa) feat(core): add `is_decorated` Window getter on 2021-05-30 +- Adds `isResizable` getter on the window API. + - [1e8af280](https://www.github.com/tauri-apps/tauri/commit/1e8af280c27f381828d6209722b10e889082fa00) feat(core): add `is_resizable` Window getter on 2021-05-30 +- Adds `isVisible` getter on the window API. + - [36506c96](https://www.github.com/tauri-apps/tauri/commit/36506c967de82bc7ff453d11e6104ecf66d7a588) feat(core): add `is_visible` API on 2021-05-30 +- Adds `requestUserAttention` API to the `window` module. + - [7dcca6e9](https://www.github.com/tauri-apps/tauri/commit/7dcca6e9281182b11ad3d4a79871f09b30b9b419) feat(core): add `request_user_attention` API, closes [#2023](https://www.github.com/tauri-apps/tauri/pull/2023) ([#2026](https://www.github.com/tauri-apps/tauri/pull/2026)) on 2021-06-20 +- Adds `setFocus` to the window API. + - [bb6992f8](https://www.github.com/tauri-apps/tauri/commit/bb6992f888196ca7c87bb2fe74ad2bd8bf393e05) feat(core): add `set_focus` window API, fixes [#1737](https://www.github.com/tauri-apps/tauri/pull/1737) on 2021-05-30 +- Adds `setSkipTaskbar` to the window API. + - [e06aa277](https://www.github.com/tauri-apps/tauri/commit/e06aa277384450cfef617c0e57b0d5d403bb1e7f) feat(core): add `set_skip_taskbar` API on 2021-05-30 +- Adds `skipTaskbar?: boolean` to the WindowOptions interface. + - [5525b03a](https://www.github.com/tauri-apps/tauri/commit/5525b03a78a2232c650043fbd9894ce1553cad41) feat(core): add `skip_taskbar` API to the WindowBuilder/WindowOptions on 2021-05-30 +- Adds `center?: boolean` to `WindowOptions` and `center()` API to the `appWindow`. + - [5cba6eb4](https://www.github.com/tauri-apps/tauri/commit/5cba6eb4d28d53f06855d60d4d0eae6b95233ccf) feat(core): add window `center` API, closes [#1822](https://www.github.com/tauri-apps/tauri/pull/1822) ([#1954](https://www.github.com/tauri-apps/tauri/pull/1954)) on 2021-06-05 +- Adds `clipboard` APIs (write and read text). + - [285bf64b](https://www.github.com/tauri-apps/tauri/commit/285bf64bf9569efb2df904c69c6df405ff0d62e2) feat(core): add clipboard writeText and readText APIs ([#2035](https://www.github.com/tauri-apps/tauri/pull/2035)) on 2021-06-21 + - [dee71ad5](https://www.github.com/tauri-apps/tauri/commit/dee71ad58349f699995cc9077b79032bacc6afcb) fix(workflows): update docs workflow syntax ([#2054](https://www.github.com/tauri-apps/tauri/pull/2054)) on 2021-06-23 +- The `http` APIs now resolve the returned promise when the API call finishes with an error status code. + - [47f75584](https://www.github.com/tauri-apps/tauri/commit/47f7558417cc654bdb1d018127e8900bc4eac622) fix(core): resolve HTTP API on non-ok status code, fix binary response, closes [#2046](https://www.github.com/tauri-apps/tauri/pull/2046) ([#2053](https://www.github.com/tauri-apps/tauri/pull/2053)) on 2021-06-23 +- Improve RPC security by requiring a numeric code to invoke commands. The codes are generated by the Rust side and injected into the app's code using a closure, so external scripts can't access the backend. This change doesn't protect `withGlobalTauri` (`window.__TAURI__`) usage. + - [160fb052](https://www.github.com/tauri-apps/tauri/commit/160fb0529fd31d755574ae30fbdf01fa221a2acb) feat(core): improve RPC security, closes [#814](https://www.github.com/tauri-apps/tauri/pull/814) ([#2047](https://www.github.com/tauri-apps/tauri/pull/2047)) on 2021-06-22 +- Mark the `WebviewWindow` constructor as public. + - [4aeb936e](https://www.github.com/tauri-apps/tauri/commit/4aeb936e9b60b895d383597dc698ee5d638436f9) fix(api): `WebviewWindow` constructor is public ([#1888](https://www.github.com/tauri-apps/tauri/pull/1888)) on 2021-05-21 +- Validate arguments on the window `setLocation`, `setSize`, `setMinSize` and `setMaxSize` API. + - [7616e6cc](https://www.github.com/tauri-apps/tauri/commit/7616e6cc7bcd49f688b0d00fdc33c94b7b93713d) feat(api): validate window API `size` and `location` arguments ([#1846](https://www.github.com/tauri-apps/tauri/pull/1846)) on 2021-05-17 + +## \[1.0.0-beta.1] + +- Adds `package.json` to the `exports` object. + - [ab1ea96](https://www.github.com/tauri-apps/tauri/commit/ab1ea964786e1781c922582b059c555b6072f1a0) chore(api): add `package.json` to the `exports` field ([#1807](https://www.github.com/tauri-apps/tauri/pull/1807)) on 2021-05-12 + +## \[1.0.0-beta.0] + +- CommonJS chunks are now properly exported with `.cjs` extension + - [ddcd923](https://www.github.com/tauri-apps/tauri/commit/ddcd9233bd6f499aa7f22484d6c151b01778bc1b) fix(api): export commonjs chunks with `.cjs` extension, fix [#1625](https://www.github.com/tauri-apps/tauri/pull/1625) ([#1627](https://www.github.com/tauri-apps/tauri/pull/1627)) on 2021-04-26 +- Adds `transparent?: boolean` to the `WindowOptions` interface. + - [08c1c5c](https://www.github.com/tauri-apps/tauri/commit/08c1c5ca5c0ebe17ea98689a5fe3b7e47a98e955) fix(api): missing `transparent` flag on `WindowOptions` ([#1764](https://www.github.com/tauri-apps/tauri/pull/1764)) on 2021-05-10 +- Adds `options` argument to the shell command API (`env` and `cwd` configuration). + - [721e98f](https://www.github.com/tauri-apps/tauri/commit/721e98f175567b360c86f30565ab1b9d08e7cf85) feat(core): add env, cwd to the command API, closes [#1634](https://www.github.com/tauri-apps/tauri/pull/1634) ([#1635](https://www.github.com/tauri-apps/tauri/pull/1635)) on 2021-04-28 +- Adds `startDragging` API on the window module. + - [c31f097](https://www.github.com/tauri-apps/tauri/commit/c31f0978c535f794fffb75a121e69a323e70b06e) refactor: update to wry 0.9 ([#1630](https://www.github.com/tauri-apps/tauri/pull/1630)) on 2021-04-28 +- Move `exit` and `relaunch` APIs from `app` to `process` module. + - [b0bb796](https://www.github.com/tauri-apps/tauri/commit/b0bb796a42e2560233aea47ce6ced54ac238eb53) refactor: rename `command` mod to `process`, move restart_application ([#1667](https://www.github.com/tauri-apps/tauri/pull/1667)) on 2021-04-30 +- The window management API was refactored: removed `setX`, `setY`, `setWidth`, `setHeight` APIs, renamed `resize` to `setSize` and the size and position APIs now allow defining both logical and physical values. + - [6bfac86](https://www.github.com/tauri-apps/tauri/commit/6bfac866a703f1499a64237fb29b2625703f4e22) refactor(core): add window getters, physical & logical sizes/positions ([#1723](https://www.github.com/tauri-apps/tauri/pull/1723)) on 2021-05-05 +- Adds window getters. + - [6bfac86](https://www.github.com/tauri-apps/tauri/commit/6bfac866a703f1499a64237fb29b2625703f4e22) refactor(core): add window getters, physical & logical sizes/positions ([#1723](https://www.github.com/tauri-apps/tauri/pull/1723)) on 2021-05-05 + +## \[1.0.0-beta-rc.3] + +- Fixes distribution of the `@tauri-apps/api` package for older bundlers. + - [7f998d0](https://www.github.com/tauri-apps/tauri/commit/7f998d08e3ab8823c99190fa283bdfa2c4f2749b) fix(api): distribution ([#1582](https://www.github.com/tauri-apps/tauri/pull/1582)) on 2021-04-22 +- Update minimum Node.js version to v12.13.0 + - [1f089fb](https://www.github.com/tauri-apps/tauri/commit/1f089fb4f964c673dcab5784bdf1da2833487a7c) chore: update minimum nodejs version to 12.13.0 ([#1562](https://www.github.com/tauri-apps/tauri/pull/1562)) on 2021-04-21 + +## \[1.0.0-beta-rc.2] + +- TS was wrongly re-exporting the module. + - [fcb3b48](https://www.github.com/tauri-apps/tauri/commit/fcb3b4857efa17d2a3717f32457e88b24520cc9b) fix: [#1512](https://www.github.com/tauri-apps/tauri/pull/1512) ([#1517](https://www.github.com/tauri-apps/tauri/pull/1517)) on 2021-04-19 + - [ae14a3f](https://www.github.com/tauri-apps/tauri/commit/ae14a3ff51a742b6ab6f76bbfc21f385310f1dc6) fix: [#1517](https://www.github.com/tauri-apps/tauri/pull/1517) had the wrong package reference in the changefile ([#1538](https://www.github.com/tauri-apps/tauri/pull/1538)) on 2021-04-19 + +## \[1.0.0-beta-rc.1] + +- Missing the `files` property in the package.json which mean that the `dist` directory was not published and used. + - [b2569a7](https://www.github.com/tauri-apps/tauri/commit/b2569a729a3caa88bdba62abc31f0665e1323aaa) fix(js-api): dist ([#1498](https://www.github.com/tauri-apps/tauri/pull/1498)) on 2021-04-15 + +## \[1.0.0-beta-rc.0] + +- Add current working directory to the path api module. + - [52c2baf](https://www.github.com/tauri-apps/tauri/commit/52c2baf940773cf7c51647fb6f20d0f7df126115) feat: add current working directory to path api module ([#1375](https://www.github.com/tauri-apps/tauri/pull/1375)) on 2021-03-23 + - [a6def70](https://www.github.com/tauri-apps/tauri/commit/a6def7066eec19c889b0f14cc1e475bf209a332e) Refactor(tauri): move tauri-api and tauri-updater to tauri ([#1455](https://www.github.com/tauri-apps/tauri/pull/1455)) on 2021-04-11 +- The shell process spawning API was rewritten and now includes stream access. + - [3713066](https://www.github.com/tauri-apps/tauri/commit/3713066e451bd30d0cc6f57bb437f08276f4c4ad) refactor(core): rewrite shell execute API, closes [#1229](https://www.github.com/tauri-apps/tauri/pull/1229) ([#1408](https://www.github.com/tauri-apps/tauri/pull/1408)) on 2021-03-31 +- The file dialog API now uses [rfd](https://github.com/PolyMeilex/rfd). The filter option is now an array of `{ name: string, extensions: string[] }`. + - [2326bcd](https://www.github.com/tauri-apps/tauri/commit/2326bcd399411f7f0eabdb7ade910be473adadae) refactor(core): use `nfd` for file dialogs, closes [#1251](https://www.github.com/tauri-apps/tauri/pull/1251) ([#1257](https://www.github.com/tauri-apps/tauri/pull/1257)) on 2021-02-18 + - [a6def70](https://www.github.com/tauri-apps/tauri/commit/a6def7066eec19c889b0f14cc1e475bf209a332e) Refactor(tauri): move tauri-api and tauri-updater to tauri ([#1455](https://www.github.com/tauri-apps/tauri/pull/1455)) on 2021-04-11 +- The HTTP API was improved with client caching and better payload and response types. + - [a7bc472](https://www.github.com/tauri-apps/tauri/commit/a7bc472e994730071f960d09a12ac85296a080ae) refactor(core): improve HTTP API, closes [#1098](https://www.github.com/tauri-apps/tauri/pull/1098) ([#1237](https://www.github.com/tauri-apps/tauri/pull/1237)) on 2021-02-15 + - [a6def70](https://www.github.com/tauri-apps/tauri/commit/a6def7066eec19c889b0f14cc1e475bf209a332e) Refactor(tauri): move tauri-api and tauri-updater to tauri ([#1455](https://www.github.com/tauri-apps/tauri/pull/1455)) on 2021-04-11 +- Update all code files to have our license header. + - [bf82136](https://www.github.com/tauri-apps/tauri/commit/bf8213646689175f8a158b956911f3a43e360690) feat(license): SPDX Headers ([#1449](https://www.github.com/tauri-apps/tauri/pull/1449)) on 2021-04-11 + - [a6def70](https://www.github.com/tauri-apps/tauri/commit/a6def7066eec19c889b0f14cc1e475bf209a332e) Refactor(tauri): move tauri-api and tauri-updater to tauri ([#1455](https://www.github.com/tauri-apps/tauri/pull/1455)) on 2021-04-11 + - [aea6145](https://www.github.com/tauri-apps/tauri/commit/aea614587bddab930d552512b54e18624fbf573e) refactor(repo): add /tooling folder ([#1457](https://www.github.com/tauri-apps/tauri/pull/1457)) on 2021-04-12 +- Use secure RNG on callback function names. + - [c8992bb](https://www.github.com/tauri-apps/tauri/commit/c8992bb0bfb8eaeae8ebed444719f9c9372d39d4) refactor(api): use secure RNG, closes [#1356](https://www.github.com/tauri-apps/tauri/pull/1356) ([#1398](https://www.github.com/tauri-apps/tauri/pull/1398)) on 2021-03-30 +- The invoke function can now be called with the cmd as the first parameter and the args as the second. + - [427d170](https://www.github.com/tauri-apps/tauri/commit/427d170930ab711fd0ca82f7a73b524d6fdc222f) feat(api/invoke): separate cmd arg ([#1321](https://www.github.com/tauri-apps/tauri/pull/1321)) on 2021-03-04 +- Adds a global shortcut API. + - [855effa](https://www.github.com/tauri-apps/tauri/commit/855effadd9ebfb6bc1a3555ac7fc733f6f766b7a) feat(core): globalShortcut API ([#1232](https://www.github.com/tauri-apps/tauri/pull/1232)) on 2021-02-14 + - [a6def70](https://www.github.com/tauri-apps/tauri/commit/a6def7066eec19c889b0f14cc1e475bf209a332e) Refactor(tauri): move tauri-api and tauri-updater to tauri ([#1455](https://www.github.com/tauri-apps/tauri/pull/1455)) on 2021-04-11 +- Added window management and window creation APIs. + - [a3d6dff](https://www.github.com/tauri-apps/tauri/commit/a3d6dff2163c7a45842253edd81dbc62248dc65d) feat(core): window API ([#1225](https://www.github.com/tauri-apps/tauri/pull/1225)) on 2021-02-13 + - [641374b](https://www.github.com/tauri-apps/tauri/commit/641374b15343518cd835bd5ada811941c65dcf2e) feat(core): window creation at runtime ([#1249](https://www.github.com/tauri-apps/tauri/pull/1249)) on 2021-02-17 diff --git a/packages/api/LICENSE-APACHE-2.0 b/packages/api/LICENSE-APACHE-2.0 new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/packages/api/LICENSE-APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/packages/api/LICENSE-MIT b/packages/api/LICENSE-MIT new file mode 100644 index 0000000..b08530d --- /dev/null +++ b/packages/api/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/api/README.md b/packages/api/README.md new file mode 100644 index 0000000..d74ab83 --- /dev/null +++ b/packages/api/README.md @@ -0,0 +1,52 @@ +# @tauri-apps/api + + + +[![status](https://img.shields.io/badge/status-stable-blue.svg)](https://github.com/tauri-apps/tauri/tree/dev) +[![License](https://img.shields.io/badge/License-MIT%20or%20Apache%202-green.svg)](https://opencollective.com/tauri) +[![lint js](https://img.shields.io/github/actions/workflow/status/tauri-apps/tauri/lint-js.yml?label=lint%20js&logo=github)](https://github.com/tauri-apps/tauri/actions/workflows/lint-js.yml) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri?ref=badge_shield) +[![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/SpmNs4S) +[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app) +[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation) +[![support](https://img.shields.io/badge/sponsor-Open%20Collective-blue.svg)](https://opencollective.com/tauri) + +| Component | Version | +| --------------- | ----------------------------------------------------- | +| @tauri-apps/api | ![](https://img.shields.io/npm/v/@tauri-apps/api.svg) | + +## About Tauri + +Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily. + +Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task. + +## This module + +This is a typescript library that creates `cjs` and `esm` JavaScript endpoints for you to import into your Frontend framework so that the Webview can call and listen to backend activity. We also ship the pure typescript, because for some frameworks this is more optimal. It uses the message passing of webviews to their hosts. + +To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document. + +## Installation + +The preferred method is to install this module locally as a dependency: + +``` +$ pnpm add @tauri-apps/api +$ yarn add @tauri-apps/api +$ npm add @tauri-apps/api +``` + +## Semver + +**tauri** is following [Semantic Versioning 2.0](https://semver.org/). + +## Licenses + +Code: (c) 2019 - 2021 - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. + +Logo: CC-BY-NC-ND + +- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum) diff --git a/packages/api/eslint.config.js b/packages/api/eslint.config.js new file mode 100644 index 0000000..803c017 --- /dev/null +++ b/packages/api/eslint.config.js @@ -0,0 +1,61 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import globals from 'globals' +import eslint from '@eslint/js' +import prettierConfig from 'eslint-config-prettier' +import securityPlugin from 'eslint-plugin-security' +import tseslint from 'typescript-eslint' + +/** @type {import('eslint').Linter.Config} */ +export default [ + eslint.configs.recommended, + prettierConfig, + securityPlugin.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + __statics: true, + process: true + }, + parserOptions: { + project: true, + tsconfigRootDir: import.meta.dirname + } + }, + rules: { + 'no-console': 'error', + 'no-debugger': 'error', + 'no-process-exit': 'off', + 'security/detect-non-literal-fs-filename': 'warn', + 'security/detect-unsafe-regex': 'error', + 'security/detect-buffer-noassert': 'error', + 'security/detect-child-process': 'warn', + 'security/detect-disable-mustache-escape': 'error', + 'security/detect-eval-with-expression': 'error', + 'security/detect-no-csrf-before-method-override': 'error', + 'security/detect-non-literal-regexp': 'error', + 'security/detect-non-literal-require': 'warn', + 'security/detect-object-injection': 'warn', + 'security/detect-possible-timing-attacks': 'error', + 'security/detect-pseudoRandomBytes': 'error', + 'space-before-function-paren': 'off', + '@typescript-eslint/default-param-last': 'off', + '@typescript-eslint/strict-boolean-expressions': 0, + 'no-return-await': 'warn', + '@typescript-eslint/return-await': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + } + ] + } + } +] diff --git a/packages/api/package.json b/packages/api/package.json new file mode 100644 index 0000000..2da80fa --- /dev/null +++ b/packages/api/package.json @@ -0,0 +1,63 @@ +{ + "name": "@tauri-apps/api", + "version": "2.11.1", + "description": "Tauri API definitions", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tauri-apps/tauri.git" + }, + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "license": "Apache-2.0 OR MIT", + "bugs": { + "url": "https://github.com/tauri-apps/tauri/issues" + }, + "homepage": "https://github.com/tauri-apps/tauri#readme", + "type": "module", + "main": "./index.cjs", + "module": "./index.js", + "types": "./index.d.ts", + "exports": { + ".": { + "import": "./index.js", + "require": "./index.cjs", + "types": "./index.d.ts" + }, + "./*": { + "import": "./*.js", + "require": "./*.cjs", + "types": "./*.d.ts" + }, + "./package.json": "./package.json" + }, + "scripts": { + "build": "rollup -c --configPlugin typescript", + "build:debug": "rollup -c --configPlugin typescript", + "npm-pack": "pnpm build && cd ./dist && npm pack", + "npm-publish": "pnpm build && cd ./dist && pnpm publish --access public --loglevel silly --no-git-checks", + "ts:check": "tsc --noEmit", + "eslint:check": "eslint src/**/*.ts", + "eslint:fix": "eslint src/**/*.ts --fix" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@rollup/plugin-terser": "1.0.0", + "@rollup/plugin-typescript": "12.3.0", + "@types/eslint": "^9.6.1", + "@types/node": "^24.11.0", + "eslint": "^10.0.2", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-security": "4.0.1", + "fast-glob": "3.3.3", + "globals": "^17.4.0", + "rollup": "4.62.4", + "tslib": "^2.8.1", + "typescript": "^6.0.3", + "typescript-eslint": "^8.58.2" + } +} diff --git a/packages/api/rollup.config.ts b/packages/api/rollup.config.ts new file mode 100644 index 0000000..bc0288e --- /dev/null +++ b/packages/api/rollup.config.ts @@ -0,0 +1,117 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { defineConfig, Plugin, RollupLog } from 'rollup' +import typescript from '@rollup/plugin-typescript' +import terser from '@rollup/plugin-terser' +import fg from 'fast-glob' +import { basename, dirname, join } from 'path' +import { copyFileSync, opendirSync, rmSync, Dir } from 'fs' +import { fileURLToPath } from 'url' + +// cleanup dist dir +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +cleanDir(join(__dirname, './dist')) + +const modules = fg.sync(['!./src/*.d.ts', './src/*.ts']) + +export default defineConfig([ + { + input: Object.fromEntries(modules.map((p) => [basename(p, '.ts'), p])), + output: [ + { + format: 'esm', + dir: './dist', + preserveModules: true, + preserveModulesRoot: 'src', + entryFileNames: (chunkInfo) => { + if (chunkInfo.name.includes('node_modules')) { + return externalLibPath(chunkInfo.name) + '.js' + } + + return '[name].js' + } + }, + { + format: 'cjs', + dir: './dist', + preserveModules: true, + preserveModulesRoot: 'src', + entryFileNames: (chunkInfo) => { + if (chunkInfo.name.includes('node_modules')) { + return externalLibPath(chunkInfo.name) + '.cjs' + } + + return '[name].cjs' + } + } + ], + plugins: [ + typescript({ + declaration: true, + declarationDir: './dist', + rootDir: 'src' + }), + makeFlatPackageInDist() + ], + onwarn + }, + + { + input: 'src/index.ts', + output: { + format: 'iife', + name: '__TAURI_IIFE__', + footer: 'window.__TAURI__ = __TAURI_IIFE__', + file: '../../crates/tauri/scripts/bundle.global.js' + }, + plugins: [typescript(), terser()], + onwarn + } +]) + +function externalLibPath(path: string) { + return `external/${basename(dirname(path))}/${basename(path)}` +} + +function onwarn(warning: RollupLog) { + // deny warnings by default + throw Object.assign(new Error(), warning) +} + +function makeFlatPackageInDist(): Plugin { + return { + name: 'makeFlatPackageInDist', + writeBundle() { + // copy necessary files like `CHANGELOG.md` , `README.md` and Licenses to `./dist` + fg.sync('(LICENSE*|*.md|package.json)').forEach((f) => + copyFileSync(f, `dist/${f}`) + ) + } + } +} + +function cleanDir(path: string) { + let dir: Dir + try { + dir = opendirSync(path) + } catch (err: any) { + switch (err.code) { + case 'ENOENT': + return // Noop when directory don't exists. + case 'ENOTDIR': + throw new Error(`'${path}' is not a directory.`) + default: + throw err + } + } + + let file = dir.readSync() + while (file) { + const filePath = join(path, file.name) + rmSync(filePath, { recursive: true }) + file = dir.readSync() + } + dir.closeSync() +} diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts new file mode 100644 index 0000000..77692fd --- /dev/null +++ b/packages/api/src/app.ts @@ -0,0 +1,421 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Application metadata and lifecycle APIs: version and identifier, theme and dock + * visibility, data stores and exiting the app. + * + * This package is also accessible with `window.__TAURI__.app` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * + * @remarks Only the read-only commands are enabled by `core:app:default` + * (`allow-version`, `allow-name`, `allow-tauri-version`, `allow-identifier`, + * `allow-bundle-type`, `allow-supports-multiple-windows`, `allow-register-listener` + * and `allow-remove-listener`). Every other function in this module documents the + * permission it needs, which you must add to a capability yourself. + * + * @module + */ + +import { addPluginListener, invoke, PluginListener } from './core' +import { Image } from './image' +import { Theme } from './window' + +/** + * Identifier type used for data stores on macOS and iOS. + * + * Represents a 128-bit identifier, commonly expressed as a 16-byte UUID. + * + * @since 2.4.0 + */ +export type DataStoreIdentifier = [ + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number +] + +/** + * Bundle type of the current application. + * + * @see {@linkcode getBundleType} + * + * @since 2.7.0 + */ +export enum BundleType { + /** Windows NSIS */ + Nsis = 'nsis', + /** Windows MSI */ + Msi = 'msi', + /** Linux Debian package */ + Deb = 'deb', + /** Linux RPM */ + Rpm = 'rpm', + /** Linux AppImage */ + AppImage = 'appimage', + /** macOS app bundle */ + App = 'app' +} + +/** + * Gets the application version. + * @example + * ```typescript + * import { getVersion } from '@tauri-apps/api/app'; + * const appVersion = await getVersion(); + * ``` + * + * @since 1.0.0 + */ +async function getVersion(): Promise { + return invoke('plugin:app|version') +} + +/** + * Gets the application name. + * @example + * ```typescript + * import { getName } from '@tauri-apps/api/app'; + * const appName = await getName(); + * ``` + * + * @since 1.0.0 + */ +async function getName(): Promise { + return invoke('plugin:app|name') +} + +/** + * Gets the Tauri framework version used by this application. + * + * @example + * ```typescript + * import { getTauriVersion } from '@tauri-apps/api/app'; + * const tauriVersion = await getTauriVersion(); + * ``` + * + * @since 1.0.0 + */ +async function getTauriVersion(): Promise { + return invoke('plugin:app|tauri_version') +} + +/** + * Gets the application identifier. + * @example + * ```typescript + * import { getIdentifier } from '@tauri-apps/api/app'; + * const identifier = await getIdentifier(); + * ``` + * + * @returns The application identifier as configured in `tauri.conf.json`. + * + * @since 2.4.0 + */ +async function getIdentifier(): Promise { + return invoke('plugin:app|identifier') +} + +/** + * Shows the application on macOS. This function does not automatically + * focus any specific app window. + * + * @example + * ```typescript + * import { show } from '@tauri-apps/api/app'; + * await show(); + * ``` + * + * @remarks Requires the `core:app:allow-app-show` permission (not included in `core:app:default`). + * + * @since 1.2.0 + */ +async function show(): Promise { + return invoke('plugin:app|app_show') +} + +/** + * Hides the application on macOS. + * + * @example + * ```typescript + * import { hide } from '@tauri-apps/api/app'; + * await hide(); + * ``` + * + * @remarks Requires the `core:app:allow-app-hide` permission (not included in `core:app:default`). + * + * @since 1.2.0 + */ +async function hide(): Promise { + return invoke('plugin:app|app_hide') +} + +/** + * Fetches the data store identifiers on macOS and iOS. + * + * See https://developer.apple.com/documentation/webkit/wkwebsitedatastore for more information. + * + * @example + * ```typescript + * import { fetchDataStoreIdentifiers } from '@tauri-apps/api/app'; + * const ids = await fetchDataStoreIdentifiers(); + * ``` + * + * @remarks Requires the `core:app:allow-fetch-data-store-identifiers` permission + * (not included in `core:app:default`). + * + * @since 2.4.0 + */ +async function fetchDataStoreIdentifiers(): Promise { + return invoke('plugin:app|fetch_data_store_identifiers') +} + +/** + * Removes the data store with the given identifier. + * + * Note that any webview using this data store should be closed before running this API. + * + * See https://developer.apple.com/documentation/webkit/wkwebsitedatastore for more information. + * + * @example + * ```typescript + * import { fetchDataStoreIdentifiers, removeDataStore } from '@tauri-apps/api/app'; + * for (const id of (await fetchDataStoreIdentifiers())) { + * await removeDataStore(id); + * } + * ``` + * + * @remarks Requires the `core:app:allow-remove-data-store` permission (not included + * in `core:app:default`). + * + * @since 2.4.0 + */ +async function removeDataStore(uuid: DataStoreIdentifier): Promise { + return invoke('plugin:app|remove_data_store', { uuid }) +} + +/** + * Gets the default window icon. + * + * @example + * ```typescript + * import { defaultWindowIcon } from '@tauri-apps/api/app'; + * const icon = await defaultWindowIcon(); + * ``` + * + * @remarks Requires the `core:app:allow-default-window-icon` permission (not + * included in `core:app:default`). + * + * @since 2.0.0 + */ + +async function defaultWindowIcon(): Promise { + return invoke('plugin:app|default_window_icon').then((rid) => + rid ? new Image(rid) : null + ) +} + +/** + * Sets the application's theme. Pass in `null` or `undefined` to follow + * the system theme. + * + * @example + * ```typescript + * import { setTheme } from '@tauri-apps/api/app'; + * await setTheme('dark'); + * ``` + * + * #### Platform-specific + * + * - **iOS / Android:** Unsupported. + * + * @remarks Requires the `core:app:allow-set-app-theme` permission (not included in + * `core:app:default`). + * + * @since 2.0.0 + */ +async function setTheme(theme?: Theme | null): Promise { + return invoke('plugin:app|set_app_theme', { theme }) +} + +/** + * Sets the dock visibility for the application on macOS. + * + * @param visible - Whether the dock should be visible or not. + * + * @example + * ```typescript + * import { setDockVisibility } from '@tauri-apps/api/app'; + * await setDockVisibility(false); + * ``` + * + * @remarks Requires the `core:app:allow-set-dock-visibility` permission (not + * included in `core:app:default`). + * + * @since 2.5.0 + */ +async function setDockVisibility(visible: boolean): Promise { + return invoke('plugin:app|set_dock_visibility', { visible }) +} + +/** + * Gets the application bundle type. + * + * @example + * ```typescript + * import { getBundleType } from '@tauri-apps/api/app'; + * const type = await getBundleType(); + * ``` + * + * @since 2.5.0 + */ +async function getBundleType(): Promise { + return invoke('plugin:app|bundle_type') +} + +/** + * Payload for the onBackButtonPress event. + * + * @since 2.9.0 + */ +type OnBackButtonPressPayload = { + /** Whether the webview canGoBack property is true. */ + canGoBack: boolean +} + +/** + * Listens to the Android hardware/gesture back button. + * + * Registering a handler takes over the default behavior, so the app no longer + * navigates back or closes on its own: decide what to do inside the handler, + * using `payload.canGoBack` to know whether the webview has history to go back to. + * + * #### Platform-specific + * + * - **Android:** Supported. + * - **Windows / Linux / macOS / iOS:** Unsupported, the handler is never called. + * + * @example + * ```typescript + * import { onBackButtonPress } from '@tauri-apps/api/app'; + * import { exit } from '@tauri-apps/api/app'; + * + * const listener = await onBackButtonPress(({ canGoBack }) => { + * if (canGoBack) { + * window.history.back(); + * } else { + * void exit(0); + * } + * }); + * + * // stop handling the back button + * await listener.unregister(); + * ``` + * + * @param handler Called on every back button press. + * @returns A listener handle, call `unregister()` on it to restore the default behavior. + * + * @since 2.9.0 + */ +async function onBackButtonPress( + handler: (payload: OnBackButtonPressPayload) => void +): Promise { + return addPluginListener( + 'app', + 'back-button', + handler + ) +} + +/** + * Whether the current platform can show more than one window at a time. + * + * Use it to hide or disable multi-window features on platforms where creating a + * second window is not possible. + * + * #### Platform-specific + * + * - **Windows / Linux / macOS:** Always `true`. + * - **Android:** `true` on API level 32 (Android 12L) and above. + * - **iOS:** Reflects [`UIApplication.supportsMultipleScenes`](https://developer.apple.com/documentation/uikit/uiapplication/supportsmultiplescenes), + * so it is `true` on iPadOS and `false` on iPhone. + * + * See the [mobile multiwindow guide](https://tauri.app/learn/mobile-multiwindow/) + * for how windows behave on mobile. + * + * @example + * ```typescript + * import { supportsMultipleWindows } from '@tauri-apps/api/app'; + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * + * if (await supportsMultipleWindows()) { + * new WebviewWindow('settings', { url: '/settings' }); + * } + * ``` + * + * @since 2.11.0 + */ +async function supportsMultipleWindows(): Promise { + return invoke('plugin:app|supports_multiple_windows') +} + +/** + * Exits the app with the given exit code. + * + * This is the same as the `exit` function of the `@tauri-apps/plugin-process` plugin, + * but does not require a plugin to be installed. + * + * #### Platform-specific + * + * - **Android**: The activity is finished instead of the process being killed, + * so the app closes with the system transition; `code` is ignored. + * + * @example + * ```typescript + * import { exit } from '@tauri-apps/api/app'; + * await exit(1); + * ``` + * + * @param code The exit code to use. Defaults to `0`. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:app:allow-exit` permission (not included in + * `core:app:default`). + * + * @since 2.12.0 + */ +async function exit(code = 0): Promise { + return invoke('plugin:app|exit', { code }) +} + +export { + getName, + getVersion, + getTauriVersion, + getIdentifier, + show, + hide, + defaultWindowIcon, + setTheme, + fetchDataStoreIdentifiers, + removeDataStore, + setDockVisibility, + getBundleType, + type OnBackButtonPressPayload, + onBackButtonPress, + supportsMultipleWindows, + exit +} diff --git a/packages/api/src/core.ts b/packages/api/src/core.ts new file mode 100644 index 0000000..17750c6 --- /dev/null +++ b/packages/api/src/core.ts @@ -0,0 +1,610 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Invoke your custom commands. + * + * This package is also accessible with `window.__TAURI__.core` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * @module + */ + +/** + * A key to be used to implement a special function + * on your types that define how your type should be serialized + * when passing across the IPC. + * @example + * Given a type in Rust that looks like this + * ```rs + * #[derive(serde::Serialize, serde::Deserialize) + * enum UserId { + * String(String), + * Number(u32), + * } + * ``` + * `UserId::String("id")` would be serialized into `{ String: "id" }` + * and so we need to pass the same structure back to Rust + * ```ts + * import { SERIALIZE_TO_IPC_FN } from "@tauri-apps/api/core" + * + * class UserIdString { + * id + * constructor(id) { + * this.id = id + * } + * + * [SERIALIZE_TO_IPC_FN]() { + * return { String: this.id } + * } + * } + * + * class UserIdNumber { + * id + * constructor(id) { + * this.id = id + * } + * + * [SERIALIZE_TO_IPC_FN]() { + * return { Number: this.id } + * } + * } + * + * type UserId = UserIdString | UserIdNumber + * ``` + * + */ +// if this value changes, make sure to update it in: +// 1. ipc.js +// 2. process-ipc-message-fn.js +export const SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__' + +/** + * Stores the callback in a known location, and returns an identifier that can be passed to the backend. + * The backend uses the identifier to `eval()` the callback. + * + * @return An unique identifier associated with the callback function. + * + * @since 1.0.0 + */ +function transformCallback( + // TODO: Make this not optional in v3 + callback?: (response: T) => void, + once = false +): number { + return window.__TAURI_INTERNALS__.transformCallback(callback, once) +} + +/** + * A message channel used to stream values from Rust to the frontend. + * + * A `Channel` is the JavaScript counterpart of [`tauri::ipc::Channel`](https://docs.rs/tauri/2/tauri/ipc/struct.Channel.html). + * Pass an instance as a command argument and the Rust command can declare a + * `tauri::ipc::Channel` parameter with the same name; every value sent from + * Rust is then delivered to {@linkcode Channel.onmessage}. Messages are delivered + * in the order they were sent, even when they arrive out of order. + * + * The channel stays alive until the Rust `Channel` is dropped, so it can be used + * for long-running work such as download progress or log streaming. + * + * Raw byte payloads are delivered to the frontend as an `ArrayBuffer` rather than + * a JSON value, so declare the channel as `Channel` when the Rust + * side sends `tauri::ipc::InvokeResponseBody::Raw` — for example through a + * `Channel` whose values are built with + * `tauri::ipc::Response::new(bytes)`. + * + * @example + * ```typescript + * import { Channel, invoke } from '@tauri-apps/api/core'; + * + * const onEvent = new Channel(); + * onEvent.onmessage = (message) => { + * console.log(`got download event ${message}`); + * }; + * + * await invoke('download', { url: 'https://tauri.app', onEvent }); + * ``` + * + * The matching Rust command: + * ```rust + * #[tauri::command] + * async fn download(url: String, on_event: tauri::ipc::Channel) -> tauri::Result<()> { + * on_event.send("started".to_string())?; + * on_event.send("finished".to_string())?; + * Ok(()) + * } + * ``` + * + * @since 2.0.0 + */ +class Channel { + /** + * The callback id returned from {@linkcode transformCallback}. + * + * This is the value sent across the IPC, it is used by the Rust side to + * address this channel and should not be changed. + */ + id: number + #onmessage: (response: T) => void + + // the index is used as a mechanism to preserve message order + #nextMessageIndex = 0 + #pendingMessages: T[] = [] + #messageEndIndex: number | undefined + + constructor(onmessage?: (response: T) => void) { + this.#onmessage = onmessage || (() => {}) + + this.id = transformCallback< + // Normal message + | { message: T; index: number } + // Message when the channel gets dropped in the rust side + | { end: true; index: number } + >((rawMessage) => { + const index = rawMessage.index + + if ('end' in rawMessage) { + if (index == this.#nextMessageIndex) { + this.cleanupCallback() + } else { + this.#messageEndIndex = index + } + return + } + + const message = rawMessage.message + // Process the message if we're at the right order + if (index == this.#nextMessageIndex) { + this.#onmessage(message) + this.#nextMessageIndex += 1 + + // process pending messages + while (this.#nextMessageIndex in this.#pendingMessages) { + const message = this.#pendingMessages[this.#nextMessageIndex] + this.#onmessage(message) + // eslint-disable-next-line @typescript-eslint/no-array-delete + delete this.#pendingMessages[this.#nextMessageIndex] + this.#nextMessageIndex += 1 + } + + if (this.#nextMessageIndex === this.#messageEndIndex) { + this.cleanupCallback() + } + } + // Queue the message if we're not + else { + // eslint-disable-next-line security/detect-object-injection + this.#pendingMessages[index] = message + } + }) + } + + private cleanupCallback() { + window.__TAURI_INTERNALS__.unregisterCallback(this.id) + } + + /** + * The handler called for every message sent by the Rust side of this channel. + * + * Assigning a new handler replaces the previous one; messages that arrived + * before a handler was set are not replayed, so set it (or pass it to the + * constructor) before sending the channel to the backend. + */ + set onmessage(handler: (response: T) => void) { + this.#onmessage = handler + } + + get onmessage(): (response: T) => void { + return this.#onmessage + } + + [SERIALIZE_TO_IPC_FN]() { + return `__CHANNEL__:${this.id}` + } + + toJSON(): string { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN]() + } +} + +/** + * A handle to a listener registered with {@linkcode addPluginListener}. + * + * Keep the returned instance around and call {@linkcode PluginListener.unregister} + * when the listener goes out of scope, otherwise the plugin keeps sending events + * to a handler nothing uses anymore. + * + * @since 2.0.0 + */ +class PluginListener { + /** The plugin name the listener is registered to. */ + plugin: string + /** The plugin event name the listener is registered to. */ + event: string + /** The id of the {@linkcode Channel} backing this listener. */ + channelId: number + + constructor(plugin: string, event: string, channelId: number) { + this.plugin = plugin + this.event = event + this.channelId = channelId + } + + /** Removes this listener from the plugin, so its handler stops being called. */ + async unregister(): Promise { + return invoke(`plugin:${this.plugin}|remove_listener`, { + event: this.event, + channelId: this.channelId + }) + } +} + +/** + * Adds a listener to a plugin event. + * + * This is meant to be used by plugin authors to wrap the `register_listener` + * command their mobile plugin implements; application code normally calls the + * wrapper the plugin exposes instead of this function. + * + * @example + * ```typescript + * import { addPluginListener } from '@tauri-apps/api/core'; + * + * interface ScanEvent { + * value: string + * } + * + * const listener = await addPluginListener( + * 'barcode-scanner', + * 'scan', + * (payload) => console.log('scanned', payload.value) + * ); + * + * // stop listening when the scanner screen is closed + * await listener.unregister(); + * ``` + * + * @param plugin The plugin name, as used in the `plugin:|` IPC command format. + * @param event The plugin event name. + * @param cb The callback executed for each event payload. + * @returns The listener object to stop listening to the events. + * + * @since 2.0.0 + */ +async function addPluginListener( + plugin: string, + event: string, + cb: (payload: T) => void +): Promise { + const handler = new Channel(cb) + try { + await invoke(`plugin:${plugin}|register_listener`, { + event, + handler + }) + return new PluginListener(plugin, event, handler.id) + } catch { + // TODO(v3): remove this fallback + // note: we must try with camelCase here for backwards compatibility + await invoke(`plugin:${plugin}|registerListener`, { event, handler }) + return new PluginListener(plugin, event, handler.id) + } +} + +type PermissionState = 'granted' | 'denied' | 'prompt' | 'prompt-with-rationale' + +/** + * Get permission state for a plugin. + * + * This should be used by plugin authors to wrap their actual implementation, + * it calls the `plugin:|check_permissions` command implemented by the + * mobile plugin and returns its permission status object without prompting + * the user. + * + * @example + * ```typescript + * import { checkPermissions, type PermissionState } from '@tauri-apps/api/core'; + * + * interface Permissions { + * camera: PermissionState + * } + * + * const status = await checkPermissions('barcode-scanner'); + * if (status.camera === 'prompt') { + * // ask the user, see requestPermissions + * } + * ``` + * + * @param plugin The plugin name, as used in the `plugin:|` IPC command format. + * + * @since 2.0.0 + */ +async function checkPermissions(plugin: string): Promise { + return invoke(`plugin:${plugin}|check_permissions`) +} + +/** + * Request permissions. + * + * This should be used by plugin authors to wrap their actual implementation, + * it calls the `plugin:|request_permissions` command implemented by the + * mobile plugin, which shows the native permission prompt when needed, and + * returns the resulting permission status object. + * + * @example + * ```typescript + * import { requestPermissions, type PermissionState } from '@tauri-apps/api/core'; + * + * interface Permissions { + * camera: PermissionState + * } + * + * const status = await requestPermissions('barcode-scanner'); + * if (status.camera !== 'granted') { + * throw new Error('camera permission denied'); + * } + * ``` + * + * @param plugin The plugin name, as used in the `plugin:|` IPC command format. + * + * @since 2.0.0 + */ +async function requestPermissions(plugin: string): Promise { + return invoke(`plugin:${plugin}|request_permissions`) +} + +/** + * Command arguments. + * + * An object is the usual form: each key becomes a command parameter, serialized + * as JSON (`application/octet-stream` is used instead when a value implements + * {@linkcode SERIALIZE_TO_IPC_FN}, see its documentation). + * + * The `ArrayBuffer`, `Uint8Array` and `number[]` variants are a *raw payload*: + * the whole request body is sent as `application/octet-stream` instead of JSON, + * which avoids the cost of base64/array encoding for large binary data. The + * command then has a single [`tauri::ipc::Request`](https://docs.rs/tauri/2/tauri/ipc/struct.Request.html) + * parameter and reads the bytes from it: + * + * ```rust + * #[tauri::command] + * fn upload(request: tauri::ipc::Request<'_>) -> Result<(), String> { + * let tauri::ipc::InvokeBody::Raw(bytes) = request.body() else { + * return Err("expected a raw body".into()); + * }; + * println!("got {} bytes", bytes.len()); + * Ok(()) + * } + * ``` + * + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * await invoke('upload', new Uint8Array([1, 2, 3])); + * ``` + * + * A command that returns [`tauri::ipc::Response`](https://docs.rs/tauri/2/tauri/ipc/struct.Response.html) + * likewise resolves to an `ArrayBuffer` on the frontend instead of a JSON value. + * + * @since 1.0.0 + */ +type InvokeArgs = Record | number[] | ArrayBuffer | Uint8Array + +/** + * @since 2.0.0 + */ +interface InvokeOptions { + /** + * Headers to send along with the IPC request. + * + * They are readable on the Rust side through + * [`tauri::ipc::Request::headers`](https://docs.rs/tauri/2/tauri/ipc/struct.Request.html#method.headers) + * and are useful to pass metadata (such as a content type for a raw payload) + * without adding it to the command arguments. + * + * @example + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * await invoke('upload', new Uint8Array([1, 2, 3]), { + * headers: { 'x-file-name': 'image.png' } + * }); + * ``` + */ + headers: HeadersInit +} + +/** + * Sends a message to the backend. + * @example + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * await invoke('login', { user: 'tauri', password: 'poiwe3h4r5ip3yrhtew9ty' }); + * ``` + * + * @param cmd The command name. + * @param args The optional arguments to pass to the command. + * @param options The request options. + * @return A promise resolving or rejecting to the backend response. + * + * @since 1.0.0 + */ +async function invoke( + cmd: string, + args: InvokeArgs = {}, + options?: InvokeOptions +): Promise { + return window.__TAURI_INTERNALS__.invoke(cmd, args, options) +} + +/** + * Convert a device file path to a URL that can be loaded by the webview. + * + * The asset protocol must be enabled and the files you want to expose must be + * included in its scope. The protocol origins must also be allowed by the + * relevant [`app.security.csp`](https://v2.tauri.app/reference/config/#csp-1) + * directive. For example, this configuration allows images from the user's + * downloads directory: + * + * ```json + * { + * "app": { + * "security": { + * "assetProtocol": { + * "enable": true, + * "scope": ["$DOWNLOAD/**"] + * }, + * "csp": "default-src 'self'; img-src 'self' asset: http://asset.localhost" + * } + * } + * } + * ``` + * + * See [`assetProtocol`](https://v2.tauri.app/reference/config/#assetprotocolconfig) + * for the available scope variables and platform-specific protocol details. + * + * @param filePath The file path. + * @param protocol The protocol to use. Defaults to `asset`. You only need to set this when using a custom protocol. + * @example + * ```typescript + * import { downloadDir, join } from '@tauri-apps/api/path'; + * import { convertFileSrc } from '@tauri-apps/api/core'; + * const downloads = await downloadDir(); + * const filePath = await join(downloads, 'photo.png'); + * const assetUrl = convertFileSrc(filePath); + * + * const image = document.getElementById('my-image') as HTMLImageElement; + * image.src = assetUrl; + * ``` + * + * @return the URL that can be used as source on the webview. + * + * @since 1.0.0 + */ +function convertFileSrc(filePath: string, protocol = 'asset'): string { + return window.__TAURI_INTERNALS__.convertFileSrc(filePath, protocol) +} + +/** + * A rust-backed resource stored through `tauri::Manager::resources_table` API. + * + * The resource lives in the main process and does not exist + * in the Javascript world, and thus will not be cleaned up automatically + * except on application exit. If you want to clean it up early, call {@linkcode Resource.close} or use [Explicit Resource Management]. + * + * Several API types are resources and inherit this behavior, among them `Menu`, + * `TrayIcon`, `Image`, `Webview` and the menu item classes. + * + * To support older browsers with [Explicit Resource Management], use a supported compiler (e.g. tsc) or bundler (e.g. rollup). + * + * @example + * ```typescript + * import { Resource, invoke } from '@tauri-apps/api/core'; + * + * export class DatabaseHandle extends Resource { + * static async open(path: string): Promise { + * const rid: number = await invoke('open_db', { path }); + * return new DatabaseHandle(rid); + * } + * + * async execute(sql: string): Promise { + * await invoke('execute_sql', { rid: this.rid, sql }); + * } + * } + * ``` + * + * Only asynchronous disposal is implemented (`Symbol.asyncDispose`), because closing + * a resource is an IPC call. Use `await using`; the synchronous `using` form does + * **not** work with resources. + * + * @example + * ``` + * await using db = await DatabaseHandle.open('test.db'); + * await db.execute('SELECT *'); + * // `db` is closed here, by awaiting `db[Symbol.asyncDispose]()` + * ``` + * + * To support older browsers, add the following to the globals (e.g. adding to the HTML file): + * + * ```javascript + * Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose"); + * ``` + * + * And for the compiler, for example `tsc`, `rollup`, `vite`, add the following to `tsconfig.json`: + * + * ```json + * { + * "compilerOptions": { + * "target": "es2022", + * "lib": ["es2022", "esnext.disposable", "dom"] + * } + * } + * ``` + * + * [Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management + */ +export class Resource { + readonly #rid: number + + get rid(): number { + return this.#rid + } + + constructor(rid: number) { + this.#rid = rid + } + + /** + * Destroys and cleans up this resource from memory. + * **You should not call any method on this object anymore and should drop any reference to it.** + * + * @remarks Uses the `core:resources:allow-close` permission, which is part of + * the `core:resources:default` permission set enabled by default. + */ + async close(): Promise { + return invoke('plugin:resources|close', { + rid: this.rid + }) + } + + async [Symbol.asyncDispose]() { + await this.close() + } +} + +/** + * Checks whether the code is running inside a Tauri webview. + * + * Useful for frontends that are also served on the web, to guard calls to APIs + * that only exist inside the application. + * + * @example + * ```typescript + * import { isTauri } from '@tauri-apps/api/core'; + * import { getVersion } from '@tauri-apps/api/app'; + * + * const version = isTauri() ? await getVersion() : 'web'; + * ``` + * + * @returns `true` when running inside a Tauri app, `false` otherwise (e.g. in a + * browser or in unit tests). + * + * @since 2.0.0 + */ +function isTauri(): boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + return !!((globalThis as any) || window).isTauri +} + +export type { InvokeArgs, InvokeOptions } + +export { + transformCallback, + Channel, + PluginListener, + addPluginListener, + PermissionState, + checkPermissions, + requestPermissions, + invoke, + convertFileSrc, + isTauri +} diff --git a/packages/api/src/dpi.ts b/packages/api/src/dpi.ts new file mode 100644 index 0000000..2444aea --- /dev/null +++ b/packages/api/src/dpi.ts @@ -0,0 +1,476 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Size and position types for working with logical and physical pixels. + * + * Window and webview APIs report sizes and positions in **physical pixels** (actual + * screen pixels), while most browser APIs and the window creation options use + * **logical pixels** (physical pixels divided by the monitor scale factor). The + * classes in this module make the unit explicit and convert between the two. + * + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const appWindow = getCurrentWindow(); + * const size = await appWindow.innerSize(); // PhysicalSize + * const logical = size.toLogical(await appWindow.scaleFactor()); + * ``` + * + * They also serialize into the shape `tauri::Size` / `tauri::Position` expect, so + * they can be passed straight to your own commands, see {@linkcode Size} and + * {@linkcode Position}. + * + * This package is also accessible with `window.__TAURI__.dpi` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * + * @module + */ + +import { SERIALIZE_TO_IPC_FN } from './core' + +/** + * A size represented in logical pixels. + * Logical pixels are scaled according to the window's DPI scale. + * Most browser APIs (i.e. `MouseEvent`'s `clientX`) will return logical pixels. + * + * For logical-pixel-based position, see {@linkcode LogicalPosition}. + * + * @since 2.0.0 + */ +class LogicalSize { + readonly type = 'Logical' + width: number + height: number + + constructor(width: number, height: number) + constructor(object: { Logical: { width: number; height: number } }) + constructor(object: { width: number; height: number }) + constructor( + ...args: + | [number, number] + | [{ width: number; height: number }] + | [{ Logical: { width: number; height: number } }] + ) { + if (args.length === 1) { + if ('Logical' in args[0]) { + this.width = args[0].Logical.width + this.height = args[0].Logical.height + } else { + this.width = args[0].width + this.height = args[0].height + } + } else { + this.width = args[0] + this.height = args[1] + } + } + + /** + * Converts the logical size to a physical one. + * @example + * ```typescript + * import { LogicalSize } from '@tauri-apps/api/dpi'; + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const size = new LogicalSize(400, 500); + * const physical = size.toPhysical(factor); + * ``` + * + * @since 2.0.0 + */ + toPhysical(scaleFactor: number): PhysicalSize { + return new PhysicalSize(this.width * scaleFactor, this.height * scaleFactor) + } + + [SERIALIZE_TO_IPC_FN]() { + return { + width: this.width, + height: this.height + } + } + + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN]() + } +} + +/** + * A size represented in physical pixels. + * + * Physical pixels represent actual screen pixels, and are DPI-independent. + * For high-DPI windows, this means that any point in the window on the screen + * will have a different position in logical pixels {@linkcode LogicalSize}. + * + * For physical-pixel-based position, see {@linkcode PhysicalPosition}. + * + * @since 2.0.0 + */ +class PhysicalSize { + readonly type = 'Physical' + width: number + height: number + + constructor(width: number, height: number) + constructor(object: { Physical: { width: number; height: number } }) + constructor(object: { width: number; height: number }) + constructor( + ...args: + | [number, number] + | [{ width: number; height: number }] + | [{ Physical: { width: number; height: number } }] + ) { + if (args.length === 1) { + if ('Physical' in args[0]) { + this.width = args[0].Physical.width + this.height = args[0].Physical.height + } else { + this.width = args[0].width + this.height = args[0].height + } + } else { + this.width = args[0] + this.height = args[1] + } + } + + /** + * Converts the physical size to a logical one. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const size = await appWindow.innerSize(); // PhysicalSize + * const logical = size.toLogical(factor); + * ``` + * + * @since 2.0.0 + */ + toLogical(scaleFactor: number): LogicalSize { + return new LogicalSize(this.width / scaleFactor, this.height / scaleFactor) + } + + [SERIALIZE_TO_IPC_FN]() { + return { + width: this.width, + height: this.height + } + } + + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN]() + } +} + +/** + * A size represented either in physical or in logical pixels. + * + * This type is basically a union type of {@linkcode LogicalSize} and {@linkcode PhysicalSize} + * but comes in handy when using `tauri::Size` in Rust as an argument to a command, as this class + * automatically serializes into a valid format so it can be deserialized correctly into `tauri::Size` + * + * So instead of + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalSize, PhysicalSize } from '@tauri-apps/api/dpi'; + * + * const size: LogicalSize | PhysicalSize = someFunction(); // where someFunction returns either LogicalSize or PhysicalSize + * const validSize = size instanceof LogicalSize + * ? { Logical: { width: size.width, height: size.height } } + * : { Physical: { width: size.width, height: size.height } } + * await invoke("do_something_with_size", { size: validSize }); + * ``` + * + * You can just use {@linkcode Size} + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalSize, PhysicalSize, Size } from '@tauri-apps/api/dpi'; + * + * const size: LogicalSize | PhysicalSize = someFunction(); // where someFunction returns either LogicalSize or PhysicalSize + * const validSize = new Size(size); + * await invoke("do_something_with_size", { size: validSize }); + * ``` + * + * @since 2.1.0 + */ +class Size { + size: LogicalSize | PhysicalSize + + constructor(size: LogicalSize | PhysicalSize) { + this.size = size + } + + /** + * Converts the wrapped size to a logical one. + * + * Returns the inner value untouched when it already is a {@linkcode LogicalSize}. + * + * @param scaleFactor The monitor scale factor, e.g. `await appWindow.scaleFactor()`. + * + * @since 2.1.0 + */ + toLogical(scaleFactor: number): LogicalSize { + return this.size instanceof LogicalSize + ? this.size + : this.size.toLogical(scaleFactor) + } + + /** + * Converts the wrapped size to a physical one. + * + * Returns the inner value untouched when it already is a {@linkcode PhysicalSize}. + * + * @param scaleFactor The monitor scale factor, e.g. `await appWindow.scaleFactor()`. + * + * @since 2.1.0 + */ + toPhysical(scaleFactor: number): PhysicalSize { + return this.size instanceof PhysicalSize + ? this.size + : this.size.toPhysical(scaleFactor) + } + + [SERIALIZE_TO_IPC_FN]() { + return { + [`${this.size.type}`]: { + width: this.size.width, + height: this.size.height + } + } + } + + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN]() + } +} + +/** + * A position represented in logical pixels. + * For an explanation of what logical pixels are, see description of {@linkcode LogicalSize}. + * + * @since 2.0.0 + */ +class LogicalPosition { + readonly type = 'Logical' + x: number + y: number + + constructor(x: number, y: number) + constructor(object: { Logical: { x: number; y: number } }) + constructor(object: { x: number; y: number }) + constructor( + ...args: + | [number, number] + | [{ x: number; y: number }] + | [{ Logical: { x: number; y: number } }] + ) { + if (args.length === 1) { + if ('Logical' in args[0]) { + this.x = args[0].Logical.x + this.y = args[0].Logical.y + } else { + this.x = args[0].x + this.y = args[0].y + } + } else { + this.x = args[0] + this.y = args[1] + } + } + + /** + * Converts the logical position to a physical one. + * @example + * ```typescript + * import { LogicalPosition } from '@tauri-apps/api/dpi'; + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const position = new LogicalPosition(400, 500); + * const physical = position.toPhysical(factor); + * ``` + * + * @since 2.0.0 + */ + toPhysical(scaleFactor: number): PhysicalPosition { + return new PhysicalPosition(this.x * scaleFactor, this.y * scaleFactor) + } + + [SERIALIZE_TO_IPC_FN]() { + return { + x: this.x, + y: this.y + } + } + + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN]() + } +} + +/** + * A position represented in physical pixels. + * + * For an explanation of what physical pixels are, see description of {@linkcode PhysicalSize}. + * + * @since 2.0.0 + */ +class PhysicalPosition { + readonly type = 'Physical' + x: number + y: number + + constructor(x: number, y: number) + constructor(object: { Physical: { x: number; y: number } }) + constructor(object: { x: number; y: number }) + constructor( + ...args: + | [number, number] + | [{ x: number; y: number }] + | [{ Physical: { x: number; y: number } }] + ) { + if (args.length === 1) { + if ('Physical' in args[0]) { + this.x = args[0].Physical.x + this.y = args[0].Physical.y + } else { + this.x = args[0].x + this.y = args[0].y + } + } else { + this.x = args[0] + this.y = args[1] + } + } + + /** + * Converts the physical position to a logical one. + * @example + * ```typescript + * import { PhysicalPosition } from '@tauri-apps/api/dpi'; + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const appWindow = getCurrentWindow(); + * const factor = await appWindow.scaleFactor(); + * const position = new PhysicalPosition(400, 500); + * const logical = position.toLogical(factor); + * ``` + * + * @since 2.0.0 + */ + toLogical(scaleFactor: number): LogicalPosition { + return new LogicalPosition(this.x / scaleFactor, this.y / scaleFactor) + } + + [SERIALIZE_TO_IPC_FN]() { + return { + x: this.x, + y: this.y + } + } + + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN]() + } +} + +/** + * A position represented either in physical or in logical pixels. + * + * This type is basically a union type of {@linkcode LogicalPosition} and {@linkcode PhysicalPosition} + * but comes in handy when using `tauri::Position` in Rust as an argument to a command, as this class + * automatically serializes into a valid format so it can be deserialized correctly into `tauri::Position` + * + * So instead of + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalPosition, PhysicalPosition } from '@tauri-apps/api/dpi'; + * + * const position: LogicalPosition | PhysicalPosition = someFunction(); // where someFunction returns either LogicalPosition or PhysicalPosition + * const validPosition = position instanceof LogicalPosition + * ? { Logical: { x: position.x, y: position.y } } + * : { Physical: { x: position.x, y: position.y } } + * await invoke("do_something_with_position", { position: validPosition }); + * ``` + * + * You can just use {@linkcode Position} + * ```typescript + * import { invoke } from '@tauri-apps/api/core'; + * import { LogicalPosition, PhysicalPosition, Position } from '@tauri-apps/api/dpi'; + * + * const position: LogicalPosition | PhysicalPosition = someFunction(); // where someFunction returns either LogicalPosition or PhysicalPosition + * const validPosition = new Position(position); + * await invoke("do_something_with_position", { position: validPosition }); + * ``` + * + * @since 2.1.0 + */ +class Position { + position: LogicalPosition | PhysicalPosition + + constructor(position: LogicalPosition | PhysicalPosition) { + this.position = position + } + + /** + * Converts the wrapped position to a logical one. + * + * Returns the inner value untouched when it already is a {@linkcode LogicalPosition}. + * + * @param scaleFactor The monitor scale factor, e.g. `await appWindow.scaleFactor()`. + * + * @since 2.1.0 + */ + toLogical(scaleFactor: number): LogicalPosition { + return this.position instanceof LogicalPosition + ? this.position + : this.position.toLogical(scaleFactor) + } + + /** + * Converts the wrapped position to a physical one. + * + * Returns the inner value untouched when it already is a {@linkcode PhysicalPosition}. + * + * @param scaleFactor The monitor scale factor, e.g. `await appWindow.scaleFactor()`. + * + * @since 2.1.0 + */ + toPhysical(scaleFactor: number): PhysicalPosition { + return this.position instanceof PhysicalPosition + ? this.position + : this.position.toPhysical(scaleFactor) + } + + [SERIALIZE_TO_IPC_FN]() { + return { + [`${this.position.type}`]: { + x: this.position.x, + y: this.position.y + } + } + } + + toJSON() { + // eslint-disable-next-line security/detect-object-injection + return this[SERIALIZE_TO_IPC_FN]() + } +} + +export { + LogicalPosition, + LogicalSize, + Size, + PhysicalPosition, + PhysicalSize, + Position +} diff --git a/packages/api/src/event.ts b/packages/api/src/event.ts new file mode 100644 index 0000000..b51e306 --- /dev/null +++ b/packages/api/src/event.ts @@ -0,0 +1,312 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * The event system allows you to emit events to the backend and listen to events from it. + * + * This package is also accessible with `window.__TAURI__.event` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * + * @remarks All commands used by this module (`core:event:allow-listen`, + * `allow-unlisten`, `allow-emit` and `allow-emit-to`) are part of the + * `core:event:default` permission set, which is enabled by default, so no extra + * capability configuration is needed. + * + * @module + */ + +import { invoke, transformCallback } from './core' + +declare global { + interface Window { + __TAURI_EVENT_PLUGIN_INTERNALS__: { + unregisterListener: (event: string, eventId: number) => void + } + } +} + +/** + * The target of an event, used to filter which listeners receive it and which + * listeners a given emit reaches. + * + * - `Any`: matches every target (the default). + * - `AnyLabel`: matches any window, webview or webview window with the given label. + * - `App`: the application itself, i.e. listeners registered with `app.listen` on the Rust side. + * - `Window` / `Webview` / `WebviewWindow`: the specific target with that label. + * + * @since 2.0.0 + */ +type EventTarget = + | { kind: 'Any' } + | { kind: 'AnyLabel'; label: string } + | { kind: 'App' } + | { kind: 'Window'; label: string } + | { kind: 'Webview'; label: string } + | { kind: 'WebviewWindow'; label: string } + +interface Event { + /** Event name */ + event: EventName + /** Event identifier used to unlisten */ + id: number + /** Event payload */ + payload: T +} + +type EventCallback = (event: Event) => void + +// TODO(v3): mark this as Promise +type UnlistenFn = () => void + +type EventName = `${TauriEvent}` | (string & Record) + +interface Options { + /** + * The event target to listen to, defaults to `{ kind: 'Any' }`, see {@link EventTarget}. + * + * If a string is provided, it is used as the label of an `AnyLabel` target, + * i.e. `{ kind: 'AnyLabel', label: }`. + */ + target?: string | EventTarget +} + +/** + * The built-in event names emitted by Tauri itself. + * + * These are the raw names behind the `on*` helpers of the `Window` and `Webview` + * classes (e.g. `Window.onResized` listens to {@linkcode TauriEvent.WINDOW_RESIZED}). + * Prefer those helpers when one exists, since they also decode the payload into + * the matching class (`PhysicalSize`, `PhysicalPosition`, ...). + * + * @example + * ```typescript + * import { listen, TauriEvent } from '@tauri-apps/api/event'; + * const unlisten = await listen(TauriEvent.WINDOW_DESTROYED, (event) => { + * console.log('a window was destroyed', event.payload); + * }); + * ``` + * + * @since 1.1.0 + */ +enum TauriEvent { + /** A window was resized. Payload: the new inner size, in physical pixels. See `Window.onResized`. */ + WINDOW_RESIZED = 'tauri://resize', + /** A window was moved. Payload: the new outer position, in physical pixels. See `Window.onMoved`. */ + WINDOW_MOVED = 'tauri://move', + /** + * The user requested a window to be closed (e.g. clicked the close button). + * See `Window.onCloseRequested`, which also handles preventing the close. + */ + WINDOW_CLOSE_REQUESTED = 'tauri://close-requested', + /** A window was destroyed, i.e. it is gone and its label can be reused. */ + WINDOW_DESTROYED = 'tauri://destroyed', + /** A window gained focus. See `Window.onFocusChanged`. */ + WINDOW_FOCUS = 'tauri://focus', + /** A window lost focus. See `Window.onFocusChanged`. */ + WINDOW_BLUR = 'tauri://blur', + /** + * The scale factor of the monitor a window is on changed, or the window moved to + * a monitor with a different scale factor. See `Window.onScaleChanged`. + */ + WINDOW_SCALE_FACTOR_CHANGED = 'tauri://scale-change', + /** The system or window theme changed. See `Window.onThemeChanged`. */ + WINDOW_THEME_CHANGED = 'tauri://theme-changed', + /** A new window was created. */ + WINDOW_CREATED = 'tauri://window-created', + /** + * The window's event loop was suspended. + * + * #### Platform-specific + * + * - **Android:** emitted when the activity is paused. + * - **Other platforms:** never emitted. + */ + WINDOW_SUSPENDED = 'tauri://suspended', + /** + * The window's event loop was resumed after being suspended. + * + * #### Platform-specific + * + * - **Android:** emitted when the activity is resumed. + * - **Other platforms:** never emitted. + */ + WINDOW_RESUMED = 'tauri://resumed', + /** A new webview was created. */ + WEBVIEW_CREATED = 'tauri://webview-created', + /** The user dragged files onto a webview. See `Webview.onDragDropEvent`. */ + DRAG_ENTER = 'tauri://drag-enter', + /** The user is moving dragged files over a webview. See `Webview.onDragDropEvent`. */ + DRAG_OVER = 'tauri://drag-over', + /** The user dropped files onto a webview. See `Webview.onDragDropEvent`. */ + DRAG_DROP = 'tauri://drag-drop', + /** The drag operation left the webview or was cancelled. See `Webview.onDragDropEvent`. */ + DRAG_LEAVE = 'tauri://drag-leave' +} + +/** + * Unregister the event listener associated with the given name and id. + * + * @ignore + * @param event The event name + * @param eventId Event identifier + * @returns + */ +async function _unlisten(event: string, eventId: number): Promise { + window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(event, eventId) + await invoke('plugin:event|unlisten', { + event, + eventId + }) +} + +/** + * Listen to an emitted event to any {@link EventTarget|target}. + * + * @example + * ```typescript + * import { listen } from '@tauri-apps/api/event'; + * const unlisten = await listen('error', (event) => { + * console.log(`Got error, payload: ${event.payload}`); + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler callback. + * @param options Event listening options. + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks Listeners bound to a window or webview are removed automatically when + * that window or webview is destroyed, so you do not need to unlisten just to + * avoid leaking across a window close. You should still call the returned + * function when the listener's own scope ends — for example on page navigation + * or when a component unmounts — otherwise the handler keeps running for the + * lifetime of the webview. + * + * @since 1.0.0 + */ +async function listen( + event: EventName, + handler: EventCallback, + options?: Options +): Promise { + const target: EventTarget = + typeof options?.target === 'string' + ? { kind: 'AnyLabel', label: options.target } + : (options?.target ?? { kind: 'Any' }) + return invoke('plugin:event|listen', { + event, + target, + handler: transformCallback(handler) + }).then((eventId) => { + return async () => _unlisten(event, eventId) + }) +} + +/** + * Listens once to an emitted event to any {@link EventTarget|target}. + * + * @example + * ```typescript + * import { once } from '@tauri-apps/api/event'; + * interface LoadedPayload { + * loggedIn: boolean, + * token: string + * } + * const unlisten = await once('loaded', (event) => { + * console.log(`App is loaded, loggedIn: ${event.payload.loggedIn}, token: ${event.payload.token}`); + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler callback. + * @param options Event listening options. + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listener removes itself after the first event, and listeners bound + * to a window or webview are also removed automatically when that target is + * destroyed. Still call the returned function when the listener's own scope ends + * before the event arrives — for example on page navigation or component unmount. + * + * @since 1.0.0 + */ +async function once( + event: EventName, + handler: EventCallback, + options?: Options +): Promise { + return listen( + event, + (eventData) => { + void _unlisten(event, eventData.id) + handler(eventData) + }, + options + ) +} + +/** + * Emits an event to all {@link EventTarget|targets}. + * + * @example + * ```typescript + * import { emit } from '@tauri-apps/api/event'; + * await emit('frontend-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + * + * @since 1.0.0 + */ +async function emit(event: string, payload?: T): Promise { + await invoke('plugin:event|emit', { + event, + payload + }) +} + +/** + * Emits an event to all {@link EventTarget|targets} matching the given target. + * + * @example + * ```typescript + * import { emitTo } from '@tauri-apps/api/event'; + * await emitTo('main', 'frontend-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object. + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + * + * @since 2.0.0 + */ +async function emitTo( + target: EventTarget | string, + event: string, + payload?: T +): Promise { + const eventTarget: EventTarget = + typeof target === 'string' ? { kind: 'AnyLabel', label: target } : target + await invoke('plugin:event|emit_to', { + target: eventTarget, + event, + payload + }) +} + +export type { + Event, + EventTarget, + EventCallback, + UnlistenFn, + EventName, + Options +} + +export { listen, once, emit, emitTo, TauriEvent } diff --git a/packages/api/src/global.d.ts b/packages/api/src/global.d.ts new file mode 100644 index 0000000..07f9cb6 --- /dev/null +++ b/packages/api/src/global.d.ts @@ -0,0 +1,64 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** @ignore */ + +import type { invoke, transformCallback, convertFileSrc } from './core' + +/** @ignore */ +declare global { + interface Window { + /** + * The whole `@tauri-apps/api` package, exposed as a global object. + * + * Only defined when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) + * is set to `true` in `tauri.conf.json`. It is meant for vanilla JavaScript + * frontends that do not use a bundler: + * + * ```js + * const { event, window: tauriWindow, path } = window.__TAURI__; + * ``` + */ + __TAURI__?: typeof import('./index') + __TAURI_INTERNALS__: { + invoke: typeof invoke + transformCallback: typeof transformCallback + unregisterCallback: (id: number) => void + runCallback: (id: number, data: unknown) => void + callbacks: Map void> + convertFileSrc: typeof convertFileSrc + ipc: (message: { + cmd: string + callback: number + error: number + payload: unknown + options?: InvokeOptions + }) => void + metadata: { + currentWindow: WindowDef + currentWebview: WebviewDef + } + plugins: { + path: { + sep: string + delimiter: string + } + } + } + __TAURI_EVENT_PLUGIN_INTERNALS__: { + unregisterListener: (event: string, eventId: number) => void + } + } +} + +/** @ignore */ +interface WebviewDef { + windowLabel: string + label: string +} + +/** @ignore */ +interface WindowDef { + label: string +} diff --git a/packages/api/src/image.ts b/packages/api/src/image.ts new file mode 100644 index 0000000..fba1580 --- /dev/null +++ b/packages/api/src/image.ts @@ -0,0 +1,243 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Load and inspect images to use as window icons, tray icons and menu item icons. + * + * Images live on the Rust side (they extend {@linkcode Resource}), the frontend only + * holds a handle to them. Call `close()` when an image is no longer needed, or let + * it be released when the app exits. + * + * @remarks {@linkcode Image.fromPath} and {@linkcode Image.fromBytes} decode PNG and ICO + * files, which requires the `image-png` and/or `image-ico` Cargo features of the + * `tauri` crate: + * ```toml + * [dependencies] + * tauri = { version = "2", features = ["image-png"] } + * ``` + * {@linkcode Image.new} takes raw RGBA data and needs no extra feature. + * + * @remarks All commands used by this module are part of the `core:image:default` + * permission set, which is enabled by default. + * + * This package is also accessible with `window.__TAURI__.image` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * + * @module + */ + +import { Resource, invoke } from './core' +import { NativeIcon } from './menu/iconMenuItem' + +/** Image dimensions, in pixels. */ +export interface ImageSize { + /** Image width, in pixels. */ + width: number + /** Image height, in pixels. */ + height: number +} + +/** + * A type that can be passed to Rust side as [`tauri::image::JsImage`](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html) through {@linkcode transformImage} + * + * Values of this type must go through {@linkcode transformImage} before being placed in `invoke` arguments; + * an {@linkcode Image} instance is not serializable on its own. + * + * ## Variants + * + * - **string:** Path to an image in the filesystem. Maps to [`JsImage::Path`](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html#variant.Path) + * - **Uint8Array | ArrayBuffer | number[]:** ICO or PNG image in raw bytes. Maps to [`JsImage::Bytes`](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html#variant.Bytes) + * - **Image:** An image that was previously loaded with the API and is stored in the resource table. Maps to [`JsImage::Resource`](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html#variant.Resource) + * + * The `string` and bytes variants require the `image-ico` or `image-png` Cargo features. + * To enable them, change your Cargo.toml file: + * ```toml + * [dependencies] + * tauri = { version = "...", features = ["...", "image-png"] } + * ``` + * + * The Rust [`JsImage::Rgba`](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html#variant.Rgba) variant is intentionally not exposed here; + * use {@linkcode Image.new} to create an image from raw RGBA data instead. + */ +export type JsImage = string | Uint8Array | ArrayBuffer | number[] | Image + +/** A type that represents an icon that can be used in menu items. */ +export type MenuIcon = JsImage | NativeIcon + +/** + * An RGBA Image in row-major order from top to bottom. + * + * Instances are created with {@linkcode Image.new}, {@linkcode Image.fromBytes} or + * {@linkcode Image.fromPath} and can be passed wherever a {@linkcode JsImage} is + * accepted, such as `Window.setIcon`, `TrayIcon.setIcon` or an icon menu item. + * + * @example + * ```typescript + * import { Image } from '@tauri-apps/api/image'; + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const icon = await Image.fromPath('icons/icon.png'); + * await getCurrentWindow().setIcon(icon); + * await icon.close(); + * ``` + * + * @since 2.0.0 + */ +export class Image extends Resource { + /** + * Creates an Image from a resource ID. For internal use only. + * + * @ignore + */ + constructor(rid: number) { + super(rid) + } + + /** + * Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height. + * + * The buffer must contain exactly `width * height * 4` bytes. + * Unlike {@linkcode Image.fromBytes} and {@linkcode Image.fromPath} this does not + * decode an image format, so it requires no extra Cargo feature. + * + * @example + * ```typescript + * import { Image } from '@tauri-apps/api/image'; + * + * // a 1x1 opaque red image + * const image = await Image.new(new Uint8Array([255, 0, 0, 255]), 1, 1); + * ``` + * + * @since 2.0.0 + */ + static async new( + rgba: number[] | Uint8Array | ArrayBuffer, + width: number, + height: number + ): Promise { + return invoke('plugin:image|new', { + rgba, + width, + height + }).then((rid) => new Image(rid)) + } + + /** + * Creates a new image using the provided bytes by inferring the file format. + * + * Only `ico` and `png` are supported (based on activated feature flag). + * + * Note that you 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"] } + * ``` + * + * @example + * ```typescript + * import { Image } from '@tauri-apps/api/image'; + * + * const bytes = await fetch('/icon.png').then((r) => r.arrayBuffer()); + * const image = await Image.fromBytes(bytes); + * ``` + * + * @since 2.0.0 + */ + static async fromBytes( + bytes: number[] | Uint8Array | ArrayBuffer + ): Promise { + return invoke('plugin:image|from_bytes', { + bytes + }).then((rid) => new Image(rid)) + } + + /** + * Creates a new image using the provided path. + * + * Only `ico` and `png` are supported (based on activated feature flag). + * + * Note that you 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"] } + * ``` + * + * The path is resolved on the Rust side, so it must be a path on the user's + * machine (for example one returned by the path APIs or a bundled resource + * resolved with `resolveResource`), not a frontend asset URL. + * + * @example + * ```typescript + * import { Image } from '@tauri-apps/api/image'; + * import { resolveResource } from '@tauri-apps/api/path'; + * + * const image = await Image.fromPath(await resolveResource('icons/icon.png')); + * ``` + * + * @since 2.0.0 + */ + static async fromPath(path: string): Promise { + return invoke('plugin:image|from_path', { path }).then( + (rid) => new Image(rid) + ) + } + + /** + * Returns the RGBA data for this image, in row-major order from top to bottom. + * + * The returned buffer has `width * height * 4` bytes, see {@linkcode Image.size}. + * + * @example + * ```typescript + * import { Image } from '@tauri-apps/api/image'; + * + * const image = await Image.fromPath('icons/icon.png'); + * const rgba = await image.rgba(); + * ``` + * + * @since 2.0.0 + */ + async rgba(): Promise> { + return invoke('plugin:image|rgba', { + rid: this.rid + }).then((buffer) => new Uint8Array(buffer)) + } + + /** + * Returns the size of this image, in pixels. + * + * @example + * ```typescript + * import { Image } from '@tauri-apps/api/image'; + * + * const image = await Image.fromPath('icons/icon.png'); + * const { width, height } = await image.size(); + * ``` + * + * @since 2.0.0 + */ + async size(): Promise { + return invoke('plugin:image|size', { rid: this.rid }) + } +} + +/** + * Transforms image from various types into a type acceptable by Rust. + * + * See [`tauri::image::JsImage`](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html) for more information. + * Note the API signature is not stable and might change. + */ +export function transformImage(image: JsImage | null): T { + const ret = + image == null + ? null + : typeof image === 'string' + ? image + : image instanceof Image + ? image.rid + : image + + return ret as T +} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts new file mode 100644 index 0000000..03639e8 --- /dev/null +++ b/packages/api/src/index.ts @@ -0,0 +1,52 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * The Tauri API allows you to interface with the backend layer. + * + * This module exposes all other modules as an object where the key is the module name, and the value is the module exports. + * @example + * ```typescript + * import { event, window, path } from '@tauri-apps/api' + * ``` + * + * ### Vanilla JS API + * + * The above import syntax is for JavaScript/TypeScript with a bundler. If you're using vanilla JavaScript, you can use the global `window.__TAURI__` object instead. It requires `app.withGlobalTauri` configuration option enabled. + * + * @example + * ```js + * const { event, window: tauriWindow, path } = window.__TAURI__; + * ``` + * + * @module + */ + +import * as app from './app' +import * as core from './core' +import * as dpi from './dpi' +import * as event from './event' +import * as image from './image' +import * as menu from './menu' +import * as mocks from './mocks' +import * as path from './path' +import * as tray from './tray' +import * as webview from './webview' +import * as webviewWindow from './webviewWindow' +import * as window from './window' + +export { + app, + core, + dpi, + event, + image, + menu, + mocks, + path, + tray, + webview, + webviewWindow, + window +} diff --git a/packages/api/src/menu.ts b/packages/api/src/menu.ts new file mode 100644 index 0000000..0f8c995 --- /dev/null +++ b/packages/api/src/menu.ts @@ -0,0 +1,31 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Build native application, window and context menus. + * + * A {@linkcode Menu} holds items ({@linkcode MenuItem}, {@linkcode CheckMenuItem}, + * {@linkcode IconMenuItem}, {@linkcode PredefinedMenuItem}) and {@linkcode Submenu}s. + * Use `Menu.setAsAppMenu()` for the macOS application menu, `Menu.setAsWindowMenu()` + * for the Windows/Linux window menu bar, or `Menu.popup()` for a context menu. + * + * Menus live on the Rust side, the frontend only holds handles to them, so keep a + * reference to a menu for as long as it is in use. + * + * This package is also accessible with `window.__TAURI__.menu` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * + * @remarks All commands used by this module are part of the `core:menu:default` + * permission set, which is enabled by default, so no extra capability + * configuration is needed. Menu item icons additionally require the `image-png` / + * `image-ico` Cargo features of the `tauri` crate. + * + * @module + */ + +export * from './menu/submenu' +export * from './menu/menuItem' +export * from './menu/menu' +export * from './menu/checkMenuItem' +export * from './menu/iconMenuItem' +export * from './menu/predefinedMenuItem' diff --git a/packages/api/src/menu/base.ts b/packages/api/src/menu/base.ts new file mode 100644 index 0000000..cc2c9ce --- /dev/null +++ b/packages/api/src/menu/base.ts @@ -0,0 +1,164 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { Channel, invoke, Resource } from '../core' +import { transformImage } from '../image' +import { CheckMenuItemOptions } from './checkMenuItem' +import { IconMenuItemOptions } from './iconMenuItem' +import { MenuOptions } from './menu' +import { MenuItemOptions } from './menuItem' +import { PredefinedMenuItemOptions } from './predefinedMenuItem' +import { SubmenuOptions } from './submenu' + +/** + * The kind of a menu item, used internally to route IPC calls to the right + * Rust-side type. + * + * @ignore + */ +export type ItemKind = + | 'MenuItem' + | 'Predefined' + | 'Check' + | 'Icon' + | 'Submenu' + | 'Menu' + +type MenuItemOptionsAlias = + | MenuItemOptions + | SubmenuOptions + | IconMenuItemOptions + | PredefinedMenuItemOptions + | CheckMenuItemOptions + +function injectChannel(i: MenuItemOptionsAlias): MenuItemOptionsAlias & { + handler?: Channel +} { + if ('items' in i) { + i.items = i.items?.map((item) => + 'rid' in item ? item : injectChannel(item) + ) + } else if ('action' in i && i.action) { + const handler = new Channel() + handler.onmessage = i.action + delete i.action + return { ...i, handler } + } + return i +} + +/** + * Creates a menu or menu item on the Rust side. Implementation detail of the + * `new` static methods of the menu classes. + * + * @ignore + */ +export async function newMenu( + kind: ItemKind, + opts?: + | MenuOptions + | MenuItemOptions + | SubmenuOptions + | PredefinedMenuItemOptions + | CheckMenuItemOptions + | IconMenuItemOptions +): Promise<[number, string]> { + const handler = new Channel() + + if (opts && typeof opts === 'object') { + if ('action' in opts && opts.action) { + handler.onmessage = opts.action + delete opts.action + } + + // about predefined menu item icon + if ( + 'item' in opts + && opts.item + && typeof opts.item === 'object' + && 'About' in opts.item + && opts.item.About + && typeof opts.item.About === 'object' + && 'icon' in opts.item.About + && opts.item.About.icon + ) { + opts.item.About.icon = transformImage(opts.item.About.icon) + } + + // icon menu item icon + if ('icon' in opts && opts.icon) { + opts.icon = transformImage(opts.icon) + } + + // submenu items + if ('items' in opts && opts.items) { + function prepareItem( + i: { rid: number; kind: string } | MenuItemOptionsAlias + ): [number, string] | MenuItemOptionsAlias { + if ('rid' in i) { + return [i.rid, i.kind] + } + + if ('item' in i && typeof i.item === 'object' && i.item.About?.icon) { + i.item.About.icon = transformImage(i.item.About.icon) + } + + if ('icon' in i && i.icon) { + i.icon = transformImage(i.icon) + } + + if ('items' in i && i.items) { + // @ts-expect-error the `prepareItem` return doesn't exactly match + // this is fine, because the difference is in `[number, string]` variant + i.items = i.items.map(prepareItem) + } + + return injectChannel(i) + } + + // @ts-expect-error the `prepareItem` return doesn't exactly match + // this is fine, because the difference is in `[number, string]` variant + opts.items = opts.items.map(prepareItem) + } + } + + return invoke('plugin:menu|new', { + kind, + options: opts, + handler + }) +} + +/** + * The base class of every menu and menu item type. + * + * It is not constructible on its own: create a {@linkcode Menu}, {@linkcode Submenu}, + * {@linkcode MenuItem}, {@linkcode CheckMenuItem}, {@linkcode IconMenuItem} or + * {@linkcode PredefinedMenuItem} instead. It provides the `id` shared by all of + * them and, through {@linkcode Resource}, the `rid` and `close()` used to release + * the Rust-side object. + */ +export class MenuItemBase extends Resource { + /** @ignore */ + readonly #id: string + /** @ignore */ + readonly #kind: ItemKind + + /** The id of this item. */ + get id(): string { + return this.#id + } + + /** @ignore */ + get kind(): string { + return this.#kind + } + + /** @ignore */ + protected constructor(rid: number, id: string, kind: ItemKind) { + super(rid) + this.#id = id + this.#kind = kind + } +} diff --git a/packages/api/src/menu/checkMenuItem.ts b/packages/api/src/menu/checkMenuItem.ts new file mode 100644 index 0000000..b1ce713 --- /dev/null +++ b/packages/api/src/menu/checkMenuItem.ts @@ -0,0 +1,86 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { MenuItemBase, newMenu } from './base' +import { invoke } from '../core' +import { type MenuItemOptions } from '../menu' + +/** Options for creating a new check menu item. */ +export interface CheckMenuItemOptions extends MenuItemOptions { + /** Whether the new check menu item is enabled or not. */ + checked?: boolean +} + +/** + * A check menu item inside a {@linkcode Menu} or {@linkcode Submenu} + * and usually contains a text and a check mark or a similar toggle + * that corresponds to a checked and unchecked states. + */ +export class CheckMenuItem extends MenuItemBase { + /** @ignore */ + protected constructor(rid: number, id: string) { + super(rid, id, 'Check') + } + + /** Create a new check menu item. */ + static async new(opts: CheckMenuItemOptions): Promise { + return newMenu('Check', opts).then( + ([rid, id]) => new CheckMenuItem(rid, id) + ) + } + + /** Returns the text of this check menu item. */ + async text(): Promise { + return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }) + } + + /** Sets the text for this check menu item. */ + async setText(text: string): Promise { + return invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }) + } + + /** Returns whether this check menu item is enabled or not. */ + async isEnabled(): Promise { + return invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind }) + } + + /** Sets whether this check menu item is enabled or not. */ + async setEnabled(enabled: boolean): Promise { + return invoke('plugin:menu|set_enabled', { + rid: this.rid, + kind: this.kind, + enabled + }) + } + + /** + * Sets the accelerator for this check menu item, or removes it when given `null`. + * + * See {@linkcode MenuItemOptions.accelerator} for the accepted format. + */ + async setAccelerator(accelerator: string | null): Promise { + return invoke('plugin:menu|set_accelerator', { + rid: this.rid, + kind: this.kind, + accelerator + }) + } + + /** Returns whether this check menu item is checked or not. */ + async isChecked(): Promise { + return invoke('plugin:menu|is_checked', { rid: this.rid }) + } + + /** Sets whether this check menu item is checked or not. */ + async setChecked(checked: boolean): Promise { + return invoke('plugin:menu|set_checked', { + rid: this.rid, + checked + }) + } +} diff --git a/packages/api/src/menu/iconMenuItem.ts b/packages/api/src/menu/iconMenuItem.ts new file mode 100644 index 0000000..a7b03cd --- /dev/null +++ b/packages/api/src/menu/iconMenuItem.ts @@ -0,0 +1,211 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { MenuItemBase, newMenu } from './base' +import { type MenuItemOptions } from '../menu' +import { invoke } from '../core' +import { transformImage, MenuIcon } from '../image' + +/** + * A native Icon to be used for the menu item + * + * #### Platform-specific: + * + * - **Windows / Linux**: Unsupported. + */ +export enum NativeIcon { + /** An add item template image. */ + Add = 'Add', + /** Advanced preferences toolbar icon for the preferences window. */ + Advanced = 'Advanced', + /** A Bluetooth template image. */ + Bluetooth = 'Bluetooth', + /** Bookmarks image suitable for a template. */ + Bookmarks = 'Bookmarks', + /** A caution image. */ + Caution = 'Caution', + /** A color panel toolbar icon. */ + ColorPanel = 'ColorPanel', + /** A column view mode template image. */ + ColumnView = 'ColumnView', + /** A computer icon. */ + Computer = 'Computer', + /** An enter full-screen mode template image. */ + EnterFullScreen = 'EnterFullScreen', + /** Permissions for all users. */ + Everyone = 'Everyone', + /** An exit full-screen mode template image. */ + ExitFullScreen = 'ExitFullScreen', + /** A cover flow view mode template image. */ + FlowView = 'FlowView', + /** A folder image. */ + Folder = 'Folder', + /** A burnable folder icon. */ + FolderBurnable = 'FolderBurnable', + /** A smart folder icon. */ + FolderSmart = 'FolderSmart', + /** A link template image. */ + FollowLinkFreestanding = 'FollowLinkFreestanding', + /** A font panel toolbar icon. */ + FontPanel = 'FontPanel', + /** A `go back` template image. */ + GoLeft = 'GoLeft', + /** A `go forward` template image. */ + GoRight = 'GoRight', + /** Home image suitable for a template. */ + Home = 'Home', + /** An iChat Theater template image. */ + IChatTheater = 'IChatTheater', + /** An icon view mode template image. */ + IconView = 'IconView', + /** An information toolbar icon. */ + Info = 'Info', + /** A template image used to denote invalid data. */ + InvalidDataFreestanding = 'InvalidDataFreestanding', + /** A generic left-facing triangle template image. */ + LeftFacingTriangle = 'LeftFacingTriangle', + /** A list view mode template image. */ + ListView = 'ListView', + /** A locked padlock template image. */ + LockLocked = 'LockLocked', + /** An unlocked padlock template image. */ + LockUnlocked = 'LockUnlocked', + /** A horizontal dash, for use in menus. */ + MenuMixedState = 'MenuMixedState', + /** A check mark template image, for use in menus. */ + MenuOnState = 'MenuOnState', + /** A MobileMe icon. */ + MobileMe = 'MobileMe', + /** A drag image for multiple items. */ + MultipleDocuments = 'MultipleDocuments', + /** A network icon. */ + Network = 'Network', + /** A path button template image. */ + Path = 'Path', + /** General preferences toolbar icon for the preferences window. */ + PreferencesGeneral = 'PreferencesGeneral', + /** A Quick Look template image. */ + QuickLook = 'QuickLook', + /** A refresh template image. */ + RefreshFreestanding = 'RefreshFreestanding', + /** A refresh template image. */ + Refresh = 'Refresh', + /** A remove item template image. */ + Remove = 'Remove', + /** A reveal contents template image. */ + RevealFreestanding = 'RevealFreestanding', + /** A generic right-facing triangle template image. */ + RightFacingTriangle = 'RightFacingTriangle', + /** A share view template image. */ + Share = 'Share', + /** A slideshow template image. */ + Slideshow = 'Slideshow', + /** A badge for a `smart` item. */ + SmartBadge = 'SmartBadge', + /** Small green indicator, similar to iChat's available image. */ + StatusAvailable = 'StatusAvailable', + /** Small clear indicator. */ + StatusNone = 'StatusNone', + /** Small yellow indicator, similar to iChat's idle image. */ + StatusPartiallyAvailable = 'StatusPartiallyAvailable', + /** Small red indicator, similar to iChat's unavailable image. */ + StatusUnavailable = 'StatusUnavailable', + /** A stop progress template image. */ + StopProgressFreestanding = 'StopProgressFreestanding', + /** A stop progress button template image. */ + StopProgress = 'StopProgress', + /** An image of the empty trash can. */ + TrashEmpty = 'TrashEmpty', + /** An image of the full trash can. */ + TrashFull = 'TrashFull', + /** Permissions for a single user. */ + User = 'User', + /** User account toolbar icon for the preferences window. */ + UserAccounts = 'UserAccounts', + /** Permissions for a group of users. */ + UserGroup = 'UserGroup', + /** Permissions for guests. */ + UserGuest = 'UserGuest' +} + +/** Options for creating a new icon menu item. */ +export interface IconMenuItemOptions extends MenuItemOptions { + /** + * Icon to be used for the new icon menu item. + * + * 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"] } + * ``` + */ + icon?: MenuIcon +} + +/** + * An icon menu item inside a {@linkcode Menu} or {@linkcode Submenu} + * and usually contains an icon and a text. + */ +export class IconMenuItem extends MenuItemBase { + /** @ignore */ + protected constructor(rid: number, id: string) { + super(rid, id, 'Icon') + } + + /** Create a new icon menu item. */ + static async new(opts: IconMenuItemOptions): Promise { + return newMenu('Icon', opts).then(([rid, id]) => new IconMenuItem(rid, id)) + } + + /** Returns the text of this icon menu item. */ + async text(): Promise { + return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }) + } + + /** Sets the text for this icon menu item. */ + async setText(text: string): Promise { + return invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }) + } + + /** Returns whether this icon menu item is enabled or not. */ + async isEnabled(): Promise { + return invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind }) + } + + /** Sets whether this icon menu item is enabled or not. */ + async setEnabled(enabled: boolean): Promise { + return invoke('plugin:menu|set_enabled', { + rid: this.rid, + kind: this.kind, + enabled + }) + } + + /** + * Sets the accelerator for this icon menu item, or removes it when given `null`. + * + * See {@linkcode MenuItemOptions.accelerator} for the accepted format. + */ + async setAccelerator(accelerator: string | null): Promise { + return invoke('plugin:menu|set_accelerator', { + rid: this.rid, + kind: this.kind, + accelerator + }) + } + + /** Sets an icon for this icon menu item */ + async setIcon(icon: MenuIcon | null): Promise { + return invoke('plugin:menu|set_icon', { + rid: this.rid, + kind: this.kind, + icon: transformImage(icon) + }) + } +} diff --git a/packages/api/src/menu/menu.ts b/packages/api/src/menu/menu.ts new file mode 100644 index 0000000..9928311 --- /dev/null +++ b/packages/api/src/menu/menu.ts @@ -0,0 +1,349 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { + MenuItemOptions, + SubmenuOptions, + IconMenuItemOptions, + PredefinedMenuItemOptions, + CheckMenuItemOptions +} from '../menu' +import { MenuItem } from './menuItem' +import { CheckMenuItem } from './checkMenuItem' +import { IconMenuItem } from './iconMenuItem' +import { PredefinedMenuItem } from './predefinedMenuItem' +import { itemFromKind, Submenu } from './submenu' +import { type LogicalPosition, PhysicalPosition, Position } from '../dpi' +import { type Window } from '../window' +import { invoke } from '../core' +import { type ItemKind, MenuItemBase, newMenu } from './base' + +/** Options for creating a new menu. */ +export interface MenuOptions { + /** Specify an id to use for the new menu. */ + id?: string + /** List of items to add to the new menu. */ + items?: Array< + | Submenu + | MenuItem + | PredefinedMenuItem + | CheckMenuItem + | IconMenuItem + | MenuItemOptions + | SubmenuOptions + | IconMenuItemOptions + | PredefinedMenuItemOptions + | CheckMenuItemOptions + > +} + +/** A type that is either a menu bar on the window + * on Windows and Linux or as a global menu in the menubar on macOS. + * + * #### Platform-specific: + * + * - **macOS**: if using {@linkcode Menu} for the global menubar, it can only contain {@linkcode Submenu}s. + */ +export class Menu extends MenuItemBase { + /** @ignore */ + protected constructor(rid: number, id: string) { + super(rid, id, 'Menu') + } + + /** + * Create a new menu. + * + * @example + * ```typescript + * import { Menu, Submenu } from '@tauri-apps/api/menu'; + * + * const menu = await Menu.new({ + * items: [ + * await Submenu.new({ + * text: 'File', + * items: [ + * { id: 'open', text: 'Open', accelerator: 'CmdOrCtrl+O', action: () => console.log('open') }, + * { item: 'Separator' }, + * { item: 'Quit' } + * ] + * }) + * ] + * }); + * ``` + */ + static async new(opts?: MenuOptions): Promise { + return newMenu('Menu', opts).then(([rid, id]) => new Menu(rid, id)) + } + + /** + * Create the default application menu, the same one Tauri installs when no menu + * is configured. + * + * It contains an `Edit` submenu (undo, redo, cut, copy, paste, select all), a + * `Window` submenu (minimize, maximize, close window) and a `Help` submenu, plus: + * + * - **macOS:** an application submenu named after your app (about, services, + * hide, hide others, quit) and a `View` submenu with the fullscreen item. + * - **Windows:** a `File` submenu with close window and quit; the about item + * lives in `Help`. + * - **Linux:** no `File` submenu; the about item lives in `Help`. + * + * Useful as a starting point you then extend with {@linkcode Menu.append}, + * {@linkcode Menu.insert} or {@linkcode Menu.prepend}. + * + * @example + * ```typescript + * import { Menu, Submenu } from '@tauri-apps/api/menu'; + * + * const menu = await Menu.default(); + * await menu.append(await Submenu.new({ text: 'Tools', items: [{ id: 'fmt', text: 'Format' }] })); + * await menu.setAsAppMenu(); + * ``` + */ + static async default(): Promise { + return invoke<[number, string]>('plugin:menu|create_default').then( + ([rid, id]) => new Menu(rid, id) + ) + } + + /** + * Add a menu item to the end of this menu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async append< + T extends + | Submenu + | MenuItem + | PredefinedMenuItem + | CheckMenuItem + | IconMenuItem + | MenuItemOptions + | SubmenuOptions + | IconMenuItemOptions + | PredefinedMenuItemOptions + | CheckMenuItemOptions + >(items: T | T[]): Promise { + return invoke('plugin:menu|append', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => + 'rid' in i ? [i.rid, i.kind] : i + ) + }) + } + + /** + * Add a menu item to the beginning of this menu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async prepend< + T extends + | Submenu + | MenuItem + | PredefinedMenuItem + | CheckMenuItem + | IconMenuItem + | MenuItemOptions + | SubmenuOptions + | IconMenuItemOptions + | PredefinedMenuItemOptions + | CheckMenuItemOptions + >(items: T | T[]): Promise { + return invoke('plugin:menu|prepend', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => + 'rid' in i ? [i.rid, i.kind] : i + ) + }) + } + + /** + * Add a menu item to the specified position in this menu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async insert< + T extends + | Submenu + | MenuItem + | PredefinedMenuItem + | CheckMenuItem + | IconMenuItem + | MenuItemOptions + | SubmenuOptions + | IconMenuItemOptions + | PredefinedMenuItemOptions + | CheckMenuItemOptions + >(items: T | T[], position: number): Promise { + return invoke('plugin:menu|insert', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => + 'rid' in i ? [i.rid, i.kind] : i + ), + position + }) + } + + /** Remove a menu item from this menu. */ + async remove( + item: Submenu | MenuItem | PredefinedMenuItem | CheckMenuItem | IconMenuItem + ): Promise { + return invoke('plugin:menu|remove', { + rid: this.rid, + kind: this.kind, + item: [item.rid, item.kind] + }) + } + + /** Remove a menu item from this menu at the specified position. */ + async removeAt( + position: number + ): Promise< + | Submenu + | MenuItem + | PredefinedMenuItem + | CheckMenuItem + | IconMenuItem + | null + > { + return invoke<[number, string, ItemKind]>('plugin:menu|remove_at', { + rid: this.rid, + kind: this.kind, + position + }).then(itemFromKind) + } + + /** Returns a list of menu items that has been added to this menu. */ + async items(): Promise< + Array< + Submenu | MenuItem | PredefinedMenuItem | CheckMenuItem | IconMenuItem + > + > { + return invoke>('plugin:menu|items', { + rid: this.rid, + kind: this.kind + }).then((i) => i.map(itemFromKind)) + } + + /** Retrieves the menu item matching the given identifier. */ + async get( + id: string + ): Promise< + | Submenu + | MenuItem + | PredefinedMenuItem + | CheckMenuItem + | IconMenuItem + | null + > { + return invoke<[number, string, ItemKind] | null>('plugin:menu|get', { + rid: this.rid, + kind: this.kind, + id + }).then((r) => (r ? itemFromKind(r) : null)) + } + + /** + * Popup this menu as a context menu on the specified window. + * + * Call it from a `contextmenu` DOM listener (and `preventDefault()` on the event) + * to replace the webview context menu with a native one. The promise resolves as + * soon as the menu is shown, not when an item is picked: use each item's `action` + * handler for that. + * + * @example + * ```typescript + * import { Menu } from '@tauri-apps/api/menu'; + * import { LogicalPosition } from '@tauri-apps/api/dpi'; + * + * const menu = await Menu.new({ + * items: [{ id: 'copy', text: 'Copy', action: () => console.log('copy') }] + * }); + * + * document.addEventListener('contextmenu', (event) => { + * event.preventDefault(); + * void menu.popup(new LogicalPosition(event.clientX, event.clientY)); + * }); + * ``` + * + * @param at If a position is provided, it is relative to the window's top-left corner. + * If there isn't one provided, the menu will pop up at the current location of the mouse. + * @param window The window to show the menu on. Defaults to the current window. + */ + async popup( + at?: PhysicalPosition | LogicalPosition | Position, + window?: Window + ): Promise { + return invoke('plugin:menu|popup', { + rid: this.rid, + kind: this.kind, + window: window?.label ?? null, + at: at instanceof Position ? at : at ? new Position(at) : null + }) + } + + /** + * Sets the app-wide menu and returns the previous one. + * + * If a window was not created with an explicit menu or had one set explicitly, + * this menu will be assigned to it. + * + * This is the menu bar shown at the top of the screen on macOS. On Windows and + * Linux, where menus belong to a window, use {@linkcode Menu.setAsWindowMenu} + * to target one window instead. + * + * @example + * ```typescript + * import { Menu } from '@tauri-apps/api/menu'; + * + * const menu = await Menu.default(); + * const previous = await menu.setAsAppMenu(); + * await previous?.close(); + * ``` + * + * @returns The menu that was set before this call, or `null` if there was none. + */ + async setAsAppMenu(): Promise { + return invoke<[number, string] | null>('plugin:menu|set_as_app_menu', { + rid: this.rid + }).then((r) => (r ? new Menu(r[0], r[1]) : null)) + } + + /** + * Sets the window menu and returns the previous one. + * + * #### Platform-specific: + * + * - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one + * window, if you need to set it, use {@linkcode Menu.setAsAppMenu} instead. + * + * @example + * ```typescript + * import { Menu } from '@tauri-apps/api/menu'; + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * + * const menu = await Menu.default(); + * await menu.setAsWindowMenu(getCurrentWindow()); + * ``` + * + * @param window The window to set the menu on. Defaults to the current window. + * @returns The menu that was set on that window before this call, or `null` if there was none. + */ + async setAsWindowMenu(window?: Window): Promise { + return invoke<[number, string] | null>('plugin:menu|set_as_window_menu', { + rid: this.rid, + window: window?.label ?? null + }).then((r) => (r ? new Menu(r[0], r[1]) : null)) + } +} diff --git a/packages/api/src/menu/menuItem.ts b/packages/api/src/menu/menuItem.ts new file mode 100644 index 0000000..5c60858 --- /dev/null +++ b/packages/api/src/menu/menuItem.ts @@ -0,0 +1,126 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { MenuItemBase, newMenu } from './base' +import { invoke } from '../core' + +/** Options for creating a new menu item. */ +export interface MenuItemOptions { + /** Specify an id to use for the new menu item. */ + id?: string + /** The text of the new menu item. */ + text: string + /** Whether the new menu item is enabled or not. */ + enabled?: boolean + /** + * Specify an accelerator (keyboard shortcut) for the new menu item, for example + * `'CmdOrCtrl+Shift+K'`. + * + * The string is a list of zero or more modifiers followed by exactly one key, + * joined with `+`. Matching is case insensitive and spaces around each token are + * ignored, so `'CmdOrCtrl+Shift+K'` and `'cmdorctrl + shift + k'` are equivalent. + * All modifiers must come before the key. + * + * Accepted modifiers: + * + * - `Shift` + * - `Control`, `Ctrl` + * - `Alt`, `Option` + * - `Command`, `Cmd`, `Super` + * - `CmdOrCtrl`, `CmdOrControl`, `CommandOrCtrl`, `CommandOrControl` — `Command` + * on macOS and `Control` everywhere else, which is what you usually want for + * application shortcuts. + * + * Accepted keys: + * + * - Letters `A`-`Z` (also written `KeyA`-`KeyZ`) and digits `0`-`9` (also `Digit0`-`Digit9`). + * - Punctuation, either as the character or by name: `` ` ``/`Backquote`, `\`/`Backslash`, + * `[`/`BracketLeft`, `]`/`BracketRight`, `,`/`Comma`, `=`/`Equal`, `-`/`Minus`, + * `.`/`Period`, `'`/`Quote`, `;`/`Semicolon`, `/`/`Slash`. + * - `Backspace`, `CapsLock`, `Enter`, `Space`, `Tab`, `Delete`, `End`, `Home`, + * `Insert`, `PageDown`, `PageUp`, `PrintScreen`, `ScrollLock`, `NumLock`, + * `Escape` (also `Esc`). + * - Arrows: `ArrowUp`, `ArrowDown`, `ArrowLeft`, `ArrowRight` (also `Up`, `Down`, `Left`, `Right`). + * - Function keys `F1` through `F24`. + * - Numpad keys: `Numpad0`-`Numpad9` (also `Num0`-`Num9`), `NumpadAdd`, `NumpadSubtract`, + * `NumpadMultiply`, `NumpadDivide`, `NumpadDecimal`, `NumpadEnter`, `NumpadEqual` + * (each also accepted with the `Num` prefix, e.g. `NumAdd`). + * - Media keys: `AudioVolumeUp`, `AudioVolumeDown`, `AudioVolumeMute` (also `VolumeUp`, + * `VolumeDown`, `VolumeMute`). + * + * @example + * ```typescript + * import { MenuItem } from '@tauri-apps/api/menu'; + * + * await MenuItem.new({ text: 'Find', accelerator: 'CmdOrCtrl+F' }); + * await MenuItem.new({ text: 'Command palette', accelerator: 'CmdOrCtrl+Shift+P' }); + * await MenuItem.new({ text: 'Refresh', accelerator: 'F5' }); + * ``` + * + * An accelerator that cannot be parsed is ignored and the item is created + * without a shortcut, so double-check the spelling of the modifiers and key. + */ + accelerator?: string + /** Specify a handler to be called when this menu item is activated. */ + action?: (id: string) => void +} + +/** A menu item inside a {@linkcode Menu} or {@linkcode Submenu} and contains only text. */ +export class MenuItem extends MenuItemBase { + /** @ignore */ + protected constructor(rid: number, id: string) { + super(rid, id, 'MenuItem') + } + + /** Create a new menu item. */ + static async new(opts: MenuItemOptions): Promise { + return newMenu('MenuItem', opts).then(([rid, id]) => new MenuItem(rid, id)) + } + + /** Returns the text of this menu item. */ + async text(): Promise { + return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }) + } + + /** Sets the text for this menu item. */ + async setText(text: string): Promise { + return invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }) + } + + /** Returns whether this menu item is enabled or not. */ + async isEnabled(): Promise { + return invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind }) + } + + /** Sets whether this menu item is enabled or not. */ + async setEnabled(enabled: boolean): Promise { + return invoke('plugin:menu|set_enabled', { + rid: this.rid, + kind: this.kind, + enabled + }) + } + + /** + * Sets the accelerator for this menu item, or removes it when given `null`. + * + * See {@linkcode MenuItemOptions.accelerator} for the accepted format. + * + * @example + * ```typescript + * await item.setAccelerator('CmdOrCtrl+Shift+K'); + * ``` + */ + async setAccelerator(accelerator: string | null): Promise { + return invoke('plugin:menu|set_accelerator', { + rid: this.rid, + kind: this.kind, + accelerator + }) + } +} diff --git a/packages/api/src/menu/predefinedMenuItem.ts b/packages/api/src/menu/predefinedMenuItem.ts new file mode 100644 index 0000000..6514018 --- /dev/null +++ b/packages/api/src/menu/predefinedMenuItem.ts @@ -0,0 +1,203 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { MenuItemBase, newMenu } from './base' +import { invoke } from '../core' +import type { JsImage } from '../image' + +/** A metadata for the about predefined menu item. */ +export interface AboutMetadata { + /** Sets the application name. */ + name?: string + /** The application version. */ + version?: string + /** + * The short version, e.g. "1.0". + * + * #### Platform-specific + * + * - **Windows / Linux:** Appended to the end of `version` in parentheses. + */ + shortVersion?: string + /** + * The authors of the application. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + */ + authors?: string[] + /** + * Application comments. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + */ + comments?: string + /** The copyright of the application. */ + copyright?: string + /** + * The license of the application. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + */ + license?: string + /** + * The application website. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + */ + website?: string + /** + * The website label. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + */ + websiteLabel?: string + /** + * The credits. + * + * #### Platform-specific + * + * - **Windows / Linux:** Unsupported. + */ + credits?: string + /** + * The application icon. + * + * #### Platform-specific + * + * - **Windows:** Unsupported. + */ + icon?: JsImage +} + +/** Options for creating a new predefined menu item. */ +export interface PredefinedMenuItemOptions { + /** + * The text of the new predefined menu item. + * + * Defaults to the platform's own label for the chosen item, so only set it to + * override or localize that label. + */ + text?: string + /** + * The predefined item type. + * + * Each variant maps to a native menu item whose behavior is implemented by the + * operating system, so there is no `action` to handle. Variants that are + * unsupported on a platform still create an item, but it does nothing there. + * + * - `Separator`: a horizontal separator line. All platforms. + * - `Copy`, `Cut`, `Paste`, `SelectAll`: the standard clipboard and selection + * commands, applied to the focused text input. All platforms. + * - `Undo`, `Redo`: undo/redo in the focused text input. **macOS only** + * (*Windows / Linux:* unsupported). + * - `Minimize`: minimizes the focused window. *Linux:* unsupported. + * - `Maximize`: maximizes the focused window. *Linux:* unsupported. + * - `Fullscreen`: toggles fullscreen for the focused window. **macOS only** + * (*Windows / Linux:* unsupported). + * - `Hide`: hides the application. *Linux:* unsupported. + * - `HideOthers`: hides every other application. *Linux:* unsupported. + * - `ShowAll`: shows all hidden applications. **macOS only** + * (*Windows / Linux:* unsupported). + * - `CloseWindow`: closes the focused window. *Linux:* unsupported. + * - `Quit`: quits the application. *Linux:* unsupported. + * - `Services`: the macOS *Services* submenu. **macOS only** + * (*Windows / Linux:* unsupported). + * - `BringAllToFront`: brings all of the app's windows to the front. **macOS only** + * (*Windows / Linux:* unsupported). + * - `{ About: AboutMetadata | null }`: opens an about dialog. All platforms; pass + * `null` to use the values from your `tauri.conf.json`, or see + * {@linkcode AboutMetadata} for the per-field platform support. + * + * @example + * ```typescript + * import { Menu, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu'; + * + * const edit = await Submenu.new({ + * text: 'Edit', + * items: [ + * { item: 'Undo' }, + * { item: 'Redo' }, + * { item: 'Separator' }, + * { item: 'Cut' }, + * { item: 'Copy' }, + * { item: 'Paste' }, + * { item: 'SelectAll' } + * ] + * }); + * + * const quit = await PredefinedMenuItem.new({ item: 'Quit', text: 'Quit my app' }); + * const menu = await Menu.new({ items: [edit, quit] }); + * ``` + */ + item: + | 'Separator' + | 'Copy' + | 'Cut' + | 'Paste' + | 'SelectAll' + | 'Undo' + | 'Redo' + | 'Minimize' + | 'Maximize' + | 'Fullscreen' + | 'Hide' + | 'HideOthers' + | 'ShowAll' + | 'CloseWindow' + | 'Quit' + | 'Services' + | 'BringAllToFront' + | { + About: AboutMetadata | null + } +} + +/** A predefined (native) menu item which has a predefined behavior by the OS or by tauri. */ +export class PredefinedMenuItem extends MenuItemBase { + /** @ignore */ + protected constructor(rid: number, id: string) { + super(rid, id, 'Predefined') + } + + /** + * Create a new predefined menu item. + * + * @example + * ```typescript + * import { PredefinedMenuItem } from '@tauri-apps/api/menu'; + * const separator = await PredefinedMenuItem.new({ item: 'Separator' }); + * ``` + */ + static async new( + opts?: PredefinedMenuItemOptions + ): Promise { + return newMenu('Predefined', opts).then( + ([rid, id]) => new PredefinedMenuItem(rid, id) + ) + } + + /** Returns the text of this predefined menu item. */ + async text(): Promise { + return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }) + } + + /** Sets the text for this predefined menu item. */ + async setText(text: string): Promise { + return invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }) + } +} diff --git a/packages/api/src/menu/submenu.ts b/packages/api/src/menu/submenu.ts new file mode 100644 index 0000000..118bb9a --- /dev/null +++ b/packages/api/src/menu/submenu.ts @@ -0,0 +1,305 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { + IconMenuItemOptions, + PredefinedMenuItemOptions, + CheckMenuItemOptions +} from '../menu' +import { MenuItem, type MenuItemOptions } from './menuItem' +import { CheckMenuItem } from './checkMenuItem' +import { IconMenuItem } from './iconMenuItem' +import { PredefinedMenuItem } from './predefinedMenuItem' +import { invoke } from '../core' +import { type LogicalPosition, PhysicalPosition, type Window } from '../window' +import { type ItemKind, MenuItemBase, newMenu } from './base' +import { type MenuOptions } from './menu' +import { Position } from '../dpi' +import { transformImage, MenuIcon } from '../image' + +/** @ignore */ +export function itemFromKind([rid, id, kind]: [number, string, ItemKind]): + | Submenu + | MenuItem + | PredefinedMenuItem + | CheckMenuItem + | IconMenuItem { + /* eslint-disable @typescript-eslint/no-unsafe-return */ + switch (kind) { + case 'Submenu': + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new Submenu(rid, id) + case 'Predefined': + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new PredefinedMenuItem(rid, id) + case 'Check': + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new CheckMenuItem(rid, id) + case 'Icon': + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new IconMenuItem(rid, id) + case 'MenuItem': + default: + // @ts-expect-error constructor is protected for external usage only, safe for us to use + return new MenuItem(rid, id) + } + /* eslint-enable @typescript-eslint/no-unsafe-return */ +} + +export type SubmenuOptions = (Omit + & MenuOptions) & { + /** + * Icon to be used for the submenu. + * Note: you may need the `image-ico` or `image-png` Cargo features to use this API. + */ + icon?: MenuIcon +} + +/** A type that is a submenu inside a {@linkcode Menu} or {@linkcode Submenu}. */ +export class Submenu extends MenuItemBase { + /** @ignore */ + protected constructor(rid: number, id: string) { + super(rid, id, 'Submenu') + } + + /** Create a new submenu. */ + static async new(opts: SubmenuOptions): Promise { + return newMenu('Submenu', opts).then(([rid, id]) => new Submenu(rid, id)) + } + + /** Returns the text of this submenu. */ + async text(): Promise { + return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind }) + } + + /** Sets the text for this submenu. */ + async setText(text: string): Promise { + return invoke('plugin:menu|set_text', { + rid: this.rid, + kind: this.kind, + text + }) + } + + /** Returns whether this submenu is enabled or not. */ + async isEnabled(): Promise { + return invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind }) + } + + /** Sets whether this submenu is enabled or not. */ + async setEnabled(enabled: boolean): Promise { + return invoke('plugin:menu|set_enabled', { + rid: this.rid, + kind: this.kind, + enabled + }) + } + + /** + * Add a menu item to the end of this submenu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async append< + T extends + | Submenu + | MenuItem + | PredefinedMenuItem + | CheckMenuItem + | IconMenuItem + | MenuItemOptions + | SubmenuOptions + | IconMenuItemOptions + | PredefinedMenuItemOptions + | CheckMenuItemOptions + >(items: T | T[]): Promise { + return invoke('plugin:menu|append', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => + 'rid' in i ? [i.rid, i.kind] : i + ) + }) + } + + /** + * Add a menu item to the beginning of this submenu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async prepend< + T extends + | Submenu + | MenuItem + | PredefinedMenuItem + | CheckMenuItem + | IconMenuItem + | MenuItemOptions + | SubmenuOptions + | IconMenuItemOptions + | PredefinedMenuItemOptions + | CheckMenuItemOptions + >(items: T | T[]): Promise { + return invoke('plugin:menu|prepend', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => + 'rid' in i ? [i.rid, i.kind] : i + ) + }) + } + + /** + * Add a menu item to the specified position in this submenu. + * + * #### Platform-specific: + * + * - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}. + */ + async insert< + T extends + | Submenu + | MenuItem + | PredefinedMenuItem + | CheckMenuItem + | IconMenuItem + | MenuItemOptions + | SubmenuOptions + | IconMenuItemOptions + | PredefinedMenuItemOptions + | CheckMenuItemOptions + >(items: T | T[], position: number): Promise { + return invoke('plugin:menu|insert', { + rid: this.rid, + kind: this.kind, + items: (Array.isArray(items) ? items : [items]).map((i) => + 'rid' in i ? [i.rid, i.kind] : i + ), + position + }) + } + + /** Remove a menu item from this submenu. */ + async remove( + item: Submenu | MenuItem | PredefinedMenuItem | CheckMenuItem | IconMenuItem + ): Promise { + return invoke('plugin:menu|remove', { + rid: this.rid, + kind: this.kind, + item: [item.rid, item.kind] + }) + } + + /** Remove a menu item from this submenu at the specified position. */ + async removeAt( + position: number + ): Promise< + | Submenu + | MenuItem + | PredefinedMenuItem + | CheckMenuItem + | IconMenuItem + | null + > { + return invoke<[number, string, ItemKind]>('plugin:menu|remove_at', { + rid: this.rid, + kind: this.kind, + position + }).then(itemFromKind) + } + + /** Returns a list of menu items that has been added to this submenu. */ + async items(): Promise< + Array< + Submenu | MenuItem | PredefinedMenuItem | CheckMenuItem | IconMenuItem + > + > { + return invoke>('plugin:menu|items', { + rid: this.rid, + kind: this.kind + }).then((i) => i.map(itemFromKind)) + } + + /** Retrieves the menu item matching the given identifier. */ + async get( + id: string + ): Promise< + | Submenu + | MenuItem + | PredefinedMenuItem + | CheckMenuItem + | IconMenuItem + | null + > { + return invoke<[number, string, ItemKind] | null>('plugin:menu|get', { + rid: this.rid, + kind: this.kind, + id + }).then((r) => (r ? itemFromKind(r) : null)) + } + + /** + * Popup this submenu as a context menu on the specified window. + * + * If the position, is provided, it is relative to the window's top-left corner. + */ + async popup( + at?: PhysicalPosition | LogicalPosition, + window?: Window + ): Promise { + return invoke('plugin:menu|popup', { + rid: this.rid, + kind: this.kind, + window: window?.label ?? null, + at: at instanceof Position ? at : at ? new Position(at) : null + }) + } + + /** + * Set this submenu as the Window menu for the application on macOS. + * + * This will cause macOS to automatically add window-switching items and + * certain other items to the menu. + * + * #### Platform-specific: + * + * - **Windows / Linux**: Unsupported. + */ + async setAsWindowsMenuForNSApp(): Promise { + return invoke('plugin:menu|set_as_windows_menu_for_nsapp', { + rid: this.rid + }) + } + + /** + * Set this submenu as the Help menu for the application on macOS. + * + * This will cause macOS to automatically add a search box to the menu. + * + * If no menu is set as the Help menu, macOS will automatically use any menu + * which has a title matching the localized word "Help". + * + * #### Platform-specific: + * + * - **Windows / Linux**: Unsupported. + */ + async setAsHelpMenuForNSApp(): Promise { + return invoke('plugin:menu|set_as_help_menu_for_nsapp', { + rid: this.rid + }) + } + + /** Sets an icon for this submenu */ + async setIcon(icon: MenuIcon | null): Promise { + return invoke('plugin:menu|set_icon', { + rid: this.rid, + kind: this.kind, + icon: transformImage(icon) + }) + } +} diff --git a/packages/api/src/mocks.ts b/packages/api/src/mocks.ts new file mode 100644 index 0000000..3566625 --- /dev/null +++ b/packages/api/src/mocks.ts @@ -0,0 +1,380 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Test helpers that stand in for the Tauri backend. + * + * Importing any other module of this package in a plain Node/browser test + * environment fails, because there is no `window.__TAURI_INTERNALS__` to talk to. + * The functions here install a fake one: {@linkcode mockIPC} intercepts `invoke` + * calls, {@linkcode mockWindows} makes the window/webview APIs believe a set of + * windows exists, and {@linkcode mockConvertFileSrc} makes `convertFileSrc` return + * the URL shape of a given platform. + * + * Call {@linkcode clearMocks} after each test when your runner reuses the same + * `window` object between tests. + * + * @example + * ```typescript + * import { beforeEach, afterEach, expect, test } from 'vitest'; + * import { mockIPC, clearMocks } from '@tauri-apps/api/mocks'; + * import { invoke } from '@tauri-apps/api/core'; + * + * afterEach(() => { + * clearMocks(); + * }); + * + * test('greet', async () => { + * mockIPC((cmd, args) => { + * if (cmd === 'greet') { + * return `Hello, ${(args as { name: string }).name}!`; + * } + * }); + * + * await expect(invoke('greet', { name: 'tauri' })).resolves.toBe('Hello, tauri!'); + * }); + * ``` + * + * This package is also accessible with `window.__TAURI__.mocks` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * + * @module + */ + +import type { InvokeArgs, InvokeOptions } from './core' +import { EventName } from './event' + +function mockInternals() { + window.__TAURI_INTERNALS__ = window.__TAURI_INTERNALS__ ?? {} + window.__TAURI_EVENT_PLUGIN_INTERNALS__ = + window.__TAURI_EVENT_PLUGIN_INTERNALS__ ?? {} +} + +/** + * Options for `mockIPC`. + * + * # Options + * `shouldMockEvents`: If true, the `listen` and `emit` functions will be mocked, allowing you to test event handling without a real backend. + * **This will consume any events emitted with the `plugin:event` prefix.** + * + * @since 2.7.0 + */ +export interface MockIPCOptions { + shouldMockEvents?: boolean +} + +/** + * Intercepts all IPC requests with the given mock handler. + * + * This function can be used when testing tauri frontend applications or when running the frontend in a Node.js context during static site generation. + * + * # Examples + * + * Testing setup using Vitest: + * ```ts + * import { mockIPC, clearMocks } from "@tauri-apps/api/mocks" + * import { invoke } from "@tauri-apps/api/core" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked command", () => { + * mockIPC((cmd, payload) => { + * switch (cmd) { + * case "add": + * return (payload.a as number) + (payload.b as number); + * default: + * break; + * } + * }); + * + * expect(invoke('add', { a: 12, b: 15 })).resolves.toBe(27); + * }) + * ``` + * + * The callback function can also return a Promise: + * ```js + * import { mockIPC, clearMocks } from "@tauri-apps/api/mocks" + * import { invoke } from "@tauri-apps/api/core" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked command", () => { + * mockIPC((cmd, payload) => { + * if(cmd === "get_data") { + * return fetch("https://example.com/data.json") + * .then((response) => response.json()) + * } + * }); + * + * expect(invoke('get_data')).resolves.toBe({ foo: 'bar' }); + * }) + * ``` + * + * `listen` can also be mocked with direct calls to the `emit` function. This functionality is opt-in via the `shouldMockEvents` option: + * ```js + * import { mockIPC, clearMocks } from "@tauri-apps/api/mocks" + * import { emit, listen } from "@tauri-apps/api/event" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked event", () => { + * mockIPC(() => {}, { shouldMockEvents: true }); // enable event mocking + * + * const eventHandler = vi.fn(); + * listen('test-event', eventHandler); // typically in component setup or similar + * + * emit('test-event', { foo: 'bar' }); + * expect(eventHandler).toHaveBeenCalledWith({ + * event: 'test-event', + * payload: { foo: 'bar' } + * }); + * }) + * ``` + * `emitTo` is currently **not** supported by this mock implementation. + * + * @since 1.0.0 + */ +export function mockIPC( + cb: (cmd: string, payload?: InvokeArgs) => unknown, + options?: MockIPCOptions +): void { + mockInternals() + + function isEventPluginInvoke(cmd: string): boolean { + return cmd.startsWith('plugin:event|') + } + + function handleEventPlugin(cmd: string, args?: InvokeArgs): unknown { + switch (cmd) { + case 'plugin:event|listen': + return handleListen(args as { event: EventName; handler: number }) + case 'plugin:event|emit': + return handleEmit(args as { event: EventName; payload?: unknown }) + case 'plugin:event|unlisten': + return handleRemoveListener(args as { event: EventName; id: number }) + } + } + + const listeners = new Map() + function handleListen(args: { event: EventName; handler: number }) { + if (!listeners.has(args.event)) { + listeners.set(args.event, []) + } + listeners.get(args.event)!.push(args.handler) + return args.handler + } + + function handleEmit(args: { event: EventName; payload?: unknown }) { + const eventListeners = listeners.get(args.event) || [] + for (const handler of eventListeners) { + runCallback(handler, args) + } + return null + } + function handleRemoveListener(args: { event: EventName; id: number }) { + const eventListeners = listeners.get(args.event) + if (eventListeners) { + const index = eventListeners.indexOf(args.id) + if (index !== -1) { + eventListeners.splice(index, 1) + } + } + } + + // eslint-disable-next-line @typescript-eslint/require-await + async function invoke( + cmd: string, + args?: InvokeArgs, + _options?: InvokeOptions + ): Promise { + if (options?.shouldMockEvents && isEventPluginInvoke(cmd)) { + return handleEventPlugin(cmd, args) as T + } + + return cb(cmd, args) as T + } + + const callbacks = new Map void>() + + function registerCallback( + callback?: (response: T) => void, + once = false + ) { + const identifier = window.crypto.getRandomValues(new Uint32Array(1))[0] + callbacks.set(identifier, (data) => { + if (once) { + unregisterCallback(identifier) + } + return callback && callback(data as T) + }) + return identifier + } + + function unregisterCallback(id: number) { + callbacks.delete(id) + } + + function runCallback(id: number, data: unknown) { + const callback = callbacks.get(id) + if (callback) { + callback(data) + } else { + // eslint-disable-next-line no-console + console.warn( + `[TAURI] Couldn't find callback id ${id}. This might happen when the app is reloaded while Rust is running an asynchronous operation.` + ) + } + } + + function unregisterListener(event: EventName, id: number) { + unregisterCallback(id) + } + + window.__TAURI_INTERNALS__.invoke = invoke + window.__TAURI_INTERNALS__.transformCallback = registerCallback + window.__TAURI_INTERNALS__.unregisterCallback = unregisterCallback + window.__TAURI_INTERNALS__.runCallback = runCallback + window.__TAURI_INTERNALS__.callbacks = callbacks + window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener = + unregisterListener +} + +/** + * Mocks one or many window labels. + * In non-tauri context it is required to call this function *before* using the `@tauri-apps/api/window` module. + * + * This function only mocks the *presence* of windows, + * window properties (e.g. width and height) can be mocked like regular IPC calls using the `mockIPC` function. + * + * # Examples + * + * ```js + * import { mockWindows } from "@tauri-apps/api/mocks"; + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * + * mockWindows("main", "second", "third"); + * + * const win = getCurrentWindow(); + * + * win.label // "main" + * ``` + * + * ```js + * import { mockWindows } from "@tauri-apps/api/mocks"; + * + * mockWindows("main", "second", "third"); + * + * mockIPC((cmd, args) => { + * if (cmd === "plugin:event|emit") { + * console.log('emit event', args?.event, args?.payload); + * } + * }); + * + * const { emit } = await import("@tauri-apps/api/event"); + * await emit('loaded'); // this will cause the mocked IPC handler to log to the console. + * ``` + * + * @param current Label of window this JavaScript context is running in. + * + * @since 1.0.0 + */ +export function mockWindows( + current: string, + ..._additionalWindows: string[] +): void { + mockInternals() + window.__TAURI_INTERNALS__.metadata = { + currentWindow: { label: current }, + currentWebview: { windowLabel: current, label: current } + } +} + +/** + * Mock `convertFileSrc` function + * + * + * @example + * ```js + * import { mockConvertFileSrc } from "@tauri-apps/api/mocks"; + * import { convertFileSrc } from "@tauri-apps/api/core"; + * + * mockConvertFileSrc("windows") + * + * const url = convertFileSrc("C:\\Users\\user\\file.txt") + * ``` + * + * @param osName The operating system to mock, can be one of linux, macos, or windows + * + * @since 1.6.0 + */ +export function mockConvertFileSrc(osName: string): void { + mockInternals() + window.__TAURI_INTERNALS__.convertFileSrc = function ( + filePath, + protocol = 'asset' + ) { + const path = encodeURIComponent(filePath) + return osName === 'windows' + ? `http://${protocol}.localhost/${path}` + : `${protocol}://localhost/${path}` + } +} + +/** + * Clears mocked functions/data injected by the other functions in this module. + * When using a test runner that doesn't provide a fresh window object for each test, calling this function will reset tauri specific properties. + * + * # Example + * + * ```js + * import { mockWindows, clearMocks } from "@tauri-apps/api/mocks" + * + * afterEach(() => { + * clearMocks() + * }) + * + * test("mocked windows", () => { + * mockWindows("main", "second", "third"); + * + * expect(window.__TAURI_INTERNALS__).toHaveProperty("metadata") + * }) + * + * test("no mocked windows", () => { + * expect(window.__TAURI_INTERNALS__).not.toHaveProperty("metadata") + * }) + * ``` + * + * @since 1.0.0 + */ +export function clearMocks(): void { + if (typeof window.__TAURI_INTERNALS__ !== 'object') { + return + } + + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.invoke + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.transformCallback + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.unregisterCallback + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.runCallback + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.callbacks + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.convertFileSrc + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_INTERNALS__.metadata + + if (typeof window.__TAURI_EVENT_PLUGIN_INTERNALS__ !== 'object') { + return + } + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case + delete window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener +} diff --git a/packages/api/src/path.ts b/packages/api/src/path.ts new file mode 100644 index 0000000..9fecce1 --- /dev/null +++ b/packages/api/src/path.ts @@ -0,0 +1,853 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * The path module provides utilities for working with file and directory paths. + * + * This package is also accessible with `window.__TAURI__.path` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * + * It is recommended to allowlist only the APIs you use for optimal bundle size and security. + * + * #### Platform-specific + * + * - **Android:** The user directories are sandboxed per app. `desktopDir`, + * `executableDir`, `fontDir`, `runtimeDir` and `templateDir` — and the matching + * {@link BaseDirectory} variants — do not exist on Android at all. The remaining + * ones resolve inside the app's own storage, so they are not shared with other + * apps, and `videoDir` resolves to the app-specific Movies directory. + * - **iOS:** Every directory resolves inside the app sandbox, so paths are private + * to your app and change between installs. Resolve them at runtime instead of + * persisting absolute paths. + * + * @remarks All commands used by this module are part of the `core:path:default` + * permission set, which is enabled by default, so no extra capability + * configuration is needed. + * + * @module + */ + +import { invoke } from './core' + +/** + * @since 2.0.0 + */ +enum BaseDirectory { + /** + * @see {@link audioDir} for more information. + */ + Audio = 1, + /** + * @see {@link cacheDir} for more information. + */ + Cache = 2, + /** + * @see {@link configDir} for more information. + */ + Config = 3, + /** + * @see {@link dataDir} for more information. + */ + Data = 4, + /** + * @see {@link localDataDir} for more information. + */ + LocalData = 5, + /** + * @see {@link documentDir} for more information. + */ + Document = 6, + /** + * @see {@link downloadDir} for more information. + */ + Download = 7, + /** + * @see {@link pictureDir} for more information. + */ + Picture = 8, + /** + * @see {@link publicDir} for more information. + */ + Public = 9, + /** + * @see {@link videoDir} for more information. + * + * #### Platform-specific + * + * - **Android:** Resolves to the app-specific Movies directory since 2.12.0, + * see {@link videoDir}. + */ + Video = 10, + /** + * @see {@link resourceDir} for more information. + */ + Resource = 11, + /** + * @see {@link tempDir} for more information. + */ + Temp = 12, + /** + * @see {@link appConfigDir} for more information. + */ + AppConfig = 13, + /** + * @see {@link appDataDir} for more information. + */ + AppData = 14, + /** + * @see {@link appLocalDataDir} for more information. + */ + AppLocalData = 15, + /** + * @see {@link appCacheDir} for more information. + */ + AppCache = 16, + /** + * @see {@link appLogDir} for more information. + */ + AppLog = 17, + /** + * @see {@link desktopDir} for more information. + * + * #### Platform-specific + * + * - **Android:** Unsupported, this variant does not exist on Android. + */ + Desktop = 18, + /** + * @see {@link executableDir} for more information. + * + * #### Platform-specific + * + * - **Android:** Unsupported, this variant does not exist on Android. + */ + Executable = 19, + /** + * @see {@link fontDir} for more information. + * + * #### Platform-specific + * + * - **Android:** Unsupported, this variant does not exist on Android. + */ + Font = 20, + /** + * @see {@link homeDir} for more information. + */ + Home = 21, + /** + * @see {@link runtimeDir} for more information. + * + * #### Platform-specific + * + * - **Android:** Unsupported, this variant does not exist on Android. + */ + Runtime = 22, + /** + * @see {@link templateDir} for more information. + * + * #### Platform-specific + * + * - **Android:** Unsupported, this variant does not exist on Android. + */ + Template = 23 +} + +/** + * Returns the path to the suggested directory for your app's config files. + * Resolves to `${configDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appConfigDir } from '@tauri-apps/api/path'; + * const appConfigDirPath = await appConfigDir(); + * ``` + * + * @since 1.2.0 + */ +async function appConfigDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.AppConfig + }) +} + +/** + * Returns the path to the suggested directory for your app's data files. + * Resolves to `${dataDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * ``` + * + * @since 1.2.0 + */ +async function appDataDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.AppData + }) +} + +/** + * Returns the path to the suggested directory for your app's local data files. + * Resolves to `${localDataDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appLocalDataDir } from '@tauri-apps/api/path'; + * const appLocalDataDirPath = await appLocalDataDir(); + * ``` + * + * @since 1.2.0 + */ +async function appLocalDataDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.AppLocalData + }) +} + +/** + * Returns the path to the suggested directory for your app's cache files. + * Resolves to `${cacheDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`. + * @example + * ```typescript + * import { appCacheDir } from '@tauri-apps/api/path'; + * const appCacheDirPath = await appCacheDir(); + * ``` + * + * @since 1.2.0 + */ +async function appCacheDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.AppCache + }) +} + +/** + * Returns the path to the user's audio directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_MUSIC_DIR`. + * - **macOS:** Resolves to `$HOME/Music`. + * - **Windows:** Resolves to `{FOLDERID_Music}`. + * @example + * ```typescript + * import { audioDir } from '@tauri-apps/api/path'; + * const audioDirPath = await audioDir(); + * ``` + * + * @since 1.0.0 + */ +async function audioDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Audio + }) +} + +/** + * Returns the path to the user's cache directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_CACHE_HOME` or `$HOME/.cache`. + * - **macOS:** Resolves to `$HOME/Library/Caches`. + * - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. + * @example + * ```typescript + * import { cacheDir } from '@tauri-apps/api/path'; + * const cacheDirPath = await cacheDir(); + * ``` + * + * @since 1.0.0 + */ +async function cacheDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Cache + }) +} + +/** + * Returns the path to the user's config directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_CONFIG_HOME` or `$HOME/.config`. + * - **macOS:** Resolves to `$HOME/Library/Application Support`. + * - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. + * @example + * ```typescript + * import { configDir } from '@tauri-apps/api/path'; + * const configDirPath = await configDir(); + * ``` + * + * @since 1.0.0 + */ +async function configDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Config + }) +} + +/** + * Returns the path to the user's data directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. + * - **macOS:** Resolves to `$HOME/Library/Application Support`. + * - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. + * @example + * ```typescript + * import { dataDir } from '@tauri-apps/api/path'; + * const dataDirPath = await dataDir(); + * ``` + * + * @since 1.0.0 + */ +async function dataDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Data + }) +} + +/** + * Returns the path to the user's desktop directory. + * + * #### Platform-specific + * + * - **Android:** Unsupported. Android has no desktop directory, the + * {@link BaseDirectory.Desktop} variant does not exist there and the call rejects. + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DESKTOP_DIR`. + * - **macOS:** Resolves to `$HOME/Desktop`. + * - **Windows:** Resolves to `{FOLDERID_Desktop}`. + * @example + * ```typescript + * import { desktopDir } from '@tauri-apps/api/path'; + * const desktopPath = await desktopDir(); + * ``` + * + * @since 1.0.0 + */ +async function desktopDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Desktop + }) +} + +/** + * Returns the path to the user's document directory. + * @example + * ```typescript + * import { documentDir } from '@tauri-apps/api/path'; + * const documentDirPath = await documentDir(); + * ``` + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOCUMENTS_DIR`. + * - **macOS:** Resolves to `$HOME/Documents`. + * - **Windows:** Resolves to `{FOLDERID_Documents}`. + * + * @since 1.0.0 + */ +async function documentDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Document + }) +} + +/** + * Returns the path to the user's download directory. + * + * #### Platform-specific + * + * - **Linux**: Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOWNLOAD_DIR`. + * - **macOS**: Resolves to `$HOME/Downloads`. + * - **Windows**: Resolves to `{FOLDERID_Downloads}`. + * @example + * ```typescript + * import { downloadDir } from '@tauri-apps/api/path'; + * const downloadDirPath = await downloadDir(); + * ``` + * + * @since 1.0.0 + */ +async function downloadDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Download + }) +} + +/** + * Returns the path to the user's executable directory. + * + * #### Platform-specific + * + * - **Android:** Unsupported. The {@link BaseDirectory.Executable} variant does not + * exist there and the call rejects. + * + * - **Linux:** Resolves to `$XDG_BIN_HOME/../bin` or `$XDG_DATA_HOME/../bin` or `$HOME/.local/bin`. + * - **macOS:** Not supported. + * - **Windows:** Not supported. + * @example + * ```typescript + * import { executableDir } from '@tauri-apps/api/path'; + * const executableDirPath = await executableDir(); + * ``` + * + * @since 1.0.0 + */ +async function executableDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Executable + }) +} + +/** + * Returns the path to the user's font directory. + * + * #### Platform-specific + * + * - **Android:** Unsupported. The {@link BaseDirectory.Font} variant does not exist + * there and the call rejects. + * + * - **Linux:** Resolves to `$XDG_DATA_HOME/fonts` or `$HOME/.local/share/fonts`. + * - **macOS:** Resolves to `$HOME/Library/Fonts`. + * - **Windows:** Not supported. + * @example + * ```typescript + * import { fontDir } from '@tauri-apps/api/path'; + * const fontDirPath = await fontDir(); + * ``` + * + * @since 1.0.0 + */ +async function fontDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Font + }) +} + +/** + * Returns the path to the user's home directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$HOME`. + * - **macOS:** Resolves to `$HOME`. + * - **Windows:** Resolves to `{FOLDERID_Profile}`. + * @example + * ```typescript + * import { homeDir } from '@tauri-apps/api/path'; + * const homeDirPath = await homeDir(); + * ``` + * + * @since 1.0.0 + */ +async function homeDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Home + }) +} + +/** + * Returns the path to the user's local data directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. + * - **macOS:** Resolves to `$HOME/Library/Application Support`. + * - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. + * @example + * ```typescript + * import { localDataDir } from '@tauri-apps/api/path'; + * const localDataDirPath = await localDataDir(); + * ``` + * + * @since 1.0.0 + */ +async function localDataDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.LocalData + }) +} + +/** + * Returns the path to the user's picture directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PICTURES_DIR`. + * - **macOS:** Resolves to `$HOME/Pictures`. + * - **Windows:** Resolves to `{FOLDERID_Pictures}`. + * @example + * ```typescript + * import { pictureDir } from '@tauri-apps/api/path'; + * const pictureDirPath = await pictureDir(); + * ``` + * + * @since 1.0.0 + */ +async function pictureDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Picture + }) +} + +/** + * Returns the path to the user's public directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PUBLICSHARE_DIR`. + * - **macOS:** Resolves to `$HOME/Public`. + * - **Windows:** Resolves to `{FOLDERID_Public}`. + * @example + * ```typescript + * import { publicDir } from '@tauri-apps/api/path'; + * const publicDirPath = await publicDir(); + * ``` + * + * @since 1.0.0 + */ +async function publicDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Public + }) +} + +/** + * Returns the path to the application's resource directory. + * To resolve a resource path, see {@linkcode resolveResource}. + * + * ## Platform-specific + * + * Although we provide the exact path where this function resolves to, + * this is not a contract and things might change in the future + * + * - **Windows:** Resolves to the directory that contains the main executable. + * - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to + * the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`. + * If not running in an AppImage, the path is `/usr/lib/${exe_name}`. + * When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`. + * - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app). + * - **iOS:** Resolves to `${exe_dir}/assets`. + * - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path, + * we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/), + * + * @example + * ```typescript + * import { resourceDir } from '@tauri-apps/api/path'; + * const resourceDirPath = await resourceDir(); + * ``` + * + * @since 1.0.0 + */ +async function resourceDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Resource + }) +} + +/** + * Resolve the path to a resource file. + * @example + * ```typescript + * import { resolveResource } from '@tauri-apps/api/path'; + * const resourcePath = await resolveResource('script.sh'); + * ``` + * + * @param resourcePath The path to the resource. + * Must follow the same syntax as defined in `tauri.conf.json > bundle > resources`, i.e. keeping subfolders and parent dir components (`../`). + * @returns The full path to the resource. + * + * @since 1.0.0 + */ +async function resolveResource(resourcePath: string): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Resource, + path: resourcePath + }) +} + +/** + * Returns the path to the user's runtime directory. + * + * #### Platform-specific + * + * - **Android:** Unsupported. The {@link BaseDirectory.Runtime} variant does not + * exist there and the call rejects. + * + * - **Linux:** Resolves to `$XDG_RUNTIME_DIR`. + * - **macOS:** Not supported. + * - **Windows:** Not supported. + * @example + * ```typescript + * import { runtimeDir } from '@tauri-apps/api/path'; + * const runtimeDirPath = await runtimeDir(); + * ``` + * + * @since 1.0.0 + */ +async function runtimeDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Runtime + }) +} + +/** + * Returns the path to the user's template directory. + * + * #### Platform-specific + * + * - **Android:** Unsupported. The {@link BaseDirectory.Template} variant does not + * exist there and the call rejects. + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_TEMPLATES_DIR`. + * - **macOS:** Not supported. + * - **Windows:** Resolves to `{FOLDERID_Templates}`. + * @example + * ```typescript + * import { templateDir } from '@tauri-apps/api/path'; + * const templateDirPath = await templateDir(); + * ``` + * + * @since 1.0.0 + */ +async function templateDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Template + }) +} + +/** + * Returns the path to the user's video directory. + * + * #### Platform-specific + * + * - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_VIDEOS_DIR`. + * - **macOS:** Resolves to `$HOME/Movies`. + * - **Windows:** Resolves to `{FOLDERID_Videos}`. + * - **Android:** Resolves to the app-specific Movies directory + * (`getExternalFilesDir(DIRECTORY_MOVIES)`), typically `.../files/Movies`. It is + * private to your app and removed when the app is uninstalled. + * - **iOS:** Resolves inside the app sandbox. + * @example + * ```typescript + * import { videoDir } from '@tauri-apps/api/path'; + * const videoDirPath = await videoDir(); + * ``` + * + * @remarks **Breaking change on Android in `tauri` 2.12.0:** `videoDir()` and the + * `$VIDEO` path variable used to resolve to external *cache* storage + * (`.../cache`) and now resolve to the app-specific Movies directory + * (`.../files/Movies`). Files written to the old location are not found at the new + * one, so migrate them or update your path assumptions. + * + * @since 1.0.0 + */ +async function videoDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Video + }) +} + +/** + * Returns the path to the suggested directory for your app's log files. + * + * #### Platform-specific + * + * - **Linux:** Resolves to `${configDir}/${bundleIdentifier}/logs`. + * - **macOS:** Resolves to `${homeDir}/Library/Logs/{bundleIdentifier}` + * - **Windows:** Resolves to `${configDir}/${bundleIdentifier}/logs`. + * @example + * ```typescript + * import { appLogDir } from '@tauri-apps/api/path'; + * const appLogDirPath = await appLogDir(); + * ``` + * + * @since 1.2.0 + */ +async function appLogDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.AppLog + }) +} + +/** + * Returns a temporary directory. + * @example + * ```typescript + * import { tempDir } from '@tauri-apps/api/path'; + * const temp = await tempDir(); + * ``` + * + * @since 2.0.0 + */ +async function tempDir(): Promise { + return invoke('plugin:path|resolve_directory', { + directory: BaseDirectory.Temp + }) +} + +/** + * Returns the platform-specific path segment separator: + * - `\` on Windows + * - `/` on POSIX + * + * @since 2.0.0 + */ +function sep(): string { + return window.__TAURI_INTERNALS__.plugins.path.sep +} + +/** + * Returns the platform-specific path segment delimiter: + * - `;` on Windows + * - `:` on POSIX + * + * @since 2.0.0 + */ +function delimiter(): string { + return window.__TAURI_INTERNALS__.plugins.path.delimiter +} +/** + * Resolves a sequence of `paths` or `path` segments into an absolute path. + * @example + * ```typescript + * import { resolve, appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * const path = await resolve(appDataDirPath, '..', 'users', 'tauri', 'avatar.png'); + * ``` + * + * @since 1.0.0 + */ +async function resolve(...paths: string[]): Promise { + return invoke('plugin:path|resolve', { paths }) +} + +/** + * Normalizes the given `path`, resolving `'..'` and `'.'` segments and resolve symbolic links. + * @example + * ```typescript + * import { normalize, appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * const path = await normalize(`${appDataDirPath}/../users/tauri/avatar.png`); + * ``` + * + * @since 1.0.0 + */ +async function normalize(path: string): Promise { + return invoke('plugin:path|normalize', { path }) +} + +/** + * Joins all given `path` segments together using the platform-specific separator as a delimiter, then normalizes the resulting path. + * @example + * ```typescript + * import { join, appDataDir } from '@tauri-apps/api/path'; + * const appDataDirPath = await appDataDir(); + * const path = await join(appDataDirPath, 'users', 'tauri', 'avatar.png'); + * ``` + * + * @since 1.0.0 + */ +async function join(...paths: string[]): Promise { + return invoke('plugin:path|join', { paths }) +} + +/** + * Returns the parent directory of a given `path`. Trailing directory separators are ignored. + * @example + * ```typescript + * import { dirname } from '@tauri-apps/api/path'; + * const dir = await dirname('/path/to/somedir/'); + * assert(dir === '/path/to'); + * ``` + * + * @since 1.0.0 + */ +async function dirname(path: string): Promise { + return invoke('plugin:path|dirname', { path }) +} + +/** + * Returns the extension of the `path`. + * @example + * ```typescript + * import { extname } from '@tauri-apps/api/path'; + * const ext = await extname('/path/to/file.html'); + * assert(ext === 'html'); + * ``` + * + * @since 1.0.0 + */ +async function extname(path: string): Promise { + return invoke('plugin:path|extname', { path }) +} + +/** + * Returns the last portion of a `path`. Trailing directory separators are ignored. + * @example + * ```typescript + * import { basename } from '@tauri-apps/api/path'; + * const base = await basename('path/to/app.conf'); + * assert(base === 'app.conf'); + * ``` + * @param ext An optional file extension to be removed from the returned path. + * + * @since 1.0.0 + */ +async function basename(path: string, ext?: string): Promise { + return invoke('plugin:path|basename', { path, ext }) +} + +/** + * Returns whether the path is absolute or not. + * @example + * ```typescript + * import { isAbsolute } from '@tauri-apps/api/path'; + * assert(await isAbsolute('/home/tauri')); + * ``` + * + * @since 1.0.0 + */ +async function isAbsolute(path: string): Promise { + return invoke('plugin:path|is_absolute', { path }) +} + +export { + BaseDirectory, + appConfigDir, + appDataDir, + appLocalDataDir, + appCacheDir, + appLogDir, + audioDir, + cacheDir, + configDir, + dataDir, + desktopDir, + documentDir, + downloadDir, + executableDir, + fontDir, + homeDir, + localDataDir, + pictureDir, + publicDir, + resourceDir, + resolveResource, + runtimeDir, + templateDir, + videoDir, + sep, + delimiter, + resolve, + normalize, + join, + dirname, + extname, + basename, + isAbsolute, + tempDir +} diff --git a/packages/api/src/tray.ts b/packages/api/src/tray.ts new file mode 100644 index 0000000..302fbe5 --- /dev/null +++ b/packages/api/src/tray.ts @@ -0,0 +1,502 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Create and manipulate system tray (menu bar / notification area) icons. + * + * Tray icons are created with {@linkcode TrayIcon.new} and live on the Rust side, + * the frontend only holds a handle to them. + * + * This package is also accessible with `window.__TAURI__.tray` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * + * @remarks All commands used by this module are part of the `core:tray:default` + * permission set, which is enabled by default, so no extra capability + * configuration is needed. Loading an icon from a path or from bytes additionally + * requires the `image-png` / `image-ico` Cargo features of the `tauri` crate. + * + * @module + */ + +import type { Menu, Submenu } from './menu' +import { Channel, invoke, Resource } from './core' +import { type JsImage, transformImage } from './image' +import { PhysicalPosition, PhysicalSize } from './dpi' + +/** Whether the mouse button was pressed (`Down`) or released (`Up`). */ +export type MouseButtonState = 'Up' | 'Down' + +/** The mouse button that triggered a tray icon event. */ +export type MouseButton = 'Left' | 'Right' | 'Middle' + +/** + * The kind of a {@linkcode TrayIconEvent}. + * + * - `Click`: a mouse button was pressed or released over the icon. + * - `DoubleClick`: the icon was double clicked. + * - `Enter`: the cursor entered the icon area. + * - `Move`: the cursor moved inside the icon area. + * - `Leave`: the cursor left the icon area. + */ +export type TrayIconEventType = + | 'Click' + | 'DoubleClick' + | 'Enter' + | 'Move' + | 'Leave' + +/** The fields shared by every {@linkcode TrayIconEvent}. */ +export type TrayIconEventBase = { + /** The tray icon event type */ + type: T + /** Id of the tray icon which triggered this event. */ + id: string + /** Physical position of the click the triggered this event. */ + position: PhysicalPosition + /** Position and size of the tray icon. */ + rect: { + position: PhysicalPosition + size: PhysicalSize + } +} + +/** The extra fields of the `Click` and `DoubleClick` {@linkcode TrayIconEvent} variants. */ +export type TrayIconClickEvent = { + /** Mouse button that triggered this event. */ + button: MouseButton + /** Mouse button state when this event was triggered. */ + buttonState: MouseButtonState +} + +/** + * Describes a tray icon event. + * + * #### Platform-specific: + * + * - **Linux**: Unsupported. The event is not emitted even though the icon is shown, + * the icon will still show a context menu on right click. + */ +export type TrayIconEvent = + | (TrayIconEventBase<'Click'> & TrayIconClickEvent) + | (TrayIconEventBase<'DoubleClick'> & Omit) + | TrayIconEventBase<'Enter'> + | TrayIconEventBase<'Move'> + | TrayIconEventBase<'Leave'> + +type RustTrayIconEvent = Omit & { + rect: { + position: { + Physical: { x: number; y: number } + } + size: { + Physical: { width: number; height: number } + } + } +} + +/** {@link TrayIcon.new|`TrayIcon`} creation options */ +export interface TrayIconOptions { + /** The tray icon id. If undefined, a random one will be assigned */ + id?: string + /** The tray icon menu */ + menu?: Menu | Submenu + /** + * The tray icon which could be icon bytes or path to the icon file. + * + * 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"] } + * ``` + */ + icon?: JsImage + /** The tray icon tooltip */ + tooltip?: string + /** + * The tray title + * + * #### Platform-specific + * + * - **Linux:** The title will not be shown unless there is an icon + * as well. The title is useful for numerical and other frequently + * updated information. In general, it shouldn't be shown unless a + * user requests it as it can take up a significant amount of space + * on the user's panel. This may not be shown in all visualizations. + * - **Windows:** Unsupported. + */ + title?: string + /** + * The tray icon temp dir path. **Linux only**. + * + * On Linux, we need to write the icon to the disk and usually it will + * be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + */ + tempDirPath?: string + /** + * Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. + */ + iconAsTemplate?: boolean + /** + * Whether to show the tray menu on left click or not, default is `true`. + * + * #### Platform-specific: + * + * - **Linux**: Unsupported. + * + * @deprecated use {@linkcode TrayIconOptions.showMenuOnLeftClick} instead. + */ + menuOnLeftClick?: boolean + /** + * Whether to show the tray menu on left click or not, default is `true`. + * + * #### Platform-specific: + * + * - **Linux**: Unsupported. + * + * @since 2.2.0 + */ + showMenuOnLeftClick?: boolean + /** A handler for an event on the tray icon. */ + action?: (event: TrayIconEvent) => void +} + +/** + * Tray icon class and associated methods. This type constructor is private, + * instead, you should use the static method {@linkcode TrayIcon.new}. + * + * #### Warning + * + * Unlike Rust, javascript does not have any way to run cleanup code + * when an object is being removed by garbage collection, but this tray icon + * will be cleaned up when the tauri app exists, however if you want to cleanup + * this object early, you need to call {@linkcode TrayIcon.close}. + * + * @example + * ```ts + * import { TrayIcon } from '@tauri-apps/api/tray'; + * const tray = await TrayIcon.new({ tooltip: 'awesome tray tooltip' }); + * await tray.setTooltip('new tooltip'); + * ``` + */ +export class TrayIcon extends Resource { + /** The id associated with this tray icon. */ + public id: string + + private constructor(rid: number, id: string) { + super(rid) + this.id = id + } + + /** + * Gets a tray icon using the provided id. + * + * Use it to get a handle from the frontend to a tray icon that was created on + * the Rust side with an explicit id. + * + * @example + * ```typescript + * import { TrayIcon } from '@tauri-apps/api/tray'; + * const tray = await TrayIcon.getById('main-tray'); + * await tray?.setTooltip('still here'); + * ``` + * + * @returns The tray icon, or `null` if no tray icon with that id exists. + */ + static async getById(id: string): Promise { + return invoke('plugin:tray|get_by_id', { id }).then((rid) => + rid ? new TrayIcon(rid, id) : null + ) + } + + /** + * Removes a tray icon using the provided id from tauri's internal state. + * + * Note that this may cause the tray icon to disappear + * if it wasn't cloned somewhere else or referenced by JS. + * + * @example + * ```typescript + * import { TrayIcon } from '@tauri-apps/api/tray'; + * await TrayIcon.removeById('main-tray'); + * ``` + */ + static async removeById(id: string): Promise { + return invoke('plugin:tray|remove_by_id', { id }) + } + + /** + * Creates a new {@linkcode TrayIcon} + * + * #### Platform-specific: + * + * - **Linux:** Sometimes the icon won't be visible unless a menu is set. + * Setting an empty {@linkcode Menu} is enough. + * + * @example + * ```typescript + * import { TrayIcon } from '@tauri-apps/api/tray'; + * import { Menu } from '@tauri-apps/api/menu'; + * import { defaultWindowIcon } from '@tauri-apps/api/app'; + * + * const menu = await Menu.new({ + * items: [{ id: 'quit', text: 'Quit', action: () => console.log('quit') }] + * }); + * + * const tray = await TrayIcon.new({ + * id: 'main-tray', + * icon: (await defaultWindowIcon()) ?? undefined, + * menu, + * tooltip: 'My app', + * action: (event) => { + * if (event.type === 'Click') { + * console.log('tray clicked with', event.button); + * } + * } + * }); + * ``` + */ + static async new(options?: TrayIconOptions): Promise { + if (options?.menu) { + // @ts-expect-error we only need the rid and kind + options.menu = [options.menu.rid, options.menu.kind] + } + if (options?.icon) { + options.icon = transformImage(options.icon) + } + + const handler = new Channel() + if (options?.action) { + const action = options.action + handler.onmessage = (e) => action(mapEvent(e)) + delete options.action + } + + return invoke<[number, string]>('plugin:tray|new', { + options: options ?? {}, + handler + }).then(([rid, id]) => new TrayIcon(rid, id)) + } + + /** + * Sets a new tray icon. If `null` is provided, it will remove the icon. + * + * 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"] } + * ``` + * + * @example + * ```typescript + * import { Image } from '@tauri-apps/api/image'; + * await tray.setIcon(await Image.fromPath('icons/active.png')); + * // remove the icon + * await tray.setIcon(null); + * ``` + */ + async setIcon(icon: JsImage | null): Promise { + let trayIcon = null + if (icon) { + trayIcon = transformImage(icon) + } + return invoke('plugin:tray|set_icon', { rid: this.rid, icon: trayIcon }) + } + + /** + * Sets a new tray menu. + * + * #### Platform-specific: + * + * - **Linux**: once a menu is set it cannot be removed so `null` has no effect + * + * @example + * ```typescript + * import { Menu } from '@tauri-apps/api/menu'; + * const menu = await Menu.new({ items: [{ id: 'quit', text: 'Quit' }] }); + * await tray.setMenu(menu); + * ``` + */ + async setMenu(menu: Menu | Submenu | null): Promise { + if (menu) { + // @ts-expect-error we only need the rid and kind + menu = [menu.rid, menu.kind] + } + return invoke('plugin:tray|set_menu', { rid: this.rid, menu }) + } + + /** + * Sets the tooltip for this tray icon, shown when the cursor hovers it. + * + * #### Platform-specific: + * + * - **Linux:** Unsupported + * + * @example + * ```typescript + * await tray.setTooltip('3 unread messages'); + * ``` + */ + async setTooltip(tooltip: string | null): Promise { + return invoke('plugin:tray|set_tooltip', { rid: this.rid, tooltip }) + } + + /** + * Sets the title shown next to this tray icon. + * + * @example + * ```typescript + * await tray.setTitle('42'); + * ``` + * + * #### Platform-specific: + * + * - **Linux:** The title will not be shown unless there is an icon + * as well. The title is useful for numerical and other frequently + * updated information. In general, it shouldn't be shown unless a + * user requests it as it can take up a significant amount of space + * on the user's panel. This may not be shown in all visualizations. + * - **Windows:** Unsupported + */ + async setTitle(title: string | null): Promise { + return invoke('plugin:tray|set_title', { rid: this.rid, title }) + } + + /** + * Show or hide this tray icon. + * + * @example + * ```typescript + * await tray.setVisible(false); + * ``` + */ + async setVisible(visible: boolean): Promise { + return invoke('plugin:tray|set_visible', { rid: this.rid, visible }) + } + + /** + * Sets the tray icon temp dir path. **Linux only**. + * + * On Linux, we need to write the icon to the disk and usually it will + * be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + * + * @example + * ```typescript + * import { appCacheDir } from '@tauri-apps/api/path'; + * await tray.setTempDirPath(await appCacheDir()); + * ``` + */ + async setTempDirPath(path: string | null): Promise { + return invoke('plugin:tray|set_temp_dir_path', { rid: this.rid, path }) + } + + /** + * Sets the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only** + * + * A template image is recolored by the system so it matches the menu bar + * appearance in light and dark mode. + * + * @example + * ```typescript + * await tray.setIconAsTemplate(true); + * ``` + */ + async setIconAsTemplate(asTemplate: boolean): Promise { + return invoke('plugin:tray|set_icon_as_template', { + rid: this.rid, + asTemplate + }) + } + + /** + * 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"] } + * ``` + * + * Prefer this over calling {@linkcode TrayIcon.setIcon} and + * {@linkcode TrayIcon.setIconAsTemplate} in sequence, which can briefly show the + * new icon with the previous template setting. + * + * @example + * ```typescript + * import { Image } from '@tauri-apps/api/image'; + * await tray.setIconWithAsTemplate(await Image.fromPath('icons/active.png'), true); + * ``` + */ + async setIconWithAsTemplate( + icon: JsImage | 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. + * + * #### Platform-specific: + * + * - **Linux**: Unsupported. + * + * @deprecated use {@linkcode TrayIcon.setShowMenuOnLeftClick} instead. + * + * @example + * ```typescript + * await tray.setShowMenuOnLeftClick(false); + * ``` + */ + async setMenuOnLeftClick(onLeft: boolean): Promise { + return invoke('plugin:tray|set_show_menu_on_left_click', { + rid: this.rid, + onLeft + }) + } + + /** + * Disable or enable showing the tray menu on left click. + * + * #### Platform-specific: + * + * - **Linux**: Unsupported. + * + * Disable it to handle left clicks yourself through the + * {@linkcode TrayIconOptions.action} handler while still showing the menu on + * right click. + * + * @example + * ```typescript + * await tray.setShowMenuOnLeftClick(false); + * ``` + * + * @since 2.2.0 + */ + async setShowMenuOnLeftClick(onLeft: boolean): Promise { + return invoke('plugin:tray|set_show_menu_on_left_click', { + rid: this.rid, + onLeft + }) + } +} + +function mapEvent(e: RustTrayIconEvent): TrayIconEvent { + const out = e as unknown as TrayIconEvent + + out.position = new PhysicalPosition(e.position) + out.rect.position = new PhysicalPosition(e.rect.position) + out.rect.size = new PhysicalSize(e.rect.size) + + return out +} diff --git a/packages/api/src/webview.ts b/packages/api/src/webview.ts new file mode 100644 index 0000000..a09e747 --- /dev/null +++ b/packages/api/src/webview.ts @@ -0,0 +1,1051 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Provides APIs to create webviews, communicate with other webviews and manipulate the current webview. + * + * #### Webview events + * + * Events can be listened to using {@link Webview.listen}: + * ```typescript + * import { getCurrentWebview } from "@tauri-apps/api/webview"; + * getCurrentWebview().listen("my-webview-event", ({ event, payload }) => { }); + * ``` + * + * #### The `unstable` Cargo feature + * + * Creating *child webviews* — several webviews inside one window, via + * {@link Webview | `new Webview(...)`} — is an unstable API. It requires the + * `unstable` feature of the `tauri` crate: + * + * ```toml + * [dependencies] + * tauri = { version = "2", features = ["unstable"] } + * ``` + * + * Without it the `plugin:webview|create_webview` command returns an + * "unstable feature not supported" error. Everything else in this module — + * {@link getCurrentWebview}, {@link getAllWebviews} and the methods of an existing + * webview — works without the feature, and so does + * {@link WebviewWindow | `new WebviewWindow(...)`}, which creates a window with a + * single webview. + * + * This package is also accessible with `window.__TAURI__.webview` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * + * @remarks `core:webview:default` only enables the getters + * (`allow-get-all-webviews`, `allow-webview-position`, `allow-webview-size` and + * `allow-internal-toggle-devtools`). Every other method documents the permission + * it needs, which you must add to a capability yourself. + * + * @module + */ + +import { PhysicalPosition, PhysicalSize } from './dpi' +import type { LogicalPosition, LogicalSize } from './dpi' +import { Position, Size } from './dpi' +import type { EventName, EventCallback, UnlistenFn } from './event' +import { + TauriEvent, + // imported for documentation purposes + type EventTarget, + emit, + emitTo, + listen, + once +} from './event' +import { invoke } from './core' +import { + BackgroundThrottlingPolicy, + ScrollBarStyle, + Color, + Window, + getCurrentWindow +} from './window' +import { WebviewWindow } from './webviewWindow' + +/** + * The payload of a drag and drop event, see {@link Webview.onDragDropEvent}. + * + * - `enter`: the user dragged files over the webview; `paths` holds the dragged files. + * - `over`: the cursor moved while dragging; only `position` changes. + * - `drop`: the files were dropped; `paths` holds the dropped files. + * - `leave`: the drag left the webview or was cancelled. + * + * Positions are in physical pixels, relative to the webview's top-left corner. + */ +type DragDropEvent = + | { type: 'enter'; paths: string[]; position: PhysicalPosition } + | { type: 'over'; position: PhysicalPosition } + | { type: 'drop'; paths: string[]; position: PhysicalPosition } + | { type: 'leave' } + +/** + * Get an instance of `Webview` for the current webview. + * + * @since 2.0.0 + */ +function getCurrentWebview(): Webview { + return new Webview( + getCurrentWindow(), + window.__TAURI_INTERNALS__.metadata.currentWebview.label, + { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + } + ) +} + +/** + * Gets a list of instances of `Webview` for all available webviews. + * + * @remarks Uses the `core:webview:allow-get-all-webviews` permission, which is part + * of `core:webview:default`. + * + * @since 2.0.0 + */ +async function getAllWebviews(): Promise { + return invoke>( + 'plugin:webview|get_all_webviews' + ).then((webviews) => + webviews.map( + (w) => + new Webview( + new Window(w.windowLabel, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }), + w.label, + { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + } + ) + ) + ) +} + +/** @ignore */ +// events that are emitted right here instead of by the created webview +const localTauriEvents = ['tauri://created', 'tauri://error'] +/** @ignore */ +export type WebviewLabel = string + +/** + * Create new webview or get a handle to an existing one. + * + * Webviews are identified by a *label* a unique identifier that can be used to reference it later. + * It may only contain alphanumeric characters `a-zA-Z` plus the following special characters `-`, `/`, `:` and `_`. + * + * #### Requires the `unstable` Cargo feature + * + * Creating a webview with `new Webview(...)` adds a *child webview* to an existing + * window, which is an unstable API gated behind the `unstable` feature of the + * `tauri` crate: + * + * ```toml + * [dependencies] + * tauri = { version = "2", features = ["unstable"] } + * ``` + * + * Without it the call rejects with an "unstable feature not supported" error. + * Getting a handle to an existing webview (`Webview.getByLabel`, + * {@link getCurrentWebview}, {@link getAllWebviews}) and every method on this class + * work without the feature. To create a window that hosts a single webview, use + * {@link WebviewWindow} instead, which is stable. + * + * @example + * ```typescript + * import { Window } from "@tauri-apps/api/window" + * import { Webview } from "@tauri-apps/api/webview" + * + * const appWindow = new Window('uniqueLabel'); + * + * appWindow.once('tauri://created', async function () { + * // `new Webview` Should be called after the window is successfully created, + * // or webview may not be attached to the window since window is not created yet. + * + * // loading embedded asset: + * const webview = new Webview(appWindow, 'theUniqueLabel', { + * url: 'path/to/page.html', + * + * // create a webview with specific logical position and size + * x: 0, + * y: 0, + * width: 800, + * height: 600, + * }); + * // alternatively, load a remote URL: + * const webview = new Webview(appWindow, 'theUniqueLabel', { + * url: 'https://github.com/tauri-apps/tauri', + * + * // create a webview with specific logical position and size + * x: 0, + * y: 0, + * width: 800, + * height: 600, + * }); + * + * webview.once('tauri://created', function () { + * // webview successfully created + * }); + * webview.once('tauri://error', function (e) { + * // an error happened creating the webview + * }); + * + * + * // emit an event to the backend + * await webview.emit("some-event", "data"); + * // listen to an event from the backend + * const unlisten = await webview.listen("event-name", e => { }); + * unlisten(); + * }); + * ``` + * + * @since 2.0.0 + */ +class Webview { + /** The webview label. It is a unique identifier for the webview, can be used to reference it later. */ + label: WebviewLabel + /** The window hosting this webview. */ + window: Window + /** Local event listeners. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + listeners: Record>> + + /** + * Creates a new Webview. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window' + * import { Webview } from '@tauri-apps/api/webview' + * const appWindow = new Window('my-label') + * + * appWindow.once('tauri://created', async function() { + * const webview = new Webview(appWindow, 'my-label', { + * url: 'https://github.com/tauri-apps/tauri', + * + * // create a webview with specific logical position and size + * x: 0, + * y: 0, + * width: 800, + * height: 600, + * }); + * + * webview.once('tauri://created', function () { + * // webview successfully created + * }); + * webview.once('tauri://error', function (e) { + * // an error happened creating the webview + * }); + * }); + * ``` + * + * @param window the window to add this webview to. + * @param label The unique webview label. Must be alphanumeric: `a-zA-Z-/:_`. + * @param options The webview configuration, see {@link WebviewOptions}. + * @returns The {@link Webview} instance to communicate with the webview. + * + * @remarks Requires the `unstable` Cargo feature on the `tauri` crate + * (`tauri = { version = "2", features = ["unstable"] }`), otherwise the call + * rejects with an "unstable feature not supported" error. It also requires the + * `core:webview:allow-create-webview` permission, which is not included in + * `core:webview:default`. + */ + constructor(window: Window, label: WebviewLabel, options: WebviewOptions) { + this.window = window + this.label = label + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.listeners = Object.create(null) + + // @ts-expect-error `skip` is not a public API so it is not defined in WebviewOptions + if (!options?.skip) { + invoke('plugin:webview|create_webview', { + windowLabel: window.label, + options: { + ...options, + label + } + }) + .then(async () => this.emit('tauri://created')) + .catch(async (e: string) => this.emit('tauri://error', e)) + } + } + + /** + * Gets the Webview for the webview associated with the given label. + * @example + * ```typescript + * import { Webview } from '@tauri-apps/api/webview'; + * const mainWebview = Webview.getByLabel('main'); + * ``` + * + * @param label The webview label. + * @returns The Webview instance to communicate with the webview or null if the webview doesn't exist. + */ + static async getByLabel(label: string): Promise { + return (await getAllWebviews()).find((w) => w.label === label) ?? null + } + + /** + * Get an instance of `Webview` for the current webview. + */ + static getCurrent(): Webview { + return getCurrentWebview() + } + + /** + * Gets a list of instances of `Webview` for all available webviews. + */ + static async getAll(): Promise { + return getAllWebviews() + } + + /** + * Listen to an emitted event on this webview. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const unlisten = await getCurrentWebview().listen('state-changed', (event) => { + * console.log(`Got error: ${payload}`); + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listener is removed automatically when this webview is destroyed, + * so you do not need to unlisten just because the webview is closing. Do call the + * returned function when the listener's own scope ends, e.g. on page navigation + * or when a component unmounts. + */ + async listen( + event: EventName, + handler: EventCallback + ): Promise { + if (this._handleTauriEvent(event, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event] + listeners.splice(listeners.indexOf(handler), 1) + } + } + return listen(event, handler, { + target: { kind: 'Webview', label: this.label } + }) + } + + /** + * Listen to an emitted event on this webview only once. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const unlisten = await getCurrentWebview().once('initialized', (event) => { + * console.log(`Webview initialized!`); + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listener removes itself after the first event and is also removed + * automatically when this webview is destroyed. Do call the returned function if + * the listener's own scope ends before the event arrives. + */ + async once( + event: EventName, + handler: EventCallback + ): Promise { + if (this._handleTauriEvent(event, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event] + listeners.splice(listeners.indexOf(handler), 1) + } + } + return once(event, handler, { + target: { kind: 'Webview', label: this.label } + }) + } + + /** + * Emits an event to all {@link EventTarget|targets}. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().emit('webview-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + async emit(event: string, payload?: T): Promise { + if (localTauriEvents.includes(event)) { + // eslint-disable-next-line + for (const handler of this.listeners[event] || []) { + handler({ + event, + id: -1, + payload + }) + } + return + } + return emit(event, payload) + } + + /** + * Emits an event to all {@link EventTarget|targets} matching the given target. + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().emitTo('main', 'webview-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object. + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + async emitTo( + target: string | EventTarget, + event: string, + payload?: T + ): Promise { + if (localTauriEvents.includes(event)) { + // eslint-disable-next-line + for (const handler of this.listeners[event] || []) { + handler({ + event, + id: -1, + payload + }) + } + return + } + return emitTo(target, event, payload) + } + + /** @ignore */ + _handleTauriEvent(event: string, handler: EventCallback): boolean { + if (localTauriEvents.includes(event)) { + if (!(event in this.listeners)) { + // eslint-disable-next-line security/detect-object-injection + this.listeners[event] = [handler] + } else { + // eslint-disable-next-line security/detect-object-injection + this.listeners[event].push(handler) + } + return true + } + return false + } + + // Getters + /** + * The position of the top-left hand corner of the webview's client area relative to the top-left hand corner of the desktop. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const position = await getCurrentWebview().position(); + * ``` + * + * @returns The webview's position, in physical pixels. + * + * @remarks Uses the `core:webview:allow-webview-position` permission, which is + * part of `core:webview:default`. + */ + async position(): Promise { + return invoke<{ x: number; y: number }>('plugin:webview|webview_position', { + label: this.label + }).then((p) => new PhysicalPosition(p)) + } + + /** + * The physical size of the webview's client area. + * The client area is the content of the webview, excluding the title bar and borders. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * const size = await getCurrentWebview().size(); + * ``` + * + * @returns The webview's size, in physical pixels. + * + * @remarks Uses the `core:webview:allow-webview-size` permission, which is part + * of `core:webview:default`. + */ + async size(): Promise { + return invoke<{ width: number; height: number }>( + 'plugin:webview|webview_size', + { + label: this.label + } + ).then((s) => new PhysicalSize(s)) + } + + // Setters + + /** + * Closes the webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().close(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:webview:allow-webview-close` permission (not + * included in `core:webview:default`). + */ + async close(): Promise { + return invoke('plugin:webview|webview_close', { + label: this.label + }) + } + + /** + * Resizes the webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * import { LogicalSize } from '@tauri-apps/api/dpi'; + * await getCurrentWebview().setSize(new LogicalSize(600, 500)); + * ``` + * + * @param size The logical or physical size. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:webview:allow-set-webview-size` permission (not + * included in `core:webview:default`). + */ + async setSize(size: LogicalSize | PhysicalSize | Size): Promise { + return invoke('plugin:webview|set_webview_size', { + label: this.label, + value: size instanceof Size ? size : new Size(size) + }) + } + + /** + * Sets the webview position. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * import { LogicalPosition } from '@tauri-apps/api/dpi'; + * await getCurrentWebview().setPosition(new LogicalPosition(600, 500)); + * ``` + * + * @param position The new position, in logical or physical pixels, relative to + * the top-left corner of the hosting window. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:webview:allow-set-webview-position` permission (not + * included in `core:webview:default`). + */ + async setPosition( + position: LogicalPosition | PhysicalPosition | Position + ): Promise { + return invoke('plugin:webview|set_webview_position', { + label: this.label, + value: position instanceof Position ? position : new Position(position) + }) + } + + /** + * Bring the webview to front and focus. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setFocus(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:webview:allow-set-webview-focus` permission (not + * included in `core:webview:default`). + */ + async setFocus(): Promise { + return invoke('plugin:webview|set_webview_focus', { + label: this.label + }) + } + + /** + * Sets whether the webview should automatically grow and shrink its size and position when the parent window resizes. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setAutoResize(true); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:webview:allow-set-webview-auto-resize` permission + * (not included in `core:webview:default`). + */ + async setAutoResize(autoResize: boolean): Promise { + return invoke('plugin:webview|set_webview_auto_resize', { + label: this.label, + value: autoResize + }) + } + + /** + * Hide the webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().hide(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:webview:allow-webview-hide` permission (not + * included in `core:webview:default`). + */ + async hide(): Promise { + return invoke('plugin:webview|webview_hide', { + label: this.label + }) + } + + /** + * Show the webview. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().show(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:webview:allow-webview-show` permission (not + * included in `core:webview:default`). + */ + async show(): Promise { + return invoke('plugin:webview|webview_show', { + label: this.label + }) + } + + /** + * Set webview zoom level. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setZoom(1.5); + * ``` + * + * @param scaleFactor The zoom level, where `1` is 100%. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:webview:allow-set-webview-zoom` permission (not + * included in `core:webview:default`). + */ + async setZoom(scaleFactor: number): Promise { + return invoke('plugin:webview|set_webview_zoom', { + label: this.label, + value: scaleFactor + }) + } + + /** + * Moves this webview to the window with the given label. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().reparent('other-window'); + * ``` + * + * @param window The target window, or its label. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:webview:allow-reparent` permission (not included in + * `core:webview:default`). + */ + async reparent(window: Window | WebviewWindow | string): Promise { + return invoke('plugin:webview|reparent', { + label: this.label, + window: typeof window === 'string' ? window : window.label + }) + } + + /** + * Clears all browsing data for this webview: cookies, localStorage, IndexedDB, + * caches and any other data the webview stores for the loaded origins. + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().clearAllBrowsingData(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:webview:allow-clear-all-browsing-data` permission + * (not included in `core:webview:default`). + */ + async clearAllBrowsingData(): Promise { + return invoke('plugin:webview|clear_all_browsing_data') + } + + /** + * Specify the webview background color. + * + * #### Platform-specific: + * + * - **macOS / iOS**: Not implemented. + * - **Windows**: + * - On Windows 7, transparency is not supported and the alpha value will be ignored. + * - On Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` + * + * @example + * ```typescript + * import { getCurrentWebview } from '@tauri-apps/api/webview'; + * await getCurrentWebview().setBackgroundColor('#2f2f2f'); + * // or with an RGBA tuple, or `null` to restore the default background + * await getCurrentWebview().setBackgroundColor([47, 47, 47, 255]); + * ``` + * + * @param color The new background color, or `null` to reset it. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:webview:allow-set-webview-background-color` + * permission (not included in `core:webview:default`). + * + * @since 2.1.0 + */ + async setBackgroundColor(color: Color | null): Promise { + return invoke('plugin:webview|set_webview_background_color', { + label: this.label, + value: color + }) + } + + // Listeners + + /** + * Listen to a file drop event. + * The listener is triggered when the user hovers the selected files on the webview, + * drops the files or cancels the operation. + * + * @example + * ```typescript + * import { getCurrentWebview } from "@tauri-apps/api/webview"; + * const unlisten = await getCurrentWebview().onDragDropEvent((event) => { + * if (event.payload.type === 'over') { + * console.log('User hovering', event.payload.position); + * } else if (event.payload.type === 'drop') { + * console.log('User dropped', event.payload.paths); + * } else { + * console.log('File drop cancelled'); + * } + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * When the debugger panel is open, the drop position of this event may be inaccurate due to a known limitation. + * To retrieve the correct drop position, please detach the debugger. + * + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listeners are removed automatically when this webview is + * destroyed. Do call the returned function when the listener's own scope ends, + * e.g. on page navigation or when a component unmounts. + */ + async onDragDropEvent( + handler: EventCallback + ): Promise { + type DragPayload = { paths: string[]; position: PhysicalPosition } + + const unlistenDragEnter = await this.listen( + TauriEvent.DRAG_ENTER, + (event) => { + handler({ + ...event, + payload: { + type: 'enter', + paths: event.payload.paths, + position: new PhysicalPosition(event.payload.position) + } + }) + } + ) + + const unlistenDragOver = await this.listen( + TauriEvent.DRAG_OVER, + (event) => { + handler({ + ...event, + payload: { + type: 'over', + position: new PhysicalPosition(event.payload.position) + } + }) + } + ) + + const unlistenDragDrop = await this.listen( + TauriEvent.DRAG_DROP, + (event) => { + handler({ + ...event, + payload: { + type: 'drop', + paths: event.payload.paths, + position: new PhysicalPosition(event.payload.position) + } + }) + } + ) + + const unlistenDragLeave = await this.listen( + TauriEvent.DRAG_LEAVE, + (event) => { + handler({ ...event, payload: { type: 'leave' } }) + } + ) + + return () => { + unlistenDragEnter() + unlistenDragDrop() + unlistenDragOver() + unlistenDragLeave() + } + } +} + +/** + * Configuration for the webview to create. + * + * @since 2.0.0 + */ +interface WebviewOptions { + /** + * Remote URL or local file path to open. + * + * - URL such as `https://github.com/tauri-apps` is opened directly on a Tauri webview. + * - data: URL such as `data:text/html,...` is only supported with the `webview-data-url` Cargo feature for the `tauri` dependency. + * - local file path or route such as `/path/to/page.html` or `/users` is appended to the application URL (the devServer URL on development, or `tauri://localhost/` and `https://tauri.localhost/` on production). + */ + url?: string + /** The initial horizontal position in logical pixels, relative to the window's top-left corner. */ + x: number + /** The initial vertical position in logical pixels, relative to the window's top-left corner. */ + y: number + /** The initial width in logical pixels. */ + width: number + /** The initial height in logical pixels. */ + height: number + /** + * Whether the webview is transparent or not. + * Note that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri.conf.json > app > macOSPrivateApi`. + * WARNING: Using private APIs on `macOS` prevents your application from being accepted to the `App Store`. + */ + transparent?: boolean + /** + * Whether the webview should have focus or not + * + * @since 2.1.0 + */ + focus?: boolean + /** + * Whether the drag and drop is enabled or not on the webview. By default it is enabled. + * + * Disabling it is required to use HTML5 drag and drop on the frontend on Windows. + */ + dragDropEnabled?: boolean + /** + * Whether clicking an inactive webview also clicks through to the webview on macOS. + */ + acceptFirstMouse?: boolean + /** + * The user agent for the webview. + */ + userAgent?: string + /** + * Whether or not the webview should be launched in incognito mode. + * + * #### Platform-specific + * + * - **Android:** Unsupported. + */ + incognito?: boolean + /** + * The proxy URL for the WebView for all network requests. + * + * Must be either a `http://` or a `socks5://` URL. + * + * #### Platform-specific + * + * - **macOS**: Requires the `macos-proxy` feature flag and only compiles for macOS 14+. + * */ + proxyUrl?: string + /** + * Whether page zooming by hotkeys is enabled + * + * #### Platform-specific: + * + * - **Windows**: Controls WebView2's [`IsZoomControlEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2settings?view=webview2-winrt-1.0.2420.47#iszoomcontrolenabled) setting. + * - **MacOS / Linux**: Injects a polyfill that zooms in and out with `ctrl/command` + `-/=`, + * 20% in each step, ranging from 20% to 1000%. Requires `webview:allow-set-webview-zoom` permission + * + * - **Android / iOS**: Unsupported. + */ + zoomHotkeysEnabled?: boolean + + /** + * Sets whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android. Defaults to `false`. + * + * #### Note + * + * Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `://localhost` protocols used on macOS and Linux. + * + * #### Warning + * + * Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access them. + * + * @since 2.1.0 + */ + useHttpsScheme?: boolean + /** + * Whether web inspector, which is usually called browser devtools, is enabled or not. Enabled by default. + * + * This API works in **debug** builds, but requires `devtools` feature flag to enable it in **release** builds. + * + * #### Platform-specific + * + * - macOS: This will call private functions on **macOS**. + * - Android: Open `chrome://inspect/#devices` in Chrome to get the devtools window. Wry's `WebView` devtools API isn't supported on Android. + * - iOS: Open Safari > Develop > [Your Device Name] > [Your WebView] to get the devtools window. + * + * @since 2.1.0 + */ + devtools?: boolean + /** + * Set the window and webview background color. + * + * #### Platform-specific: + * + * - **macOS / iOS**: Not implemented. + * - **Windows**: + * - On Windows 7, alpha channel is ignored. + * - On Windows 8 and newer, if alpha channel is not `0`, it will be ignored. + * + * @since 2.1.0 + */ + backgroundColor?: Color + + /** Change the default background throttling behaviour. + * + * By default, browsers use a suspend policy that will throttle timers and even unload + * the whole tab (view) to free resources after roughly 5 minutes when a view became + * minimized or hidden. This will pause all tasks until the documents visibility state + * changes back from hidden to visible by bringing the view back to the foreground. + * + * ## Platform-specific + * + * - **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice. + * - **iOS**: Supported since version 17.0+. + * - **macOS**: Supported since version 14.0+. + * + * see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 + * + * @since 2.3.0 + */ + backgroundThrottling?: BackgroundThrottlingPolicy + /** + * Whether we should disable JavaScript code execution on the webview or not. + */ + javascriptDisabled?: boolean + /** + * on macOS and iOS there is a link preview on long pressing links, this is enabled by default. + * see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview + */ + allowLinkPreview?: boolean + /** + * Allows disabling the input accessory view on iOS. + * + * The accessory view is the view that appears above the keyboard when a text input element is focused. + * It usually displays a view with "Done", "Next" buttons. + */ + disableInputAccessoryView?: boolean + /** + * Set a custom path for the webview's data directory (localStorage, cache, etc.) **relative to [`appDataDir()`]/${label}**. + * For security reasons, paths outside of that location can only be configured on the Rust side. + * + * #### Platform-specific: + * + * - **Windows**: WebViews with different values for settings like `additionalBrowserArgs`, `browserExtensionsEnabled` or `scrollBarStyle` must have different data directories. + * - **macOS / iOS**: Unsupported, use `dataStoreIdentifier` instead. + * - **Android**: Unsupported. + * + * @since 2.9.0 + */ + dataDirectory?: string + /** + * Initialize the WebView with a custom data store identifier. This can be seen as a replacement for `dataDirectory` which is unavailable in WKWebView. + * See https://developer.apple.com/documentation/webkit/wkwebsitedatastore/init(foridentifier:)?language=objc + * + * The array must contain 16 u8 numbers. + * + * #### Platform-specific: + * + * - **macOS / iOS**: Available on macOS >= 14 and iOS >= 17 + * - **Windows / Linux / Android**: Unsupported. + * + * @since 2.9.0 + */ + dataStoreIdentifier?: number[] + /** + * Specifies the native scrollbar style to use with the webview. + * CSS styles that modify the scrollbar are applied on top of the native appearance configured here. + * + * Defaults to `default`, which is the browser default. + * + * ## Platform-specific + * + * - **Windows**: + * - `fluentOverlay` requires WebView2 Runtime version 125.0.2535.41 or higher, and does nothing + * on older versions. + * - This option must be given the same value for all webviews. + * - **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 } + +export type { DragDropEvent, WebviewOptions, Color } diff --git a/packages/api/src/webviewWindow.ts b/packages/api/src/webviewWindow.ts new file mode 100644 index 0000000..29d1dcd --- /dev/null +++ b/packages/api/src/webviewWindow.ts @@ -0,0 +1,324 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Create and manipulate windows that host a single webview. + * + * {@linkcode WebviewWindow} is the type you want for ordinary multi-window apps: it + * mixes together the {@link Window} and {@link Webview} APIs, so one object exposes + * both the window methods (`setTitle`, `maximize`, `close`, ...) and the webview + * ones (`setZoom`, `clearAllBrowsingData`, ...). + * + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * + * const settings = new WebviewWindow('settings', { + * url: '/settings', + * title: 'Settings' + * }); + * settings.once('tauri://created', () => console.log('window created')); + * settings.once('tauri://error', (e) => console.error(e)); + * ``` + * + * Unlike {@link Webview | child webviews}, this does **not** require the `unstable` + * Cargo feature. + * + * This package is also accessible with `window.__TAURI__.webviewWindow` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * + * @remarks Creating a webview window requires the + * `core:webview:allow-create-webview-window` permission, which is not included in + * `core:webview:default`. The methods inherited from {@link Window} and + * {@link Webview} each document their own permission. + * + * @module + */ + +import { + getCurrentWebview, + Webview, + WebviewLabel, + WebviewOptions +} from './webview' +import type { WindowOptions } from './window' +import { Window } from './window' +import { listen, once } from './event' +import type { EventName, EventCallback, UnlistenFn } from './event' +import { invoke } from './core' +import type { Color, DragDropEvent } from './webview' + +/** + * Get an instance of `Webview` for the current webview window. + * + * @since 2.0.0 + */ +function getCurrentWebviewWindow(): WebviewWindow { + const webview = getCurrentWebview() + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + return new WebviewWindow(webview.label, { skip: true }) +} + +/** + * Gets a list of instances of `Webview` for all available webview windows. + * + * @remarks Uses the `core:window:allow-get-all-windows` permission, which is part + * of `core:window:default`. + * + * @since 2.0.0 + */ +async function getAllWebviewWindows(): Promise { + return invoke('plugin:window|get_all_windows').then((windows) => + windows.map( + (w) => + new WebviewWindow(w, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }) + ) + ) +} + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +interface WebviewWindow extends Webview, Window {} + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +class WebviewWindow { + label: string + /** Local event listeners. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + listeners: Record>> + + /** + * Creates a new {@link Window} hosting a {@link Webview}. + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow' + * const webview = new WebviewWindow('my-label', { + * url: 'https://github.com/tauri-apps/tauri' + * }); + * webview.once('tauri://created', function () { + * // webview successfully created + * }); + * webview.once('tauri://error', function (e) { + * // an error happened creating the webview + * }); + * ``` + * + * @param label The unique webview label. Must be alphanumeric: `a-zA-Z-/:_`. + * @param options The window and webview configuration, see {@link WindowOptions} + * and {@link WebviewOptions}. + * @returns The {@link WebviewWindow} instance to communicate with the window and webview. + * + * @remarks Requires the `core:webview:allow-create-webview-window` permission + * (not included in `core:webview:default`). + */ + constructor( + label: WebviewLabel, + options: Omit + & WindowOptions = {} + ) { + this.label = label + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.listeners = Object.create(null) + + // @ts-expect-error `skip` is not a public API so it is not defined in WebviewOptions + if (!options?.skip) { + invoke('plugin:webview|create_webview_window', { + options: { + ...options, + parent: + typeof options.parent === 'string' + ? options.parent + : options.parent?.label, + label + } + }) + .then(async () => this.emit('tauri://created')) + .catch(async (e: string) => this.emit('tauri://error', e)) + } + } + + /** + * Gets the Webview for the webview associated with the given label. + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * const mainWebview = WebviewWindow.getByLabel('main'); + * ``` + * + * @param label The webview label. + * @returns The Webview instance to communicate with the webview or null if the webview doesn't exist. + */ + static async getByLabel(label: string): Promise { + const webview = + (await getAllWebviewWindows()).find((w) => w.label === label) ?? null + if (webview) { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + return new WebviewWindow(webview.label, { skip: true }) + } + return null + } + + /** + * Get an instance of `Webview` for the current webview. + */ + static getCurrent(): WebviewWindow { + return getCurrentWebviewWindow() + } + + /** + * Gets a list of instances of `Webview` for all available webviews. + */ + static async getAll(): Promise { + return getAllWebviewWindows() + } + + /** + * Listen to an emitted event on this webview window. + * + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * const unlisten = await WebviewWindow.getCurrent().listen('state-changed', (event) => { + * console.log(`Got error: ${payload}`); + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listener is removed automatically when this webview window is + * destroyed, so you do not need to unlisten just because the window is closing. + * Do call the returned function when the listener's own scope ends, e.g. on page + * navigation or when a component unmounts. + */ + async listen( + event: EventName, + handler: EventCallback + ): Promise { + if (this._handleTauriEvent(event, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event] + listeners.splice(listeners.indexOf(handler), 1) + } + } + return listen(event, handler, { + target: { kind: 'WebviewWindow', label: this.label } + }) + } + + /** + * Listen to an emitted event on this webview window only once. + * + * @example + * ```typescript + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * const unlisten = await WebviewWindow.getCurrent().once('initialized', (event) => { + * console.log(`Webview initialized!`); + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listener removes itself after the first event and is also removed + * automatically when this webview window is destroyed. Do call the returned + * function if the listener's own scope ends before the event arrives. + */ + async once( + event: EventName, + handler: EventCallback + ): Promise { + if (this._handleTauriEvent(event, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event] + listeners.splice(listeners.indexOf(handler), 1) + } + } + return once(event, handler, { + target: { kind: 'WebviewWindow', label: this.label } + }) + } + + /** + * Set the window and webview background color. + * + * #### Platform-specific: + * + * - **Android / iOS:** Unsupported for the window layer. + * - **macOS / iOS**: Not implemented for the webview layer. + * - **Windows**: + * - alpha channel is ignored for the window layer. + * - On Windows 7, alpha channel is ignored for the webview layer. + * - On Windows 8 and newer, if alpha channel is not `0`, it will be ignored. + * + * @example + * ```typescript + * import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; + * await getCurrentWebviewWindow().setBackgroundColor('#2f2f2f'); + * ``` + * + * @param color The new background color. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires both the `core:window:allow-set-background-color` and + * `core:webview:allow-set-webview-background-color` permissions, neither of which + * is included in the respective default permission set. + * + * @since 2.1.0 + */ + async setBackgroundColor(color: Color): Promise { + return invoke('plugin:window|set_background_color', { + label: this.label, + value: color + }).then(() => { + return invoke('plugin:webview|set_webview_background_color', { + label: this.label, + value: color + }) + }) + } +} + +// Order matters, we use window APIs by default +applyMixins(WebviewWindow, [Window, Webview]) + +/** Extends a base class by other specified classes, without overriding existing properties */ +function applyMixins( + baseClass: { prototype: unknown }, + extendedClasses: unknown +): void { + ;(Array.isArray(extendedClasses) + ? extendedClasses + : [extendedClasses] + ).forEach((extendedClass: { prototype: unknown }) => { + Object.getOwnPropertyNames(extendedClass.prototype).forEach((name) => { + if ( + typeof baseClass.prototype === 'object' + && baseClass.prototype + && name in baseClass.prototype + ) + return + Object.defineProperty( + baseClass.prototype, + name, + // eslint-disable-next-line + Object.getOwnPropertyDescriptor(extendedClass.prototype, name) + ?? Object.create(null) + ) + }) + }) +} + +export { WebviewWindow, getCurrentWebviewWindow, getAllWebviewWindows } +export type { DragDropEvent, Color } diff --git a/packages/api/src/window.ts b/packages/api/src/window.ts new file mode 100644 index 0000000..834d623 --- /dev/null +++ b/packages/api/src/window.ts @@ -0,0 +1,3215 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/** + * Provides APIs to create windows, communicate with other windows and manipulate the current window. + * + * #### Window events + * + * Events can be listened to using {@link Window.listen}: + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * getCurrentWindow().listen("my-window-event", ({ event, payload }) => { }); + * ``` + * + * This package is also accessible with `window.__TAURI__.window` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`. + * + * @remarks `core:window:default` only enables the getters (sizes, positions, + * monitors, title, theme and the `is*` queries). Creating a window and every + * method that changes one needs its own permission, which each member documents, + * added to a capability in your app. + * + * @module + */ + +import { + LogicalPosition, + LogicalSize, + PhysicalPosition, + PhysicalSize, + Position, + Size +} from './dpi' +import type { Event, EventName, EventCallback, UnlistenFn } from './event' +import { + TauriEvent, + // imported for documentation purposes + type EventTarget, + emit, + emitTo, + listen, + once +} from './event' +import { invoke } from './core' +import { WebviewWindow } from './webviewWindow' +import type { DragDropEvent } from './webview' +import { type JsImage, transformImage } from './image' + +/** + * Allows you to retrieve information about a given monitor. + * + * @since 1.0.0 + */ +export interface Monitor { + /** Human-readable name of the monitor */ + name: string | null + /** + * The monitor's resolution in physical pixels. + * + * Use {@linkcode Monitor.scaleFactor} to convert to logical pixels: + * ```typescript + * const logicalSize = monitor.size.toLogical(monitor.scaleFactor); + * ``` + */ + size: PhysicalSize + /** + * the Top-left corner position of the monitor relative to the larger full screen area, in physical pixels. + * + * Note that window creation options such as `x`, `y`, `width` and `height` expect + * logical pixels, so convert with {@linkcode Monitor.scaleFactor} first: + * ```typescript + * import { currentMonitor } from '@tauri-apps/api/window'; + * import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; + * + * const monitor = await currentMonitor(); + * if (monitor) { + * const position = monitor.position.toLogical(monitor.scaleFactor); + * const webview = new WebviewWindow('my-label', { x: position.x, y: position.y }); + * } + * ``` + */ + position: PhysicalPosition + /** + * The monitor's work area (the monitor area excluding taskbars and docks) in physical pixels. + * + * Use {@linkcode Monitor.scaleFactor} to convert to logical pixels as shown in + * {@linkcode Monitor.position}. + */ + workArea: { + position: PhysicalPosition + size: PhysicalSize + } + /** + * The scale factor that can be used to map physical pixels to logical pixels, + * e.g. `monitor.position.toLogical(monitor.scaleFactor)`. + */ + scaleFactor: number +} + +/** The light or dark appearance of a window or of the system. */ +type Theme = 'light' | 'dark' + +/** + * The style of the window title bar. **macOS only** + * + * - `visible`: the standard title bar. + * - `transparent`: the title bar is transparent but the traffic lights stay in place. + * - `overlay`: the content extends under a transparent title bar, leaving only the + * traffic lights visible. + */ +type TitleBarStyle = 'visible' | 'transparent' | 'overlay' + +/** The edge or corner a resize drag started from, see {@link Window.startResizeDragging}. */ +type ResizeDirection = + | 'East' + | 'North' + | 'NorthEast' + | 'NorthWest' + | 'South' + | 'SouthEast' + | 'SouthWest' + | 'West' + +/** + * The payload for the `scaleChange` event. + * + * @since 1.0.2 + */ +interface ScaleFactorChanged { + /** The new window scale factor. */ + scaleFactor: number + /** The new window size */ + size: PhysicalSize +} + +/** + * Attention type to request on a window. + * + * @since 1.0.0 + */ +enum UserAttentionType { + /** + * #### Platform-specific + * - **macOS:** Bounces the dock icon until the application is in focus. + * - **Windows:** Flashes both the window and the taskbar button until the application is in focus. + */ + Critical = 1, + /** + * #### Platform-specific + * - **macOS:** Bounces the dock icon once. + * - **Windows:** Flashes the taskbar button until the application is in focus. + */ + Informational +} + +/** + * The event passed to a {@link Window.onCloseRequested} handler. + * + * Call {@link CloseRequestedEvent.preventDefault} to keep the window open, for + * example to ask the user to save their work first. + */ +class CloseRequestedEvent { + /** Event name */ + event: EventName + /** Event identifier used to unlisten */ + id: number + private _preventDefault = false + + constructor(event: Event) { + this.event = event.event + this.id = event.id + } + + /** + * Prevents the window from being closed. + * + * Must be called before the handler returns (await anything you need first), + * otherwise the window is destroyed as usual. + */ + preventDefault(): void { + this._preventDefault = true + } + + /** Whether {@link CloseRequestedEvent.preventDefault} was called on this event. */ + isPreventDefault(): boolean { + return this._preventDefault + } +} + +/** + * The cursor shape to display over a window, see {@link Window.setCursorIcon}. + * + * The names mirror the CSS `cursor` keywords in camelCase (e.g. `notAllowed` for + * `not-allowed`, `eResize` for `e-resize`). Support varies per platform and window + * manager; unsupported values fall back to the default arrow. + */ +export type CursorIcon = + | 'default' + | 'crosshair' + | 'hand' + | 'arrow' + | 'move' + | 'text' + | 'wait' + | 'help' + | 'progress' + // something cannot be done + | 'notAllowed' + | 'contextMenu' + | 'cell' + | 'verticalText' + | 'alias' + | 'copy' + | 'noDrop' + // something can be grabbed + | 'grab' + /// something is grabbed + | 'grabbing' + | 'allScroll' + | 'zoomIn' + | 'zoomOut' + // edge is to be moved + | 'eResize' + | 'nResize' + | 'neResize' + | 'nwResize' + | 'sResize' + | 'seResize' + | 'swResize' + | 'wResize' + | 'ewResize' + | 'nsResize' + | 'neswResize' + | 'nwseResize' + | 'colResize' + | 'rowResize' + +/** + * The state of the taskbar/dock progress bar, see {@link Window.setProgressBar}. + * + * @since 2.0.0 + */ +export enum ProgressBarStatus { + /** + * Hide progress bar. + */ + None = 'none', + /** + * Normal state. + */ + Normal = 'normal', + /** + * Indeterminate state. **Treated as Normal on Linux and macOS** + */ + Indeterminate = 'indeterminate', + /** + * Paused state. **Treated as Normal on Linux** + */ + Paused = 'paused', + /** + * Error state. **Treated as Normal on linux** + */ + Error = 'error' +} + +/** + * Inner size constraints for a window, see {@link Window.setSizeConstraints}. + * + * Unlike {@link Window.setMinSize} and {@link Window.setMaxSize}, each dimension is + * independent: you can constrain only the width and leave the height free. + * + * All values are in **logical pixels**. Leave a field out (or set it to + * `undefined`) to remove that constraint. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * // at least 300 logical pixels wide, height unconstrained + * await getCurrentWindow().setSizeConstraints({ minWidth: 300 }); + * ``` + * + * @since 2.0.0 + */ +export interface WindowSizeConstraints { + /** The minimum width, in logical pixels. */ + minWidth?: number + /** The minimum height, in logical pixels. */ + minHeight?: number + /** The maximum width, in logical pixels. */ + maxWidth?: number + /** The maximum height, in logical pixels. */ + maxHeight?: number +} + +/** + * The taskbar/dock progress indicator state, see {@link Window.setProgressBar}. + * + * @since 2.0.0 + */ +export interface ProgressBarState { + /** + * The progress bar status. + */ + status?: ProgressBarStatus + /** + * The progress bar progress. This can be a value ranging from `0` to `100` + */ + progress?: number +} + +/** + * Get an instance of `Window` for the current window. + * + * @since 1.0.0 + */ +function getCurrentWindow(): Window { + return new Window(window.__TAURI_INTERNALS__.metadata.currentWindow.label, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }) +} + +/** + * Gets a list of instances of `Window` for all available windows. + * + * @remarks Uses the `core:window:allow-get-all-windows` permission, which is part + * of `core:window:default`. + * + * @since 1.0.0 + */ +async function getAllWindows(): Promise { + return invoke('plugin:window|get_all_windows').then((windows) => + windows.map( + (w) => + new Window(w, { + // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor + skip: true + }) + ) + ) +} + +/** @ignore */ +// events that are emitted right here instead of by the created window +const localTauriEvents = ['tauri://created', 'tauri://error'] +/** @ignore */ +export type WindowLabel = string + +/** + * Create new window or get a handle to an existing one. + * + * Windows are identified by a *label* a unique identifier that can be used to reference it later. + * It may only contain alphanumeric characters `a-zA-Z` plus the following special characters `-`, `/`, `:` and `_`. + * + * @example + * ```typescript + * import { Window } from "@tauri-apps/api/window" + * + * const appWindow = new Window('theUniqueLabel'); + * + * appWindow.once('tauri://created', function () { + * // window successfully created + * }); + * appWindow.once('tauri://error', function (e) { + * // an error happened creating the window + * }); + * + * // emit an event to the backend + * await appWindow.emit("some-event", "data"); + * // listen to an event from the backend + * const unlisten = await appWindow.listen("event-name", e => {}); + * unlisten(); + * ``` + * + * @since 2.0.0 + */ +class Window { + /** The window label. It is a unique identifier for the window, can be used to reference it later. */ + label: WindowLabel + /** Local event listeners. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + listeners: Record>> + + /** + * Creates a new Window. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window'; + * const appWindow = new Window('my-label'); + * appWindow.once('tauri://created', function () { + * // window successfully created + * }); + * appWindow.once('tauri://error', function (e) { + * // an error happened creating the window + * }); + * ``` + * + * @param label The unique window label. Must be alphanumeric: `a-zA-Z-/:_`. + * @param options The window configuration, see {@link WindowOptions}. + * @returns The {@link Window} instance to communicate with the window. + * + * @remarks Requires the `core:window:allow-create` permission (not included in + * `core:window:default`). + */ + constructor(label: WindowLabel, options: WindowOptions = {}) { + this.label = label + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.listeners = Object.create(null) + + // @ts-expect-error `skip` is not a public API so it is not defined in WindowOptions + if (!options?.skip) { + invoke('plugin:window|create', { + options: { + ...options, + parent: + typeof options.parent === 'string' + ? options.parent + : options.parent?.label, + label + } + }) + .then(async () => this.emit('tauri://created')) + .catch(async (e: string) => this.emit('tauri://error', e)) + } + } + + /** + * Gets the Window associated with the given label. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window'; + * const mainWindow = Window.getByLabel('main'); + * ``` + * + * @param label The window label. + * @returns The Window instance to communicate with the window or null if the window doesn't exist. + */ + static async getByLabel(label: string): Promise { + return (await getAllWindows()).find((w) => w.label === label) ?? null + } + + /** + * Get an instance of `Window` for the current window. + */ + static getCurrent(): Window { + return getCurrentWindow() + } + + /** + * Gets a list of instances of `Window` for all available windows. + */ + static async getAll(): Promise { + return getAllWindows() + } + + /** + * Gets the focused window. + * @example + * ```typescript + * import { Window } from '@tauri-apps/api/window'; + * const focusedWindow = Window.getFocusedWindow(); + * ``` + * + * @returns The Window instance or `undefined` if there is not any focused window. + */ + static async getFocusedWindow(): Promise { + for (const w of await getAllWindows()) { + if (await w.isFocused()) { + return w + } + } + return null + } + + /** + * Listen to an emitted event on this window. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const unlisten = await getCurrentWindow().listen('state-changed', (event) => { + * console.log(`Got error: ${payload}`); + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listener is removed automatically when this window is destroyed, + * so you do not need to unlisten just because the window is closing. Do call the + * returned function when the listener's own scope ends, e.g. on page navigation + * or when a component unmounts. + */ + async listen( + event: EventName, + handler: EventCallback + ): Promise { + if (this._handleTauriEvent(event, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event] + listeners.splice(listeners.indexOf(handler), 1) + } + } + return listen(event, handler, { + target: { kind: 'Window', label: this.label } + }) + } + + /** + * Listen to an emitted event on this window only once. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const unlisten = await getCurrentWindow().once('initialized', (event) => { + * console.log(`Window initialized!`); + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param handler Event handler. + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listener removes itself after the first event and is also removed + * automatically when this window is destroyed. Do call the returned function if + * the listener's own scope ends before the event arrives. + */ + async once( + event: EventName, + handler: EventCallback + ): Promise { + if (this._handleTauriEvent(event, handler)) { + return () => { + // eslint-disable-next-line security/detect-object-injection + const listeners = this.listeners[event] + listeners.splice(listeners.indexOf(handler), 1) + } + } + return once(event, handler, { + target: { kind: 'Window', label: this.label } + }) + } + + /** + * Emits an event to all {@link EventTarget|targets}. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().emit('window-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + async emit(event: string, payload?: T): Promise { + if (localTauriEvents.includes(event)) { + // eslint-disable-next-line + for (const handler of this.listeners[event] || []) { + handler({ + event, + id: -1, + payload + }) + } + return + } + return emit(event, payload) + } + + /** + * Emits an event to all {@link EventTarget|targets} matching the given target. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().emitTo('main', 'window-loaded', { loggedIn: true, token: 'authToken' }); + * ``` + * @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object. + * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. + * @param payload Event payload. + */ + async emitTo( + target: string | EventTarget, + event: string, + payload?: T + ): Promise { + if (localTauriEvents.includes(event)) { + // eslint-disable-next-line security/detect-object-injection + for (const handler of this.listeners[event] || []) { + handler({ + event, + id: -1, + payload + }) + } + return + } + return emitTo(target, event, payload) + } + + /** @ignore */ + _handleTauriEvent(event: string, handler: EventCallback): boolean { + if (localTauriEvents.includes(event)) { + if (!(event in this.listeners)) { + // eslint-disable-next-line + this.listeners[event] = [handler] + } else { + // eslint-disable-next-line + this.listeners[event].push(handler) + } + return true + } + return false + } + + // Getters + /** + * The scale factor that can be used to map physical pixels to logical pixels. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const factor = await getCurrentWindow().scaleFactor(); + * ``` + * + * @returns The window's monitor scale factor. + */ + async scaleFactor(): Promise { + return invoke('plugin:window|scale_factor', { + label: this.label + }) + } + + /** + * The position of the top-left hand corner of the window's client area relative to the top-left hand corner of the desktop. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const position = await getCurrentWindow().innerPosition(); + * ``` + * + * @returns The window's inner position. + */ + async innerPosition(): Promise { + return invoke<{ x: number; y: number }>('plugin:window|inner_position', { + label: this.label + }).then((p) => new PhysicalPosition(p)) + } + + /** + * The position of the top-left hand corner of the window relative to the top-left hand corner of the desktop. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const position = await getCurrentWindow().outerPosition(); + * ``` + * + * @returns The window's outer position. + */ + async outerPosition(): Promise { + return invoke<{ x: number; y: number }>('plugin:window|outer_position', { + label: this.label + }).then((p) => new PhysicalPosition(p)) + } + + /** + * The physical size of the window's client area. + * The client area is the content of the window, excluding the title bar and borders. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const size = await getCurrentWindow().innerSize(); + * ``` + * + * @returns The window's inner size. + */ + async innerSize(): Promise { + return invoke<{ width: number; height: number }>( + 'plugin:window|inner_size', + { + label: this.label + } + ).then((s) => new PhysicalSize(s)) + } + + /** + * The physical size of the entire window. + * These dimensions include the title bar and borders. If you don't want that (and you usually don't), use inner_size instead. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const size = await getCurrentWindow().outerSize(); + * ``` + * + * @returns The window's outer size. + */ + async outerSize(): Promise { + return invoke<{ width: number; height: number }>( + 'plugin:window|outer_size', + { + label: this.label + } + ).then((s) => new PhysicalSize(s)) + } + + /** + * Gets the window's current fullscreen state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const fullscreen = await getCurrentWindow().isFullscreen(); + * ``` + * + * @returns Whether the window is in fullscreen mode or not. + */ + async isFullscreen(): Promise { + return invoke('plugin:window|is_fullscreen', { + label: this.label + }) + } + + /** + * Gets the window's current minimized state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const minimized = await getCurrentWindow().isMinimized(); + * ``` + */ + async isMinimized(): Promise { + return invoke('plugin:window|is_minimized', { + label: this.label + }) + } + + /** + * Gets the window's current maximized state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const maximized = await getCurrentWindow().isMaximized(); + * ``` + * + * @returns Whether the window is maximized or not. + */ + async isMaximized(): Promise { + return invoke('plugin:window|is_maximized', { + label: this.label + }) + } + + /** + * Gets the window's current focus state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const focused = await getCurrentWindow().isFocused(); + * ``` + * + * @returns Whether the window is focused or not. + */ + async isFocused(): Promise { + return invoke('plugin:window|is_focused', { + label: this.label + }) + } + + /** + * Gets the window's current decorated state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const decorated = await getCurrentWindow().isDecorated(); + * ``` + * + * @returns Whether the window is decorated or not. + */ + async isDecorated(): Promise { + return invoke('plugin:window|is_decorated', { + label: this.label + }) + } + + /** + * Gets the window's current resizable state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const resizable = await getCurrentWindow().isResizable(); + * ``` + * + * @returns Whether the window is resizable or not. + */ + async isResizable(): Promise { + return invoke('plugin:window|is_resizable', { + label: this.label + }) + } + + /** + * Gets the window's native maximize button state. + * + * #### Platform-specific + * + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const maximizable = await getCurrentWindow().isMaximizable(); + * ``` + * + * @returns Whether the window's native maximize button is enabled or not. + */ + async isMaximizable(): Promise { + return invoke('plugin:window|is_maximizable', { + label: this.label + }) + } + + /** + * Gets the window's native minimize button state. + * + * #### Platform-specific + * + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const minimizable = await getCurrentWindow().isMinimizable(); + * ``` + * + * @returns Whether the window's native minimize button is enabled or not. + */ + async isMinimizable(): Promise { + return invoke('plugin:window|is_minimizable', { + label: this.label + }) + } + + /** + * Gets the window's native close button state. + * + * #### Platform-specific + * + * - **iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const closable = await getCurrentWindow().isClosable(); + * ``` + * + * @returns Whether the window's native close button is enabled or not. + */ + async isClosable(): Promise { + return invoke('plugin:window|is_closable', { + label: this.label + }) + } + + /** + * Gets the window's current visible state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const visible = await getCurrentWindow().isVisible(); + * ``` + * + * @returns Whether the window is visible or not. + */ + async isVisible(): Promise { + return invoke('plugin:window|is_visible', { + label: this.label + }) + } + + /** + * Gets the window's current title. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const title = await getCurrentWindow().title(); + * ``` + */ + async title(): Promise { + return invoke('plugin:window|title', { + label: this.label + }) + } + + /** + * Gets the window's current theme. + * + * #### Platform-specific + * + * - **macOS:** Theme was introduced on macOS 10.14. Returns `light` on macOS 10.13 and below. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const theme = await getCurrentWindow().theme(); + * ``` + * + * @returns The window theme. + */ + async theme(): Promise { + return invoke('plugin:window|theme', { + label: this.label + }) + } + + /** + * Whether the window is configured to be always on top of other windows or not. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const alwaysOnTop = await getCurrentWindow().isAlwaysOnTop(); + * ``` + * + * @returns Whether the window is visible or not. + */ + async isAlwaysOnTop(): Promise { + return invoke('plugin:window|is_always_on_top', { + label: this.label + }) + } + + /** + * The name of the Android activity hosting this window. + * + * On Android each window is backed by its own activity. Use this to tell windows + * apart from native code, or to match a window with the activity declared in your + * `AndroidManifest.xml`. + * + * #### Platform-specific + * + * - **Android:** Supported. + * - **Windows / Linux / macOS / iOS:** Unsupported, the call rejects because the + * command is not registered. + * + * See the [mobile multiwindow guide](https://tauri.app/learn/mobile-multiwindow/). + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const activity = await getCurrentWindow().activityName(); + * ``` + * + * @returns The activity name. + * + * @remarks Uses the `core:window:allow-activity-name` permission, which is part + * of `core:window:default`. + * + * @since 2.11.0 + */ + async activityName(): Promise { + return invoke('plugin:window|activity_name', { + label: this.label + }) + } + + /** + * The identifier of the iOS scene hosting this window. + * + * On iOS each window is backed by a `UIScene`. Use this to tell windows apart + * from native code, or to match a window with a scene configuration declared in + * your `Info.plist`. + * + * #### Platform-specific + * + * - **iOS:** Supported. + * - **Windows / Linux / macOS / Android:** Unsupported, the call rejects because + * the command is not registered. + * + * See the [mobile multiwindow guide](https://tauri.app/learn/mobile-multiwindow/). + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const scene = await getCurrentWindow().sceneIdentifier(); + * ``` + * + * @returns The scene identifier. + * + * @remarks Uses the `core:window:allow-scene-identifier` permission, which is part + * of `core:window:default`. + * + * @since 2.11.0 + */ + async sceneIdentifier(): Promise { + return invoke('plugin:window|scene_identifier', { + label: this.label + }) + } + + // Setters + + /** + * Centers the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().center(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-center` permission (not included + * in `core:window:default`). + */ + async center(): Promise { + return invoke('plugin:window|center', { + label: this.label + }) + } + + /** + * 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 `null` 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:** `null` has no effect. + * - **Linux:** Urgency levels have the same effect. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().requestUserAttention(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-request-user-attention` permission (not included + * in `core:window:default`). + */ + async requestUserAttention( + requestType: UserAttentionType | null + ): Promise { + let requestType_ = null + if (requestType) { + if (requestType === UserAttentionType.Critical) { + requestType_ = { type: 'Critical' } + } else { + requestType_ = { type: 'Informational' } + } + } + + return invoke('plugin:window|request_user_attention', { + label: this.label, + value: requestType_ + }) + } + + /** + * Updates the window resizable flag. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setResizable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-resizable` permission (not included + * in `core:window:default`). + */ + async setResizable(resizable: boolean): Promise { + return invoke('plugin:window|set_resizable', { + label: this.label, + value: resizable + }) + } + + /** + * Enable or disable the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setEnabled(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-enabled` permission (not included + * in `core:window:default`). + * + * @since 2.0.0 + */ + async setEnabled(enabled: boolean): Promise { + return invoke('plugin:window|set_enabled', { + label: this.label, + value: enabled + }) + } + + /** + * Whether the window is enabled or disabled. + * + * A disabled window ignores all user input; see {@link Window.setEnabled}. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * const enabled = await getCurrentWindow().isEnabled(); + * ``` + * + * @returns `true` when the window accepts user input, `false` when it is disabled. + * + * @since 2.0.0 + */ + async isEnabled(): Promise { + return invoke('plugin:window|is_enabled', { + label: this.label + }) + } + + /** + * Sets whether the window's native maximize button is enabled or not. + * 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. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMaximizable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-maximizable` permission (not included + * in `core:window:default`). + */ + async setMaximizable(maximizable: boolean): Promise { + return invoke('plugin:window|set_maximizable', { + label: this.label, + value: maximizable + }) + } + + /** + * Sets whether the window's native minimize button is enabled or not. + * + * #### Platform-specific + * + * - **Linux / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMinimizable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-minimizable` permission (not included + * in `core:window:default`). + */ + async setMinimizable(minimizable: boolean): Promise { + return invoke('plugin:window|set_minimizable', { + label: this.label, + value: minimizable + }) + } + + /** + * Sets whether the window's native close button is enabled or not. + * + * #### 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. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setClosable(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-closable` permission (not included + * in `core:window:default`). + */ + async setClosable(closable: boolean): Promise { + return invoke('plugin:window|set_closable', { + label: this.label, + value: closable + }) + } + + /** + * Sets the window title. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setTitle('Tauri'); + * ``` + * + * @param title The new title + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-title` permission (not included + * in `core:window:default`). + */ + async setTitle(title: string): Promise { + return invoke('plugin:window|set_title', { + label: this.label, + value: title + }) + } + + /** + * Maximizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().maximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-maximize` permission (not included + * in `core:window:default`). + */ + async maximize(): Promise { + return invoke('plugin:window|maximize', { + label: this.label + }) + } + + /** + * Unmaximizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().unmaximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-unmaximize` permission (not included + * in `core:window:default`). + */ + async unmaximize(): Promise { + return invoke('plugin:window|unmaximize', { + label: this.label + }) + } + + /** + * Toggles the window maximized state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().toggleMaximize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-toggle-maximize` permission (not included + * in `core:window:default`). + */ + async toggleMaximize(): Promise { + return invoke('plugin:window|toggle_maximize', { + label: this.label + }) + } + + /** + * Minimizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().minimize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-minimize` permission (not included + * in `core:window:default`). + */ + async minimize(): Promise { + return invoke('plugin:window|minimize', { + label: this.label + }) + } + + /** + * Unminimizes the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().unminimize(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-unminimize` permission (not included + * in `core:window:default`). + */ + async unminimize(): Promise { + return invoke('plugin:window|unminimize', { + label: this.label + }) + } + + /** + * Sets the window visibility to true. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().show(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-show` permission (not included + * in `core:window:default`). + */ + async show(): Promise { + return invoke('plugin:window|show', { + label: this.label + }) + } + + /** + * Sets the window visibility to false. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().hide(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-hide` permission (not included + * in `core:window:default`). + */ + async hide(): Promise { + return invoke('plugin:window|hide', { + label: this.label + }) + } + + /** + * Closes the window. + * + * Note this emits a closeRequested event so you can intercept it. To force window close, use {@link Window.destroy}. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().close(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-close` permission (not included + * in `core:window:default`). + */ + async close(): Promise { + return invoke('plugin:window|close', { + label: this.label + }) + } + + /** + * Destroys the window. Behaves like {@link Window.close} but forces the window close instead of emitting a closeRequested event. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().destroy(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-destroy` permission (not included + * in `core:window:default`). + */ + async destroy(): Promise { + return invoke('plugin:window|destroy', { + label: this.label + }) + } + + /** + * Whether the window should have borders and bars. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setDecorations(false); + * ``` + * + * @param decorations Whether the window should have borders and bars. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-decorations` permission (not included + * in `core:window:default`). + */ + async setDecorations(decorations: boolean): Promise { + return invoke('plugin:window|set_decorations', { + label: this.label, + value: decorations + }) + } + + /** + * Whether or not the window should have shadow. + * + * #### Platform-specific + * + * - **Windows:** + * - `false` has no effect on decorated window, shadows are always ON. + * - `true` will make undecorated window have a 1px white border, + * and on Windows 11, it will have a rounded corners. + * - **Linux:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setShadow(false); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-shadow` permission (not included + * in `core:window:default`). + */ + async setShadow(enable: boolean): Promise { + return invoke('plugin:window|set_shadow', { + label: this.label, + value: enable + }) + } + + /** + * Applies platform-specific window effects such as Mica, Acrylic, Blur or the + * macOS vibrancy materials. + * + * Requires the window to be transparent, so create it with `transparent: true` + * (or set `transparent` in `tauri.conf.json`). Give the page a transparent or + * translucent background as well, otherwise the effect is hidden behind your own + * background color. + * + * Conflicting effects are resolved by applying the first supported one and + * ignoring the rest, so you can list a Windows and a macOS effect together. + * + * #### Platform-specific + * + * - **Windows:** If the window uses decorations or shadows, you may need + * [this workaround](https://github.com/tauri-apps/tao/issues/72#issuecomment-975607891). + * - **Linux:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow, Effect, EffectState } from '@tauri-apps/api/window'; + * + * await getCurrentWindow().setEffects({ + * effects: [Effect.Mica, Effect.Acrylic, Effect.UnderWindowBackground], + * state: EffectState.Active, + * radius: 8 + * }); + * ``` + * + * @param effects The effects to apply, see {@link Effects}. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-effects` permission (not included + * in `core:window:default`). + * + * @since 2.0.0 + */ + async setEffects(effects: Effects): Promise { + return invoke('plugin:window|set_effects', { + label: this.label, + value: effects + }) + } + + /** + * Clear any applied effects if possible. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().clearEffects(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-effects` permission (not included + * in `core:window:default`), the same one used by {@link Window.setEffects}. + * + * @since 2.0.0 + */ + async clearEffects(): Promise { + return invoke('plugin:window|set_effects', { + label: this.label, + value: null + }) + } + + /** + * Whether the window should always be on top of other windows. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setAlwaysOnTop(true); + * ``` + * + * @param alwaysOnTop Whether the window should always be on top of other windows or not. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-always-on-top` permission (not included + * in `core:window:default`). + */ + async setAlwaysOnTop(alwaysOnTop: boolean): Promise { + return invoke('plugin:window|set_always_on_top', { + label: this.label, + value: alwaysOnTop + }) + } + + /** + * Whether the window should always be below other windows. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setAlwaysOnBottom(true); + * ``` + * + * @param alwaysOnBottom Whether the window should always be below other windows or not. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-always-on-bottom` permission (not included + * in `core:window:default`). + */ + async setAlwaysOnBottom(alwaysOnBottom: boolean): Promise { + return invoke('plugin:window|set_always_on_bottom', { + label: this.label, + value: alwaysOnBottom + }) + } + + /** + * Prevents the window contents from being captured by other apps. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setContentProtected(true); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-content-protected` permission (not included + * in `core:window:default`). + */ + async setContentProtected(protected_: boolean): Promise { + return invoke('plugin:window|set_content_protected', { + label: this.label, + value: protected_ + }) + } + + /** + * Resizes the window with a new inner size. + * @example + * ```typescript + * import { getCurrentWindow, LogicalSize } from '@tauri-apps/api/window'; + * await getCurrentWindow().setSize(new LogicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-size` permission (not included + * in `core:window:default`). + */ + async setSize(size: LogicalSize | PhysicalSize | Size): Promise { + return invoke('plugin:window|set_size', { + label: this.label, + value: size instanceof Size ? size : new Size(size) + }) + } + + /** + * Sets the window minimum inner size. If the `size` argument is not provided, the constraint is unset. + * @example + * ```typescript + * import { getCurrentWindow, PhysicalSize } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMinSize(new PhysicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size, or `null` to unset the constraint. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-min-size` permission (not included + * in `core:window:default`). + */ + async setMinSize( + size: LogicalSize | PhysicalSize | Size | null | undefined + ): Promise { + return invoke('plugin:window|set_min_size', { + label: this.label, + value: size instanceof Size ? size : size ? new Size(size) : null + }) + } + + /** + * Sets the window maximum inner size. If the `size` argument is undefined, the constraint is unset. + * @example + * ```typescript + * import { getCurrentWindow, LogicalSize } from '@tauri-apps/api/window'; + * await getCurrentWindow().setMaxSize(new LogicalSize(600, 500)); + * ``` + * + * @param size The logical or physical inner size, or `null` to unset the constraint. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-max-size` permission (not included + * in `core:window:default`). + */ + async setMaxSize( + size: LogicalSize | PhysicalSize | Size | null | undefined + ): Promise { + return invoke('plugin:window|set_max_size', { + label: this.label, + value: size instanceof Size ? size : size ? new Size(size) : null + }) + } + + /** + * Sets the window inner size constraints. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setSizeConstraints({ minWidth: 300 }); + * ``` + * + * @param constraints The inner size constraints in **logical pixels**, or `null` + * to unset every constraint. Each field is optional and independent, so + * `{ minWidth: 300 }` constrains only the width. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-size-constraints` permission (not included + * in `core:window:default`). + */ + async setSizeConstraints( + constraints: WindowSizeConstraints | null | undefined + ): Promise { + function logical(pixel?: number): { Logical: number } | null { + return pixel ? { Logical: pixel } : null + } + + return invoke('plugin:window|set_size_constraints', { + label: this.label, + value: { + minWidth: logical(constraints?.minWidth), + minHeight: logical(constraints?.minHeight), + maxWidth: logical(constraints?.maxWidth), + maxHeight: logical(constraints?.maxHeight) + } + }) + } + + /** + * Sets the window outer position. + * @example + * ```typescript + * import { getCurrentWindow, LogicalPosition } from '@tauri-apps/api/window'; + * await getCurrentWindow().setPosition(new LogicalPosition(600, 500)); + * ``` + * + * @param position The new position, in logical or physical pixels. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-position` permission (not included + * in `core:window:default`). + */ + async setPosition( + position: LogicalPosition | PhysicalPosition | Position + ): Promise { + return invoke('plugin:window|set_position', { + label: this.label, + value: position instanceof Position ? position : new Position(position) + }) + } + + /** + * Sets the window fullscreen state. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setFullscreen(true); + * ``` + * + * @param fullscreen Whether the window should go to fullscreen or not. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-fullscreen` permission (not included + * in `core:window:default`). + */ + async setFullscreen(fullscreen: boolean): Promise { + return invoke('plugin:window|set_fullscreen', { + label: this.label, + value: fullscreen + }) + } + + /** + * Sets the window as fullscreen on the monitor that contains the given physical position. + * + * Does nothing if no monitor contains the position. + * @example + * ```typescript + * import { getCurrentWindow, availableMonitors } from '@tauri-apps/api/window'; + * const monitors = await availableMonitors(); + * if (monitors.length > 1) { + * await getCurrentWindow().setFullscreenOnMonitor(monitors[1].position); + * } + * ``` + * + * @param position A physical position inside the target monitor, such as {@linkcode Monitor.position}. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-fullscreen-on-monitor` permission (not included + * in `core:window:default`). + * + * @since 2.12.0 + */ + async setFullscreenOnMonitor(position: PhysicalPosition): Promise { + return invoke('plugin:window|set_fullscreen_on_monitor', { + label: this.label, + value: position + }) + } + + /** + * On macOS, Toggles a fullscreen mode that doesn’t require a new macOS space. Returns a boolean indicating whether the transition was successful (this won't work if the window was already in the native fullscreen). + * This is how fullscreen used to work on macOS in versions before Lion. And allows the user to have a fullscreen window without using another space or taking control over the entire monitor. + * + * On other platforms, this is the same as {@link Window.setFullscreen}. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setSimpleFullscreen(true); + * ``` + * + * @param fullscreen Whether the window should go to simple fullscreen or not. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-simple-fullscreen` permission (not included + * in `core:window:default`). + * + * @since 2.8.0 + */ + async setSimpleFullscreen(fullscreen: boolean): Promise { + return invoke('plugin:window|set_simple_fullscreen', { + label: this.label, + value: fullscreen + }) + } + + /** + * Bring the window to front and focus. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setFocus(); + * ``` + * + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-focus` permission (not included + * in `core:window:default`). + */ + async setFocus(): Promise { + return invoke('plugin:window|set_focus', { + label: this.label + }) + } + + /** + * 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 {@link Window.setFocus} but it will move the window to the back i.e. at the bottom in terms of z-order. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setFocusable(true); + * ``` + * + * @param focusable Whether the window can be focused. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-focusable` permission (not included + * in `core:window:default`). + */ + async setFocusable(focusable: boolean): Promise { + return invoke('plugin:window|set_focusable', { + label: this.label, + value: focusable + }) + } + + /** + * Sets the window icon. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setIcon('/tauri/awesome.png'); + * ``` + * + * 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"] } + * ``` + * + * @param icon Icon bytes or path to the icon file. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-icon` permission (not included + * in `core:window:default`). + */ + async setIcon(icon: JsImage): Promise { + return invoke('plugin:window|set_icon', { + label: this.label, + value: transformImage(icon) + }) + } + + /** + * Whether the window icon should be hidden from the taskbar or not. + * + * #### Platform-specific + * + * - **macOS:** Unsupported. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setSkipTaskbar(true); + * ``` + * + * @param skip true to hide window icon, false to show it. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-skip-taskbar` permission (not included + * in `core:window:default`). + */ + async setSkipTaskbar(skip: boolean): Promise { + return invoke('plugin:window|set_skip_taskbar', { + label: this.label, + value: skip + }) + } + + /** + * Grabs the cursor, preventing it from leaving the window. + * + * There's no guarantee that the cursor will be hidden. You should + * hide it by yourself if you want so. + * + * #### Platform-specific + * + * - **Linux:** Unsupported. + * - **macOS:** This locks the cursor in a fixed location, which looks visually awkward. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorGrab(true); + * ``` + * + * @param grab `true` to grab the cursor icon, `false` to release it. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-cursor-grab` permission (not included + * in `core:window:default`). + */ + async setCursorGrab(grab: boolean): Promise { + return invoke('plugin:window|set_cursor_grab', { + label: this.label, + value: grab + }) + } + + /** + * Modifies the cursor's visibility. + * + * #### Platform-specific + * + * - **Windows:** The cursor is only hidden within the confines of the window. + * - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor is + * outside of the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorVisible(false); + * ``` + * + * @param visible If `false`, this will hide the cursor. If `true`, this will show the cursor. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-cursor-visible` permission (not included + * in `core:window:default`). + */ + async setCursorVisible(visible: boolean): Promise { + return invoke('plugin:window|set_cursor_visible', { + label: this.label, + value: visible + }) + } + + /** + * Modifies the cursor icon of the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorIcon('help'); + * ``` + * + * @param icon The new cursor icon. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-cursor-icon` permission (not included + * in `core:window:default`). + */ + async setCursorIcon(icon: CursorIcon): Promise { + return invoke('plugin:window|set_cursor_icon', { + label: this.label, + value: icon + }) + } + + /** + * Sets the window background color. + * + * #### Platform-specific: + * + * - **Windows:** alpha channel is ignored. + * - **iOS / Android:** Unsupported. + * + * This sets the color of the window itself, which is what you see while the + * webview is still loading or behind a transparent page. To also change the + * webview background use {@link WebviewWindow.setBackgroundColor}. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setBackgroundColor('#2f2f2f'); + * // also accepts an RGB/RGBA tuple or an object + * await getCurrentWindow().setBackgroundColor([47, 47, 47, 255]); + * ``` + * + * @param color The new background color, see {@link Color}. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-background-color` permission (not included + * in `core:window:default`). + * + * @since 2.1.0 + */ + async setBackgroundColor(color: Color): Promise { + return invoke('plugin:window|set_background_color', { + label: this.label, + value: color + }) + } + + /** + * Changes the position of the cursor in window coordinates. + * @example + * ```typescript + * import { getCurrentWindow, LogicalPosition } from '@tauri-apps/api/window'; + * await getCurrentWindow().setCursorPosition(new LogicalPosition(600, 300)); + * ``` + * + * @param position The new cursor position. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-cursor-position` permission (not included + * in `core:window:default`). + */ + async setCursorPosition( + position: LogicalPosition | PhysicalPosition | Position + ): Promise { + return invoke('plugin:window|set_cursor_position', { + label: this.label, + value: position instanceof Position ? position : new Position(position) + }) + } + + /** + * Changes the cursor events behavior. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setIgnoreCursorEvents(true); + * ``` + * + * @param ignore `true` to ignore the cursor events; `false` to process them as usual. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-ignore-cursor-events` permission (not included + * in `core:window:default`). + */ + async setIgnoreCursorEvents(ignore: boolean): Promise { + return invoke('plugin:window|set_ignore_cursor_events', { + label: this.label, + value: ignore + }) + } + + /** + * Starts dragging the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().startDragging(); + * ``` + * + * @return A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-start-dragging` permission (not included + * in `core:window:default`). + */ + async startDragging(): Promise { + return invoke('plugin:window|start_dragging', { + label: this.label + }) + } + + /** + * Starts resize-dragging the window. + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().startResizeDragging(); + * ``` + * + * @return A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-start-resize-dragging` permission (not included + * in `core:window:default`). + */ + async startResizeDragging(direction: ResizeDirection): Promise { + return invoke('plugin:window|start_resize_dragging', { + label: this.label, + value: direction + }) + } + + /** + * Sets the badge count. It is app wide and not specific to this window. + * + * #### Platform-specific + * + * - **Windows**: Unsupported. Use {@linkcode Window.setOverlayIcon} instead. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setBadgeCount(5); + * ``` + * + * @param count The badge count. Use `undefined` to remove the badge. + * @return A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-badge-count` permission (not included + * in `core:window:default`). + */ + async setBadgeCount(count?: number): Promise { + return invoke('plugin:window|set_badge_count', { + label: this.label, + value: count + }) + } + + /** + * Sets the badge label shown on the app's dock icon. **macOS only** + * + * Unlike {@linkcode Window.setBadgeCount}, which takes a number, this shows + * arbitrary short text. It is app-wide and not specific to this window. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setBadgeLabel("Hello"); + * ``` + * + * @param label The badge label. Use `undefined` to remove the badge. + * @return A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-badge-label` permission (not included + * in `core:window:default`). + */ + async setBadgeLabel(label?: string): Promise { + return invoke('plugin:window|set_badge_label', { + label: this.label, + value: label + }) + } + + /** + * Sets the overlay icon. **Windows only** + * The overlay icon can be set for every window. + * + * + * 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"] } + * ``` + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setOverlayIcon("/tauri/awesome.png"); + * ``` + * + * @param icon Icon bytes or path to the icon file. Use `undefined` to remove the overlay icon. + * @return A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-overlay-icon` permission (not included + * in `core:window:default`). + */ + async setOverlayIcon(icon?: JsImage): Promise { + return invoke('plugin:window|set_overlay_icon', { + label: this.label, + value: icon ? transformImage(icon) : undefined + }) + } + + /** + * Sets the taskbar progress state. + * + * #### Platform-specific + * + * - **Linux / macOS**: Progress bar is app-wide and not specific to this window. + * - **Linux**: Only supported desktop environments with `libunity` (e.g. GNOME). + * + * @example + * ```typescript + * import { getCurrentWindow, ProgressBarStatus } from '@tauri-apps/api/window'; + * await getCurrentWindow().setProgressBar({ + * status: ProgressBarStatus.Normal, + * progress: 50, + * }); + * ``` + * + * @return A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-progress-bar` permission (not included + * in `core:window:default`). + */ + async setProgressBar(state: ProgressBarState): Promise { + return invoke('plugin:window|set_progress_bar', { + label: this.label, + value: state + }) + } + + /** + * Sets whether the window should be visible on all workspaces or virtual desktops. + * + * #### Platform-specific + * + * - **Windows / iOS / Android:** Unsupported. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setVisibleOnAllWorkspaces(true); + * ``` + * + * @param visible Whether the window should follow the user across workspaces. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-visible-on-all-workspaces` permission (not included + * in `core:window:default`). + * + * @since 2.0.0 + */ + async setVisibleOnAllWorkspaces(visible: boolean): Promise { + return invoke('plugin:window|set_visible_on_all_workspaces', { + label: this.label, + value: visible + }) + } + + /** + * Sets the title bar style. **macOS only**. + * + * Use `transparent` or `overlay` to build a custom title bar: with `overlay` the + * page extends under the title bar and only the traffic lights remain, so leave + * room for them in your layout and pair it with {@link Window.startDragging} on a + * draggable region. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setTitleBarStyle('overlay'); + * ``` + * + * @param style The new title bar style, see {@link TitleBarStyle}. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-title-bar-style` permission (not included + * in `core:window:default`). + * + * @since 2.0.0 + */ + async setTitleBarStyle(style: TitleBarStyle): Promise { + return invoke('plugin:window|set_title_bar_style', { + label: this.label, + value: style + }) + } + + /** + * Set window theme, pass in `null` or `undefined` to follow system theme + * + * #### Platform-specific + * + * - **Linux / macOS**: Theme is app-wide and not specific to this window. + * - **iOS / Android:** Unsupported. + * + * Use {@link Window.theme} to read the effective theme and + * {@link Window.onThemeChanged} to react to changes. + * + * @example + * ```typescript + * import { getCurrentWindow } from '@tauri-apps/api/window'; + * await getCurrentWindow().setTheme('dark'); + * // follow the system theme again + * await getCurrentWindow().setTheme(null); + * ``` + * + * @param theme The theme to apply, or `null`/`undefined` to follow the system theme. + * @returns A promise indicating the success or failure of the operation. + * + * @remarks Requires the `core:window:allow-set-theme` permission (not included + * in `core:window:default`). + * + * @since 2.0.0 + */ + async setTheme(theme?: Theme | null): Promise { + return invoke('plugin:window|set_theme', { + label: this.label, + value: theme + }) + } + + // Listeners + + /** + * Listen to window resize. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onResized(({ payload: size }) => { + * console.log('Window resized', size); + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listener is removed automatically when this window is destroyed, + * so you do not need to unlisten just because the window is closing. Do call the + * returned function when the listener's own scope ends, e.g. on page navigation + * or when a component unmounts. + */ + async onResized(handler: EventCallback): Promise { + return this.listen(TauriEvent.WINDOW_RESIZED, (e) => { + e.payload = new PhysicalSize(e.payload) + handler(e) + }) + } + + /** + * Listen to window move. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onMoved(({ payload: position }) => { + * console.log('Window moved', position); + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listener is removed automatically when this window is destroyed, + * so you do not need to unlisten just because the window is closing. Do call the + * returned function when the listener's own scope ends, e.g. on page navigation + * or when a component unmounts. + */ + async onMoved(handler: EventCallback): Promise { + return this.listen(TauriEvent.WINDOW_MOVED, (e) => { + e.payload = new PhysicalPosition(e.payload) + handler(e) + }) + } + + /** + * Listen to window close requested. Emitted when the user requests to closes the window. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * // `confirm` comes from the dialog plugin, which you have to add separately: + * // `pnpm tauri add dialog` + * import { confirm } from '@tauri-apps/plugin-dialog'; + * const unlisten = await getCurrentWindow().onCloseRequested(async (event) => { + * const confirmed = await confirm('Are you sure?'); + * if (!confirmed) { + * // user did not confirm closing the window; let's prevent it + * event.preventDefault(); + * } + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listener is removed automatically when this window is destroyed, + * so you do not need to unlisten just because the window is closing. Do call the + * returned function when the listener's own scope ends, e.g. on page navigation + * or when a component unmounts. + */ + async onCloseRequested( + handler: (event: CloseRequestedEvent) => void | Promise + ): Promise { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + return this.listen(TauriEvent.WINDOW_CLOSE_REQUESTED, async (event) => { + const evt = new CloseRequestedEvent(event) + await handler(evt) + if (!evt.isPreventDefault()) { + await this.destroy() + } + }) + } + + /** + * Listen to a file drop event. + * The listener is triggered when the user hovers the selected files on the webview, + * drops the files or cancels the operation. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onDragDropEvent((event) => { + * if (event.payload.type === 'over') { + * console.log('User hovering', event.payload.position); + * } else if (event.payload.type === 'drop') { + * console.log('User dropped', event.payload.paths); + * } else { + * console.log('File drop cancelled'); + * } + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listener is removed automatically when this window is destroyed, + * so you do not need to unlisten just because the window is closing. Do call the + * returned function when the listener's own scope ends, e.g. on page navigation + * or when a component unmounts. + */ + async onDragDropEvent( + handler: EventCallback + ): Promise { + type DragPayload = { paths: string[]; position: PhysicalPosition } + + const unlistenDrag = await this.listen( + TauriEvent.DRAG_ENTER, + (event) => { + handler({ + ...event, + payload: { + type: 'enter', + paths: event.payload.paths, + position: new PhysicalPosition(event.payload.position) + } + }) + } + ) + + const unlistenDragOver = await this.listen( + TauriEvent.DRAG_OVER, + (event) => { + handler({ + ...event, + payload: { + type: 'over', + position: new PhysicalPosition(event.payload.position) + } + }) + } + ) + + const unlistenDrop = await this.listen( + TauriEvent.DRAG_DROP, + (event) => { + handler({ + ...event, + payload: { + type: 'drop', + paths: event.payload.paths, + position: new PhysicalPosition(event.payload.position) + } + }) + } + ) + + const unlistenCancel = await this.listen( + TauriEvent.DRAG_LEAVE, + (event) => { + handler({ ...event, payload: { type: 'leave' } }) + } + ) + + return () => { + unlistenDrag() + unlistenDrop() + unlistenDragOver() + unlistenCancel() + } + } + + /** + * Listen to window focus change. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onFocusChanged(({ payload: focused }) => { + * console.log('Focus changed, window is focused? ' + focused); + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listener is removed automatically when this window is destroyed, + * so you do not need to unlisten just because the window is closing. Do call the + * returned function when the listener's own scope ends, e.g. on page navigation + * or when a component unmounts. + */ + async onFocusChanged(handler: EventCallback): Promise { + const unlistenFocus = await this.listen( + TauriEvent.WINDOW_FOCUS, + (event) => { + handler({ ...event, payload: true }) + } + ) + const unlistenBlur = await this.listen( + TauriEvent.WINDOW_BLUR, + (event) => { + handler({ ...event, payload: false }) + } + ) + return () => { + unlistenFocus() + unlistenBlur() + } + } + + /** + * Listen to window scale change. Emitted when the window's scale factor has changed. + * The following user actions can cause DPI changes: + * - Changing the display's resolution. + * - Changing the display's scale factor (e.g. in Control Panel on Windows). + * - Moving the window to a display with a different scale factor. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onScaleChanged(({ payload }) => { + * console.log('Scale changed', payload.scaleFactor, payload.size); + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listener is removed automatically when this window is destroyed, + * so you do not need to unlisten just because the window is closing. Do call the + * returned function when the listener's own scope ends, e.g. on page navigation + * or when a component unmounts. + */ + async onScaleChanged( + handler: EventCallback + ): Promise { + return this.listen( + TauriEvent.WINDOW_SCALE_FACTOR_CHANGED, + handler + ) + } + + /** + * Listen to the system theme change. + * + * @example + * ```typescript + * import { getCurrentWindow } from "@tauri-apps/api/window"; + * const unlisten = await getCurrentWindow().onThemeChanged(({ payload: theme }) => { + * console.log('New theme: ' + theme); + * }); + * + * // call unlisten when your handler goes out of scope e.g. the component is unmounted + * unlisten(); + * ``` + * + * @returns A promise resolving to a function to unlisten to the event. + * + * @remarks The listener is removed automatically when this window is destroyed, + * so you do not need to unlisten just because the window is closing. Do call the + * returned function when the listener's own scope ends, e.g. on page navigation + * or when a component unmounts. + */ + async onThemeChanged(handler: EventCallback): Promise { + return this.listen(TauriEvent.WINDOW_THEME_CHANGED, handler) + } +} + +/** + * An RGBA color. Each value has minimum of 0 and maximum of 255. + * + * It can be either a string `#ffffff`, an array of 3 or 4 elements or an object. + * + * @since 2.0.0 + */ +type Color = + | [number, number, number] + | [number, number, number, number] + | { red: number; green: number; blue: number; alpha: number } + | string + +/** + * Background throttling policy + * + * @since 2.0.0 + */ +enum BackgroundThrottlingPolicy { + Disabled = 'disabled', + Throttle = 'throttle', + Suspend = 'suspend' +} + +/** + * The scrollbar style to use in the webview. + * + * ## Platform-specific + * + * **Windows**: This option must be given the same value for all webviews. + * + * @since 2.8.0 + */ +enum ScrollBarStyle { + /** + * The default scrollbar style for the webview. + */ + Default = 'default', + /** + * 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 + */ + FluentOverlay = 'fluentOverlay' +} + +/** + * Platform-specific window effects + * + * @since 2.0.0 + */ +enum Effect { + /** + * A default material appropriate for the view's effectiveAppearance. **macOS 10.14-** + * + * @deprecated since macOS 10.14. You should instead choose an appropriate semantic material. + */ + AppearanceBased = 'appearanceBased', + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + Light = 'light', + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + Dark = 'dark', + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + MediumLight = 'mediumLight', + /** + * **macOS 10.14-** + * + * @deprecated since macOS 10.14. Use a semantic material instead. + */ + UltraDark = 'ultraDark', + /** + * **macOS 10.10+** + */ + Titlebar = 'titlebar', + /** + * **macOS 10.10+** + */ + Selection = 'selection', + /** + * **macOS 10.11+** + */ + Menu = 'menu', + /** + * **macOS 10.11+** + */ + Popover = 'popover', + /** + * **macOS 10.11+** + */ + Sidebar = 'sidebar', + /** + * **macOS 10.14+** + */ + HeaderView = 'headerView', + /** + * **macOS 10.14+** + */ + Sheet = 'sheet', + /** + * **macOS 10.14+** + */ + WindowBackground = 'windowBackground', + /** + * **macOS 10.14+** + */ + HudWindow = 'hudWindow', + /** + * **macOS 10.14+** + */ + FullScreenUI = 'fullScreenUI', + /** + * **macOS 10.14+** + */ + Tooltip = 'tooltip', + /** + * **macOS 10.14+** + */ + ContentBackground = 'contentBackground', + /** + * **macOS 10.14+** + */ + UnderWindowBackground = 'underWindowBackground', + /** + * **macOS 10.14+** + */ + UnderPageBackground = 'underPageBackground', + /** + * **macOS 26.0+** + */ + LiquidGlassRegular = 'liquidGlassRegular', + /** + * **macOS 26.0+** + */ + LiquidGlassClear = 'liquidGlassClear', + /** + * **Windows 11 Only** + */ + Mica = 'mica', + /** + * **Windows 7/10/11(22H1) Only** + * + * #### Notes + * + * This effect has bad performance when resizing/dragging the window on Windows 11 build 22621. + */ + Blur = 'blur', + /** + * **Windows 10/11** + * + * #### Notes + * + * This effect has bad performance when resizing/dragging the window on Windows 10 v1903+ and Windows 11 build 22000. + */ + Acrylic = 'acrylic', + /** + * Tabbed effect that matches the system dark preference **Windows 11 Only** + */ + Tabbed = 'tabbed', + /** + * Tabbed effect with dark mode but only if dark mode is enabled on the system **Windows 11 Only** + */ + TabbedDark = 'tabbedDark', + /** + * Tabbed effect with light mode **Windows 11 Only** + */ + TabbedLight = 'tabbedLight' +} + +/** + * Window effect state **macOS only** + * + * @see https://developer.apple.com/documentation/appkit/nsvisualeffectview/state + * + * @since 2.0.0 + */ +enum EffectState { + /** + * Make window effect state follow the window's active state **macOS only** + */ + FollowsWindowActiveState = 'followsWindowActiveState', + /** + * Make window effect state always active **macOS only** + */ + Active = 'active', + /** + * Make window effect state always inactive **macOS only** + */ + Inactive = 'inactive' +} + +/** The window effects configuration object + * + * @since 2.0.0 + */ +interface Effects { + /** + * List of Window effects to apply to the Window. + * + * Generally, conflicting effects will apply the first one and ignore the rest but + * on macOS you can specify one Liquid Glass style and one Visual Effect material at the same time + * to make Tauri fallback to the latter on macOS 15 and below. + */ + effects: Effect[] + /** + * Window effect state **macOS Only**. Ignored for Liquid Glass Effects. + */ + state?: EffectState + /** + * Window effect corner radius **macOS Only** + */ + radius?: number + /** + * Window effect color. + * + * #### Platform-specific + * + * - **Windows**: Affects {@link Effect.Blur} and {@link Effect.Acrylic} only + * on Windows 10 v1903+. Doesn't have any effect on Windows 7 or Windows 11. + * - **macOS**: Only affects Liquid Glass effects. + */ + color?: Color + /** + * Enables interactive glass behavior, which adds a visual response to user interactions. + * + * **macOS 27.0+**. Only affects Liquid Glass effects. + */ + interactive?: boolean +} + +/** + * An extra margin kept free around a window when + * {@link WindowOptions.preventOverflow} limits it to the working area. + * + * Values are in logical pixels. + * + * @since 2.5.0 + */ +interface PreventOverflowMargin { + /** The horizontal margin, in logical pixels. */ + width: number + /** The vertical margin, in logical pixels. */ + height: number +} + +/** + * Configuration for the window to create. + * + * @since 1.0.0 + */ +interface WindowOptions { + /** Show window in the center of the screen.. */ + center?: boolean + /** + * The initial horizontal position in logical pixels, measured from the left edge + * of the screen. Only applies if `y` is also set. + */ + x?: number + /** + * The initial vertical position in logical pixels, measured from the top edge of + * the screen. Only applies if `x` is also set. + */ + y?: number + /** The initial width in logical pixels. */ + width?: number + /** The initial height in logical pixels. */ + height?: number + /** The minimum width in logical pixels. Only applies if `minHeight` is also set. */ + minWidth?: number + /** The minimum height in logical pixels. Only applies if `minWidth` is also set. */ + minHeight?: number + /** The maximum width in logical pixels. Only applies if `maxHeight` is also set. */ + maxWidth?: number + /** The maximum height in logical pixels. Only applies if `maxWidth` is also set. */ + maxHeight?: number + /** + * 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` + * + * Can either be set to `true` or to a {@link PreventOverflowMargin} object to set an additional margin + * that should be considered to determine the working area + * (in this case the window size will be limited to `monitor size - taskbar size - margin`) + * + * **NOTE**: The overflow check is only performed on window creation, resizes can still overflow + * + * #### Platform-specific + * + * - **iOS / Android:** Unsupported. + */ + preventOverflow?: boolean | PreventOverflowMargin + /** Whether the window is resizable or not. */ + resizable?: boolean + /** Window title. */ + title?: string + /** Whether the window is in fullscreen mode or not. */ + fullscreen?: boolean + /** Whether the window will be initially focused or not. */ + focus?: boolean + /** Whether the window can be focused or not. */ + focusable?: boolean + /** + * Whether the window is transparent or not. + * Note that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri.conf.json > app > macOSPrivateApi`. + * WARNING: Using private APIs on `macOS` prevents your application from being accepted to the `App Store`. + * + * On Windows, using `noRedirectionBitmap` can help avoid a white flash when creating a transparent window. + */ + transparent?: boolean + /** Whether the window should be maximized upon creation or not. */ + maximized?: boolean + /** Whether the window should be immediately visible upon creation or not. */ + visible?: boolean + /** Whether the window should have borders and bars or not. */ + decorations?: boolean + /** Whether the window should always be on top of other windows or not. */ + alwaysOnTop?: boolean + /** Whether the window should always be below other windows. */ + alwaysOnBottom?: boolean + /** Prevents the window contents from being captured by other apps. */ + contentProtected?: boolean + /** Whether or not the window icon should be added to the taskbar. */ + skipTaskbar?: boolean + /** + * This sets `WS_EX_NOREDIRECTIONBITMAP`. + * + * This can avoid the white flash that may appear before the webview content is rendered + * when using a transparent window. **Windows only**. + */ + noRedirectionBitmap?: boolean + /** + * Whether or not the window has shadow. + * + * #### Platform-specific + * + * - **Windows:** + * - `false` has no effect on decorated window, shadows are always ON. + * - `true` will make undecorated window have a 1px white border, + * and on Windows 11, it will have a rounded corners. + * - **Linux:** Unsupported. + * + * @since 2.0.0 + */ + shadow?: boolean + /** + * The initial window theme. Defaults to the system theme. + * + * Only implemented on Windows and macOS 10.14+. + */ + theme?: Theme + /** + * The style of the macOS title bar. + */ + titleBarStyle?: TitleBarStyle + /** + * The position of the window controls on macOS. + * + * Requires `titleBarStyle: 'overlay'` and `decorations: true`. + * + * @since 2.4.0 + */ + trafficLightPosition?: LogicalPosition + /** + * If `true`, sets the window title to be hidden on macOS. + */ + hiddenTitle?: boolean + /** + * Defines the window [tabbing identifier](https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier) on macOS. + * + * Windows with the same tabbing identifier will be grouped together. + * If the tabbing identifier is not set, automatic tabbing will be disabled. + */ + tabbingIdentifier?: string + /** + * Whether the window's native maximize button is enabled or not. Defaults to `true`. + */ + maximizable?: boolean + /** + * Whether the window's native minimize button is enabled or not. Defaults to `true`. + */ + minimizable?: boolean + /** + * Whether the window's native close button is enabled or not. Defaults to `true`. + */ + closable?: boolean + /** + * Sets a parent to the window to be created. Can be either a {@linkcode Window} or a label of the window. + * + * #### Platform-specific + * + * - **Windows**: This sets the passed parent as an owner window to the window to be created. + * From [MSDN owned windows docs](https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#owned-windows): + * - An owned window is always above its owner in the z-order. + * - The system automatically destroys an owned window when its owner is destroyed. + * - An owned window is hidden when its owner is minimized. + * - **Linux**: This makes the new window transient for parent, see + * - **macOS**: This adds the window as a child of parent, see + */ + parent?: Window | WebviewWindow | string + /** Whether the window should be visible on all workspaces or virtual desktops. + * + * #### Platform-specific + * + * - **Windows / iOS / Android:** Unsupported. + * + * @since 2.0.0 + */ + visibleOnAllWorkspaces?: boolean + /** + * Window effects. + * + * Requires the window to be transparent. + * + * #### Platform-specific: + * + * - **Windows**: If using decorations or shadows, you may want to try this workaround + * - **Linux**: Unsupported + */ + windowEffects?: Effects + /** + * Set the window background color. + * + * #### Platform-specific: + * + * - **Android / iOS:** Unsupported. + * - **Windows**: alpha channel is ignored. + * + * @since 2.1.0 + */ + backgroundColor?: Color + + /** Change the default background throttling behaviour. + * + * ## Platform-specific + * + * - **Linux / Windows / Android**: Unsupported. Workarounds like a pending WebLock transaction might suffice. + * - **iOS**: Supported since version 17.0+. + * - **macOS**: Supported since version 14.0+. + * + * see https://github.com/tauri-apps/tauri/issues/5250#issuecomment-2569380578 + * + * @since 2.3.0 + */ + backgroundThrottling?: BackgroundThrottlingPolicy + /** + * Whether we should disable JavaScript code execution on the webview or not. + */ + javascriptDisabled?: boolean + /** + * on macOS and iOS there is a link preview on long pressing links, this is enabled by default. + * see https://docs.rs/objc2-web-kit/latest/objc2_web_kit/struct.WKWebView.html#method.allowsLinkPreview + */ + allowLinkPreview?: boolean + /** + * Allows disabling the input accessory view on iOS. + * + * The accessory view is the view that appears above the keyboard when a text input element is focused. + * It usually displays a view with "Done", "Next" buttons. + */ + disableInputAccessoryView?: boolean + /** + * Specifies the native scrollbar style to use with the webview. + * CSS styles that modify the scrollbar are applied on top of the native appearance configured here. + * + * Defaults to `default`, which is the browser default. + * + * ## Platform-specific + * + * - **Windows**: + * - `fluentOverlay` requires WebView2 Runtime version 125.0.2535.41 or higher, and does nothing + * on older versions. + * - This option must be given the same value for all webviews. + * - **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 { + return m === null + ? null + : { + name: m.name, + scaleFactor: m.scaleFactor, + position: new PhysicalPosition(m.position), + size: new PhysicalSize(m.size), + workArea: { + position: new PhysicalPosition(m.workArea.position), + size: new PhysicalSize(m.workArea.size) + } + } +} + +/** + * Returns the monitor on which the window currently resides. + * Returns `null` if current monitor can't be detected. + * @example + * ```typescript + * import { currentMonitor } from '@tauri-apps/api/window'; + * const monitor = await currentMonitor(); + * ``` + * + * @since 1.0.0 + */ +async function currentMonitor(): Promise { + return invoke('plugin:window|current_monitor').then( + mapMonitor + ) +} + +/** + * Returns the primary monitor of the system. + * Returns `null` if it can't identify any monitor as a primary one. + * @example + * ```typescript + * import { primaryMonitor } from '@tauri-apps/api/window'; + * const monitor = await primaryMonitor(); + * ``` + * + * @since 1.0.0 + */ +async function primaryMonitor(): Promise { + return invoke('plugin:window|primary_monitor').then( + mapMonitor + ) +} + +/** + * Returns the monitor that contains the given point. Returns `null` if can't find any. + * @example + * ```typescript + * import { monitorFromPoint } from '@tauri-apps/api/window'; + * const monitor = await monitorFromPoint(100.0, 200.0); + * ``` + * + * @since 1.0.0 + */ +async function monitorFromPoint(x: number, y: number): Promise { + return invoke('plugin:window|monitor_from_point', { + x, + y + }).then(mapMonitor) +} + +/** + * Returns the list of all the monitors available on the system. + * @example + * ```typescript + * import { availableMonitors } from '@tauri-apps/api/window'; + * const monitors = await availableMonitors(); + * ``` + * + * @since 1.0.0 + */ +async function availableMonitors(): Promise { + return invoke('plugin:window|available_monitors').then( + (ms) => ms.map(mapMonitor) as Monitor[] + ) +} + +/** + * Get the cursor position relative to the top-left hand corner of the desktop. + * + * Note that the top-left hand corner of the desktop is not necessarily the same as the screen. + * If the user uses a desktop with multiple monitors, + * the top-left hand corner of the desktop is the top-left hand corner of the main monitor on Windows and macOS + * or the top-left of the leftmost monitor on X11. + * + * The coordinates can be negative if the top-left hand corner of the window is outside of the visible screen region. + * + * @example + * ```typescript + * import { cursorPosition } from '@tauri-apps/api/window'; + * const position = await cursorPosition(); + * console.log(position.x, position.y); + * ``` + * + * @returns The cursor position, in physical pixels. + * + * @remarks Uses the `core:window:allow-cursor-position` permission, which is part + * of `core:window:default`. + * + * @since 2.0.0 + */ +async function cursorPosition(): Promise { + return invoke('plugin:window|cursor_position').then( + (v) => new PhysicalPosition(v) + ) +} + +export { + Window, + CloseRequestedEvent, + getCurrentWindow, + getAllWindows, + LogicalSize, + PhysicalSize, + LogicalPosition, + PhysicalPosition, + UserAttentionType, + Effect, + EffectState, + currentMonitor, + monitorFromPoint, + primaryMonitor, + availableMonitors, + cursorPosition +} + +export type { + Effects, + Theme, + TitleBarStyle, + ScaleFactorChanged, + WindowOptions, + PreventOverflowMargin, + Color, + BackgroundThrottlingPolicy, + DragDropEvent, + ScrollBarStyle +} diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json new file mode 100644 index 0000000..de0c8d3 --- /dev/null +++ b/packages/api/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2019", + "module": "esnext", + "moduleResolution": "bundler", + "skipLibCheck": true, + "strict": true, + "noUnusedLocals": true, + "noImplicitAny": true, + "noEmit": true, + "lib": ["es2022", "esnext.disposable", "dom"] + } +} diff --git a/packages/cli/.cargo/config.toml b/packages/cli/.cargo/config.toml new file mode 100644 index 0000000..13390d1 --- /dev/null +++ b/packages/cli/.cargo/config.toml @@ -0,0 +1,23 @@ +# napi-rs can't handle custom target-dir configs yet so we make sure it's set to the default +[build] +target-dir = "target" + +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" + +[target.aarch64-unknown-linux-musl] +linker = "aarch64-linux-musl-gcc" +rustflags = ["-C", "target-feature=-crt-static"] + +[target.armv7-unknown-linux-gnueabihf] +linker = "arm-linux-gnueabihf-gcc" + +[target.riscv64gc-unknown-linux-gnu] +linker = "riscv64-linux-gnu-gcc" + +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] +[target.i686-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] +[target.aarch64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore new file mode 100644 index 0000000..b6ef3d3 --- /dev/null +++ b/packages/cli/.gitignore @@ -0,0 +1,2 @@ +# debug builds of NAPI +*.node diff --git a/packages/cli/.npmignore b/packages/cli/.npmignore new file mode 100644 index 0000000..4054f47 --- /dev/null +++ b/packages/cli/.npmignore @@ -0,0 +1,14 @@ +target +Cargo.lock +.cargo +.github +npm +test +.eslintrc +.prettierignore +rustfmt.toml +*.node +__tests__ +src +Cargo.toml +build.rs \ No newline at end of file diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml new file mode 100644 index 0000000..3d6dd1a --- /dev/null +++ b/packages/cli/Cargo.toml @@ -0,0 +1,24 @@ +[package] +edition = "2024" +name = "tauri-cli-node" +version = "0.0.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +napi = "3.12.2" +napi-derive = "3.6.3" +tauri-cli = { path = "../../crates/tauri-cli", default-features = false } +log = "0.4.21" + +[build-dependencies] +napi-build = "2.4.1" + +[features] +default = ["tauri-cli/default"] +native-tls = ["tauri-cli/native-tls"] +native-tls-vendored = ["tauri-cli/native-tls-vendored"] + +[lints] +workspace = true diff --git a/packages/cli/LICENSE-APACHE-2.0 b/packages/cli/LICENSE-APACHE-2.0 new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/packages/cli/LICENSE-APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/packages/cli/LICENSE-MIT b/packages/cli/LICENSE-MIT new file mode 100644 index 0000000..b08530d --- /dev/null +++ b/packages/cli/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000..2255c26 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,52 @@ +# @tauri-apps/cli + + + +[![status](https://img.shields.io/badge/status-stable-blue.svg)](https://github.com/tauri-apps/tauri/tree/dev) +[![License](https://img.shields.io/badge/License-MIT%20or%20Apache%202-green.svg)](https://opencollective.com/tauri) +[![test cli](https://img.shields.io/github/actions/workflow/status/tauri-apps/tauri/test-cli-js.yml?label=test%20cli&logo=github)](https://github.com/tauri-apps/tauri/actions/workflows/test-cli-js.yml) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri?ref=badge_shield) +[![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/SpmNs4S) +[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app) +[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation) +[![support](https://img.shields.io/badge/sponsor-Open%20Collective-blue.svg)](https://opencollective.com/tauri) + +| Component | Version | +| --------------- | ----------------------------------------------------- | +| @tauri-apps/cli | ![](https://img.shields.io/npm/v/@tauri-apps/cli.svg) | + +## About Tauri + +Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily. + +Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task. + +## This module + +Written in Typescript and packaged such that it can be used with `npm`, `pnpm`, `yarn`, and `bun`, this library provides a node.js runner for common tasks when using Tauri, like `pnpm tauri dev`. For the most part it is a wrapper around [tauri-cli](https://github.com/tauri-apps/tauri/blob/dev/crates/tauri-cli). + +To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document. + +## Installation + +The preferred method is to install this module locally as a development dependency: + +``` +$ pnpm add -D @tauri-apps/cli +$ yarn add -D @tauri-apps/cli +$ npm add -D @tauri-apps/cli +``` + +## Semver + +**tauri** is following [Semantic Versioning 2.0](https://semver.org/). + +## Licenses + +Code: (c) 2019 - 2021 - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. + +Logo: CC-BY-NC-ND + +- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum) diff --git a/packages/cli/__tests__/fixtures/empty/.gitignore b/packages/cli/__tests__/fixtures/empty/.gitignore new file mode 100644 index 0000000..7abb34a --- /dev/null +++ b/packages/cli/__tests__/fixtures/empty/.gitignore @@ -0,0 +1 @@ +src-tauri diff --git a/packages/cli/__tests__/fixtures/empty/dist/index.html b/packages/cli/__tests__/fixtures/empty/dist/index.html new file mode 100644 index 0000000..6206991 --- /dev/null +++ b/packages/cli/__tests__/fixtures/empty/dist/index.html @@ -0,0 +1,6 @@ + + + +
+ + diff --git a/packages/cli/__tests__/fixtures/empty/package.json b/packages/cli/__tests__/fixtures/empty/package.json new file mode 100644 index 0000000..8f2a284 --- /dev/null +++ b/packages/cli/__tests__/fixtures/empty/package.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "build": "" + } +} diff --git a/packages/cli/__tests__/template.spec.ts b/packages/cli/__tests__/template.spec.ts new file mode 100644 index 0000000..a205c54 --- /dev/null +++ b/packages/cli/__tests__/template.spec.ts @@ -0,0 +1,89 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { resolve } from 'node:path' +import { spawnSync } from 'node:child_process' +import { + existsSync, + readFileSync, + writeFileSync, + rmSync, + renameSync +} from 'node:fs' +import { beforeAll, describe, it } from 'vitest' + +// Build CLI before tests, for local usage only. +// CI builds the CLI on different platforms and architectures +if (!process.env.CI) { + beforeAll(() => { + const cliDir = resolve(__dirname, '..') + exec('pnpm', ['build:debug'], { cwd: cliDir }) + }) +} + +describe('[CLI] @tauri-apps/cli template', () => { + it('init a project and builds it', { timeout: 15 * 60 * 1000 }, async () => { + const cwd = process.cwd() + const fixturePath = resolve(__dirname, './fixtures/empty') + const tauriFixturePath = resolve(fixturePath, 'src-tauri') + const outPath = resolve(tauriFixturePath, 'target') + const cacheOutPath = resolve(fixturePath, 'target') + + process.chdir(fixturePath) + + const outExists = existsSync(outPath) + if (outExists) { + if (existsSync(cacheOutPath)) { + rmSync(cacheOutPath, { recursive: true, force: true }) + } + renameSync(outPath, cacheOutPath) + } + + const cli = await import('../main.js') + + await cli.run([ + 'init', + '-vv', + '--directory', + process.cwd(), + '--force', + '--tauri-path', + resolve(__dirname, '../../..'), + '--before-build-command', + '', + '--before-dev-command', + '', + '--ci' + ]) + + if (outExists) { + renameSync(cacheOutPath, outPath) + } + + process.chdir(tauriFixturePath) + + const manifestPath = resolve(tauriFixturePath, 'Cargo.toml') + const manifestFile = readFileSync(manifestPath).toString() + writeFileSync(manifestPath, `workspace = { }\n${manifestFile}`) + + const configPath = resolve(tauriFixturePath, 'tauri.conf.json') + const config = readFileSync(configPath).toString() + writeFileSync(configPath, config.replace('com.tauri.dev', 'com.tauri.test')) + + await cli.run(['build', '-vv']) + process.chdir(cwd) + }) +}) + +function exec( + bin: string, + args?: string[], + opts?: { + cwd?: string + } +) { + process.platform === 'win32' + ? spawnSync('cmd', ['/c', bin, ...(args ?? [])], { cwd: opts?.cwd }) + : spawnSync(bin, args, { cwd: opts?.cwd }) +} diff --git a/packages/cli/append-headers.js b/packages/cli/append-headers.js new file mode 100644 index 0000000..22c6768 --- /dev/null +++ b/packages/cli/append-headers.js @@ -0,0 +1,15 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const HEADERS = `// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT` + +const fs = require('fs') + +for (const file of ['index.js', 'index.d.ts']) { + const content = fs.readFileSync(file, 'utf8') + const newContent = `${HEADERS}\n\n${content}` + fs.writeFileSync(file, newContent, 'utf8') +} diff --git a/packages/cli/build.rs b/packages/cli/build.rs new file mode 100644 index 0000000..3409fdb --- /dev/null +++ b/packages/cli/build.rs @@ -0,0 +1,7 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +fn main() { + ::napi_build::setup(); +} diff --git a/packages/cli/index.d.ts b/packages/cli/index.d.ts new file mode 100644 index 0000000..956ddc1 --- /dev/null +++ b/packages/cli/index.d.ts @@ -0,0 +1,9 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/* auto-generated by NAPI-RS */ +/* eslint-disable */ +export declare function logError(error: string): void + +export declare function run(args: Array, binName: string | undefined | null, callback: ((err: Error | null, arg: boolean) => any)): void diff --git a/packages/cli/index.js b/packages/cli/index.js new file mode 100644 index 0000000..9f54039 --- /dev/null +++ b/packages/cli/index.js @@ -0,0 +1,595 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// prettier-ignore +/* eslint-disable */ +// @ts-nocheck +/* auto-generated by NAPI-RS */ + +const { readFileSync } = require('fs') +let nativeBinding = null +const loadErrors = [] + +const isMusl = () => { + let musl = false + if (process.platform === 'linux') { + musl = isMuslFromFilesystem() + if (musl === null) { + musl = isMuslFromReport() + } + if (musl === null) { + musl = isMuslFromChildProcess() + } + } + return musl +} + +const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') + +const isMuslFromFilesystem = () => { + try { + return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') + } catch { + return null + } +} + +const isMuslFromReport = () => { + let report = null + if (process.report && typeof process.report.getReport === 'function') { + process.report.excludeNetwork = true + report = process.report.getReport() + } + if (!report) { + return null + } + if (report.header && report.header.glibcVersionRuntime) { + return false + } + if (Array.isArray(report.sharedObjects)) { + if (report.sharedObjects.some(isFileMusl)) { + return true + } + } + return false +} + +const isMuslFromChildProcess = () => { + try { + return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') + } catch (e) { + // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false + return false + } +} + +function requireNative() { + if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) { + try { + return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); + } catch (err) { + loadErrors.push(err) + } + } else if (process.platform === 'android') { + if (process.arch === 'arm64') { + try { + return require('./cli.android-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@tauri-apps/cli-android-arm64') + const bindingPackageVersion = require('@tauri-apps/cli-android-arm64/package.json').version + if (bindingPackageVersion !== '2.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm') { + try { + return require('./cli.android-arm-eabi.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) + } + } else if (process.platform === 'win32') { + if (process.arch === 'x64') { + if ((process.config && process.config.variables && process.config.variables.shlib_suffix === 'dll.a') || (process.config && process.config.variables && process.config.variables.node_target_type === 'shared_library')) { + try { + return require('./cli.win32-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./cli.win32-x64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'ia32') { + try { + return require('./cli.win32-ia32-msvc.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./cli.win32-arm64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) + } + } else if (process.platform === 'darwin') { + try { + return require('./cli.darwin-universal.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@tauri-apps/cli-darwin-universal') + const bindingPackageVersion = require('@tauri-apps/cli-darwin-universal/package.json').version + if (bindingPackageVersion !== '2.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + if (process.arch === 'x64') { + try { + return require('./cli.darwin-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@tauri-apps/cli-darwin-x64') + const bindingPackageVersion = require('@tauri-apps/cli-darwin-x64/package.json').version + if (bindingPackageVersion !== '2.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./cli.darwin-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@tauri-apps/cli-darwin-arm64') + const bindingPackageVersion = require('@tauri-apps/cli-darwin-arm64/package.json').version + if (bindingPackageVersion !== '2.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) + } + } else if (process.platform === 'freebsd') { + if (process.arch === 'x64') { + try { + return require('./cli.freebsd-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@tauri-apps/cli-freebsd-x64') + const bindingPackageVersion = require('@tauri-apps/cli-freebsd-x64/package.json').version + if (bindingPackageVersion !== '2.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./cli.freebsd-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@tauri-apps/cli-freebsd-arm64') + const bindingPackageVersion = require('@tauri-apps/cli-freebsd-arm64/package.json').version + if (bindingPackageVersion !== '2.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) + } + } else if (process.platform === 'linux') { + if (process.arch === 'x64') { + if (isMusl()) { + try { + return require('./cli.linux-x64-musl.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./cli.linux-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'arm64') { + if (isMusl()) { + try { + return require('./cli.linux-arm64-musl.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./cli.linux-arm64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'arm') { + if (isMusl()) { + try { + return require('./cli.linux-arm-musleabihf.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./cli.linux-arm-gnueabihf.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'loong64') { + if (isMusl()) { + try { + return require('./cli.linux-loong64-musl.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./cli.linux-loong64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'riscv64') { + if (isMusl()) { + try { + return require('./cli.linux-riscv64-musl.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./cli.linux-riscv64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'ppc64') { + try { + return require('./cli.linux-ppc64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 's390x') { + try { + return require('./cli.linux-s390x-gnu.node') + } catch (e) { + loadErrors.push(e) + } + 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.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) + } + } else if (process.platform === 'openharmony') { + if (process.arch === 'arm64') { + try { + return require('./cli.openharmony-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@tauri-apps/cli-openharmony-arm64') + const bindingPackageVersion = require('@tauri-apps/cli-openharmony-arm64/package.json').version + if (bindingPackageVersion !== '2.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'x64') { + try { + return require('./cli.openharmony-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@tauri-apps/cli-openharmony-x64') + const bindingPackageVersion = require('@tauri-apps/cli-openharmony-x64/package.json').version + if (bindingPackageVersion !== '2.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm') { + try { + return require('./cli.openharmony-arm.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@tauri-apps/cli-openharmony-arm') + const bindingPackageVersion = require('@tauri-apps/cli-openharmony-arm/package.json').version + if (bindingPackageVersion !== '2.11.4' && 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.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`)) + } + } else { + loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) + } +} + +nativeBinding = requireNative() + +// NAPI_RS_FORCE_WASI is a tri-state flag: +// unset / any other value → native binding preferred, WASI is only a fallback +// 'true' → force WASI fallback even if native loaded +// 'error' → force WASI and throw if no WASI binding is found +// Treating any non-empty string as truthy (the historical behavior) meant +// NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered +// the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file. +const forceWasi = + process.env.NAPI_RS_FORCE_WASI === 'true' || process.env.NAPI_RS_FORCE_WASI === 'error' + +if (!nativeBinding || forceWasi) { + let wasiBinding = null + let wasiBindingError = null + try { + wasiBinding = require('./cli.wasi.cjs') + nativeBinding = wasiBinding + } catch (err) { + if (forceWasi) { + wasiBindingError = err + } + } + if (!nativeBinding || forceWasi) { + try { + wasiBinding = require('@tauri-apps/cli-wasm32-wasi') + nativeBinding = wasiBinding + } catch (err) { + if (forceWasi) { + if (!wasiBindingError) { + wasiBindingError = err + } else { + wasiBindingError.cause = err + } + loadErrors.push(err) + } + } + } + if (process.env.NAPI_RS_FORCE_WASI === 'error' && !wasiBinding) { + const error = new Error('WASI binding not found and NAPI_RS_FORCE_WASI is set to error') + error.cause = wasiBindingError + throw error + } +} + +if (!nativeBinding) { + if (loadErrors.length > 0) { + const error = new Error( + `Cannot find native binding. ` + + `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` + + 'Please try `npm i` again after removing both package-lock.json and node_modules directory.', + ) + // assign instead of the `new Error(message, { cause })` options form, + // which Node < 16.9 silently ignores + error.cause = loadErrors.reduce((err, cur) => { + cur.cause = err + return cur + }) + throw error + } + throw new Error(`Failed to load native binding`) +} + +module.exports = nativeBinding +module.exports.logError = nativeBinding.logError +module.exports.run = nativeBinding.run diff --git a/packages/cli/main.d.ts b/packages/cli/main.d.ts new file mode 100644 index 0000000..ee5d858 --- /dev/null +++ b/packages/cli/main.d.ts @@ -0,0 +1,11 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/* tslint:disable */ +/* eslint-disable */ + +export function run( + args: Array, + binName?: string | undefined | null +): Promise diff --git a/packages/cli/main.js b/packages/cli/main.js new file mode 100644 index 0000000..5fb1933 --- /dev/null +++ b/packages/cli/main.js @@ -0,0 +1,19 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const { run, logError } = require('./index') + +module.exports.run = (args, binName) => { + return new Promise((resolve, reject) => { + run(args, binName, (error, res) => { + if (error) { + reject(error) + } else { + resolve(res) + } + }) + }) +} + +module.exports.logError = logError diff --git a/packages/cli/npm/darwin-arm64/README.md b/packages/cli/npm/darwin-arm64/README.md new file mode 100644 index 0000000..3af0749 --- /dev/null +++ b/packages/cli/npm/darwin-arm64/README.md @@ -0,0 +1,3 @@ +# `@tauri-apps/cli-darwin-arm64` + +This is the **aarch64-apple-darwin** binary for `@tauri-apps/cli` diff --git a/packages/cli/npm/darwin-arm64/package.json b/packages/cli/npm/darwin-arm64/package.json new file mode 100644 index 0000000..fbef68b --- /dev/null +++ b/packages/cli/npm/darwin-arm64/package.json @@ -0,0 +1,33 @@ +{ + "name": "@tauri-apps/cli-darwin-arm64", + "version": "0.0.0", + "cpu": [ + "arm64" + ], + "main": "cli.darwin-arm64.node", + "files": [ + "cli.darwin-arm64.node" + ], + "description": "Command line interface for building Tauri apps", + "homepage": "https://github.com/tauri-apps/tauri#readme", + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">= 10" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tauri-apps/tauri.git" + }, + "bugs": { + "url": "https://github.com/tauri-apps/tauri/issues" + }, + "publishConfig": { + "access": "public" + }, + "os": [ + "darwin" + ] +} diff --git a/packages/cli/npm/darwin-x64/README.md b/packages/cli/npm/darwin-x64/README.md new file mode 100644 index 0000000..2945287 --- /dev/null +++ b/packages/cli/npm/darwin-x64/README.md @@ -0,0 +1,3 @@ +# `@tauri-apps/cli-darwin-x64` + +This is the **x86_64-apple-darwin** binary for `@tauri-apps/cli` diff --git a/packages/cli/npm/darwin-x64/package.json b/packages/cli/npm/darwin-x64/package.json new file mode 100644 index 0000000..4ac481c --- /dev/null +++ b/packages/cli/npm/darwin-x64/package.json @@ -0,0 +1,33 @@ +{ + "name": "@tauri-apps/cli-darwin-x64", + "version": "0.0.0", + "cpu": [ + "x64" + ], + "main": "cli.darwin-x64.node", + "files": [ + "cli.darwin-x64.node" + ], + "description": "Command line interface for building Tauri apps", + "homepage": "https://github.com/tauri-apps/tauri#readme", + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">= 10" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tauri-apps/tauri.git" + }, + "bugs": { + "url": "https://github.com/tauri-apps/tauri/issues" + }, + "publishConfig": { + "access": "public" + }, + "os": [ + "darwin" + ] +} diff --git a/packages/cli/npm/linux-arm-gnueabihf/README.md b/packages/cli/npm/linux-arm-gnueabihf/README.md new file mode 100644 index 0000000..28eabad --- /dev/null +++ b/packages/cli/npm/linux-arm-gnueabihf/README.md @@ -0,0 +1,3 @@ +# `@tauri-apps/cli-linux-arm-gnueabihf` + +This is the **armv7-unknown-linux-gnueabihf** binary for `@tauri-apps/cli` diff --git a/packages/cli/npm/linux-arm-gnueabihf/package.json b/packages/cli/npm/linux-arm-gnueabihf/package.json new file mode 100644 index 0000000..5bb7438 --- /dev/null +++ b/packages/cli/npm/linux-arm-gnueabihf/package.json @@ -0,0 +1,33 @@ +{ + "name": "@tauri-apps/cli-linux-arm-gnueabihf", + "version": "0.0.0", + "cpu": [ + "arm" + ], + "main": "cli.linux-arm-gnueabihf.node", + "files": [ + "cli.linux-arm-gnueabihf.node" + ], + "description": "Command line interface for building Tauri apps", + "homepage": "https://github.com/tauri-apps/tauri#readme", + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">= 10" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tauri-apps/tauri.git" + }, + "bugs": { + "url": "https://github.com/tauri-apps/tauri/issues" + }, + "publishConfig": { + "access": "public" + }, + "os": [ + "linux" + ] +} diff --git a/packages/cli/npm/linux-arm64-gnu/README.md b/packages/cli/npm/linux-arm64-gnu/README.md new file mode 100644 index 0000000..7cb17b8 --- /dev/null +++ b/packages/cli/npm/linux-arm64-gnu/README.md @@ -0,0 +1,3 @@ +# `@tauri-apps/cli-linux-arm64-gnu` + +This is the **aarch64-unknown-linux-gnu** binary for `@tauri-apps/cli` diff --git a/packages/cli/npm/linux-arm64-gnu/package.json b/packages/cli/npm/linux-arm64-gnu/package.json new file mode 100644 index 0000000..76fbdbb --- /dev/null +++ b/packages/cli/npm/linux-arm64-gnu/package.json @@ -0,0 +1,36 @@ +{ + "name": "@tauri-apps/cli-linux-arm64-gnu", + "version": "0.0.0", + "cpu": [ + "arm64" + ], + "main": "cli.linux-arm64-gnu.node", + "files": [ + "cli.linux-arm64-gnu.node" + ], + "description": "Command line interface for building Tauri apps", + "homepage": "https://github.com/tauri-apps/tauri#readme", + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">= 10" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tauri-apps/tauri.git" + }, + "bugs": { + "url": "https://github.com/tauri-apps/tauri/issues" + }, + "publishConfig": { + "access": "public" + }, + "os": [ + "linux" + ], + "libc": [ + "glibc" + ] +} diff --git a/packages/cli/npm/linux-arm64-musl/README.md b/packages/cli/npm/linux-arm64-musl/README.md new file mode 100644 index 0000000..fc83b51 --- /dev/null +++ b/packages/cli/npm/linux-arm64-musl/README.md @@ -0,0 +1,3 @@ +# `@tauri-apps/cli-linux-arm64-musl` + +This is the **aarch64-unknown-linux-musl** binary for `@tauri-apps/cli` diff --git a/packages/cli/npm/linux-arm64-musl/package.json b/packages/cli/npm/linux-arm64-musl/package.json new file mode 100644 index 0000000..13db8d3 --- /dev/null +++ b/packages/cli/npm/linux-arm64-musl/package.json @@ -0,0 +1,36 @@ +{ + "name": "@tauri-apps/cli-linux-arm64-musl", + "version": "0.0.0", + "cpu": [ + "arm64" + ], + "main": "cli.linux-arm64-musl.node", + "files": [ + "cli.linux-arm64-musl.node" + ], + "description": "Command line interface for building Tauri apps", + "homepage": "https://github.com/tauri-apps/tauri#readme", + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">= 10" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tauri-apps/tauri.git" + }, + "bugs": { + "url": "https://github.com/tauri-apps/tauri/issues" + }, + "publishConfig": { + "access": "public" + }, + "os": [ + "linux" + ], + "libc": [ + "musl" + ] +} diff --git a/packages/cli/npm/linux-riscv64-gnu/README.md b/packages/cli/npm/linux-riscv64-gnu/README.md new file mode 100644 index 0000000..2f94164 --- /dev/null +++ b/packages/cli/npm/linux-riscv64-gnu/README.md @@ -0,0 +1,3 @@ +# `@tauri-apps/cli-linux-riscv64-gnu` + +This is the **riscv64gc-unknown-linux-gnu** binary for `@tauri-apps/cli` diff --git a/packages/cli/npm/linux-riscv64-gnu/package.json b/packages/cli/npm/linux-riscv64-gnu/package.json new file mode 100644 index 0000000..a018512 --- /dev/null +++ b/packages/cli/npm/linux-riscv64-gnu/package.json @@ -0,0 +1,36 @@ +{ + "name": "@tauri-apps/cli-linux-riscv64-gnu", + "version": "0.0.0", + "cpu": [ + "riscv64" + ], + "main": "cli.linux-riscv64-gnu.node", + "files": [ + "cli.linux-riscv64-gnu.node" + ], + "description": "Command line interface for building Tauri apps", + "homepage": "https://github.com/tauri-apps/tauri#readme", + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">= 10" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tauri-apps/tauri.git" + }, + "bugs": { + "url": "https://github.com/tauri-apps/tauri/issues" + }, + "publishConfig": { + "access": "public" + }, + "os": [ + "linux" + ], + "libc": [ + "glibc" + ] +} diff --git a/packages/cli/npm/linux-x64-gnu/README.md b/packages/cli/npm/linux-x64-gnu/README.md new file mode 100644 index 0000000..38e990a --- /dev/null +++ b/packages/cli/npm/linux-x64-gnu/README.md @@ -0,0 +1,3 @@ +# `@tauri-apps/cli-linux-x64-gnu` + +This is the **x86_64-unknown-linux-gnu** binary for `@tauri-apps/cli` diff --git a/packages/cli/npm/linux-x64-gnu/package.json b/packages/cli/npm/linux-x64-gnu/package.json new file mode 100644 index 0000000..f82c73a --- /dev/null +++ b/packages/cli/npm/linux-x64-gnu/package.json @@ -0,0 +1,36 @@ +{ + "name": "@tauri-apps/cli-linux-x64-gnu", + "version": "0.0.0", + "cpu": [ + "x64" + ], + "main": "cli.linux-x64-gnu.node", + "files": [ + "cli.linux-x64-gnu.node" + ], + "description": "Command line interface for building Tauri apps", + "homepage": "https://github.com/tauri-apps/tauri#readme", + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">= 10" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tauri-apps/tauri.git" + }, + "bugs": { + "url": "https://github.com/tauri-apps/tauri/issues" + }, + "publishConfig": { + "access": "public" + }, + "os": [ + "linux" + ], + "libc": [ + "glibc" + ] +} diff --git a/packages/cli/npm/linux-x64-musl/README.md b/packages/cli/npm/linux-x64-musl/README.md new file mode 100644 index 0000000..a987fb5 --- /dev/null +++ b/packages/cli/npm/linux-x64-musl/README.md @@ -0,0 +1,3 @@ +# `@tauri-apps/cli-linux-x64-musl` + +This is the **x86_64-unknown-linux-musl** binary for `@tauri-apps/cli` diff --git a/packages/cli/npm/linux-x64-musl/package.json b/packages/cli/npm/linux-x64-musl/package.json new file mode 100644 index 0000000..a14491a --- /dev/null +++ b/packages/cli/npm/linux-x64-musl/package.json @@ -0,0 +1,36 @@ +{ + "name": "@tauri-apps/cli-linux-x64-musl", + "version": "0.0.0", + "cpu": [ + "x64" + ], + "main": "cli.linux-x64-musl.node", + "files": [ + "cli.linux-x64-musl.node" + ], + "description": "Command line interface for building Tauri apps", + "homepage": "https://github.com/tauri-apps/tauri#readme", + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">= 10" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tauri-apps/tauri.git" + }, + "bugs": { + "url": "https://github.com/tauri-apps/tauri/issues" + }, + "publishConfig": { + "access": "public" + }, + "os": [ + "linux" + ], + "libc": [ + "musl" + ] +} diff --git a/packages/cli/npm/win32-arm64-msvc/README.md b/packages/cli/npm/win32-arm64-msvc/README.md new file mode 100644 index 0000000..eba7d40 --- /dev/null +++ b/packages/cli/npm/win32-arm64-msvc/README.md @@ -0,0 +1,3 @@ +# `@tauri-apps/cli-win32-arm64-msvc` + +This is the **aarch64-pc-windows-msvc** binary for `@tauri-apps/cli` diff --git a/packages/cli/npm/win32-arm64-msvc/package.json b/packages/cli/npm/win32-arm64-msvc/package.json new file mode 100644 index 0000000..209e388 --- /dev/null +++ b/packages/cli/npm/win32-arm64-msvc/package.json @@ -0,0 +1,33 @@ +{ + "name": "@tauri-apps/cli-win32-arm64-msvc", + "version": "0.0.0", + "cpu": [ + "arm64" + ], + "main": "cli.win32-arm64-msvc.node", + "files": [ + "cli.win32-arm64-msvc.node" + ], + "description": "Command line interface for building Tauri apps", + "homepage": "https://github.com/tauri-apps/tauri#readme", + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">= 10" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tauri-apps/tauri.git" + }, + "bugs": { + "url": "https://github.com/tauri-apps/tauri/issues" + }, + "publishConfig": { + "access": "public" + }, + "os": [ + "win32" + ] +} diff --git a/packages/cli/npm/win32-ia32-msvc/README.md b/packages/cli/npm/win32-ia32-msvc/README.md new file mode 100644 index 0000000..8ef1c64 --- /dev/null +++ b/packages/cli/npm/win32-ia32-msvc/README.md @@ -0,0 +1,3 @@ +# `@tauri-apps/cli-win32-ia32-msvc` + +This is the **i686-pc-windows-msvc** binary for `@tauri-apps/cli` diff --git a/packages/cli/npm/win32-ia32-msvc/package.json b/packages/cli/npm/win32-ia32-msvc/package.json new file mode 100644 index 0000000..7878f6d --- /dev/null +++ b/packages/cli/npm/win32-ia32-msvc/package.json @@ -0,0 +1,33 @@ +{ + "name": "@tauri-apps/cli-win32-ia32-msvc", + "version": "0.0.0", + "cpu": [ + "ia32" + ], + "main": "cli.win32-ia32-msvc.node", + "files": [ + "cli.win32-ia32-msvc.node" + ], + "description": "Command line interface for building Tauri apps", + "homepage": "https://github.com/tauri-apps/tauri#readme", + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">= 10" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tauri-apps/tauri.git" + }, + "bugs": { + "url": "https://github.com/tauri-apps/tauri/issues" + }, + "publishConfig": { + "access": "public" + }, + "os": [ + "win32" + ] +} diff --git a/packages/cli/npm/win32-x64-msvc/README.md b/packages/cli/npm/win32-x64-msvc/README.md new file mode 100644 index 0000000..c22490b --- /dev/null +++ b/packages/cli/npm/win32-x64-msvc/README.md @@ -0,0 +1,3 @@ +# `@tauri-apps/cli-win32-x64-msvc` + +This is the **x86_64-pc-windows-msvc** binary for `@tauri-apps/cli` diff --git a/packages/cli/npm/win32-x64-msvc/package.json b/packages/cli/npm/win32-x64-msvc/package.json new file mode 100644 index 0000000..939d302 --- /dev/null +++ b/packages/cli/npm/win32-x64-msvc/package.json @@ -0,0 +1,33 @@ +{ + "name": "@tauri-apps/cli-win32-x64-msvc", + "version": "0.0.0", + "cpu": [ + "x64" + ], + "main": "cli.win32-x64-msvc.node", + "files": [ + "cli.win32-x64-msvc.node" + ], + "description": "Command line interface for building Tauri apps", + "homepage": "https://github.com/tauri-apps/tauri#readme", + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">= 10" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tauri-apps/tauri.git" + }, + "bugs": { + "url": "https://github.com/tauri-apps/tauri/issues" + }, + "publishConfig": { + "access": "public" + }, + "os": [ + "win32" + ] +} diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..6a16061 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,67 @@ +{ + "name": "@tauri-apps/cli", + "version": "2.11.4", + "description": "Command line interface for building Tauri apps", + "type": "commonjs", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tauri-apps/tauri.git" + }, + "contributors": [ + "Tauri Programme within The Commons Conservancy" + ], + "license": "Apache-2.0 OR MIT", + "bugs": { + "url": "https://github.com/tauri-apps/tauri/issues" + }, + "homepage": "https://github.com/tauri-apps/tauri#readme", + "publishConfig": { + "access": "public" + }, + "main": "main.js", + "types": "main.d.ts", + "napi": { + "binaryName": "cli", + "targets": [ + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", + "x86_64-apple-darwin", + "aarch64-apple-darwin", + "aarch64-unknown-linux-gnu", + "aarch64-unknown-linux-musl", + "armv7-unknown-linux-gnueabihf", + "x86_64-unknown-linux-musl", + "riscv64gc-unknown-linux-gnu", + "i686-pc-windows-msvc", + "aarch64-pc-windows-msvc" + ] + }, + "devDependencies": { + "@napi-rs/cli": "3.9.0", + "@types/node": "^24.11.0", + "cross-env": "10.1.0", + "vitest": "^5.0.0" + }, + "engines": { + "node": ">= 10" + }, + "bin": { + "tauri": "./tauri.js" + }, + "scripts": { + "artifacts": "napi artifacts", + "build": "cross-env TARGET=node napi build --platform --profile release-size-optimized", + "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", + "prepack": "cp ../../crates/tauri-schema-generator/schemas/config.schema.json .", + "version": "napi version", + "test": "vitest run", + "tauri": "node ./tauri.js" + } +} diff --git a/packages/cli/src/lib.rs b/packages/cli/src/lib.rs new file mode 100644 index 0000000..cd00c36 --- /dev/null +++ b/packages/cli/src/lib.rs @@ -0,0 +1,53 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg(any(target_os = "macos", target_os = "linux", windows))] + +use std::sync::Arc; + +use napi::{ + Error, Result, Status, + threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, +}; + +#[napi_derive::napi] +pub fn run( + args: Vec, + bin_name: Option, + callback: Arc>, +) -> Result<()> { + // we need to run in a separate thread so Node.js consumers + // can do work while `tauri dev` is running. + std::thread::spawn(move || { + let res = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + tauri_cli::try_run(args, bin_name).inspect_err(|e| eprintln!("{e:#}")) + })) { + Ok(t) => t, + Err(_) => { + return callback.call( + Err(Error::new( + Status::GenericFailure, + "Tauri CLI unexpected panic", + )), + ThreadsafeFunctionCallMode::Blocking, + ); + } + }; + + match res { + Ok(_) => callback.call(Ok(true), ThreadsafeFunctionCallMode::Blocking), + Err(e) => callback.call( + Err(Error::new(Status::GenericFailure, format!("{e:#}"))), + ThreadsafeFunctionCallMode::Blocking, + ), + } + }); + + Ok(()) +} + +#[napi_derive::napi] +pub fn log_error(error: String) { + log::error!("{}", error); +} diff --git a/packages/cli/tauri.js b/packages/cli/tauri.js new file mode 100644 index 0000000..81f16da --- /dev/null +++ b/packages/cli/tauri.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node + +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +const cli = require('./main') +const path = require('path') + +const [bin, script, ...args] = process.argv +const binStem = path.parse(bin).name.toLowerCase() + +// We want to make a helpful binary name for the underlying CLI helper, if we +// can successfully detect what command likely started the execution. +let binName + +// deno run -A npm:@tauri-apps/cli or deno task tauri +if (globalThis.navigator?.userAgent?.includes('Deno')) { + binName = bin +} +// Even if started by a package manager, the binary will be NodeJS. +// Some distribution still use "nodejs" as the binary name. +else if (/(nodejs|node|bun|electron)-?[0-9]*$/.test(binStem)) { + const managerStem = process.env.npm_execpath + ? path.parse(process.env.npm_execpath).name.toLowerCase() + : null + if (managerStem) { + let manager + switch (managerStem) { + // Only supported package manager that has a different filename is npm. + case 'npm-cli': + manager = 'npm' + break + + // Yarn, pnpm, and bun have the same stem name as their bin. + // We assume all unknown package managers do as well. + default: + manager = managerStem + break + } + + binName = `${manager} run ${process.env.npm_lifecycle_event}` + } else { + // Assume running NodeJS if we didn't detect a manager from the env. + // We normalize the path to prevent the script's absolute path being used. + const scriptNormal = path.normalize(path.relative(process.cwd(), script)) + binName = `${binStem} ${scriptNormal}` + } +} else { + // We don't know what started it, assume it's already stripped. + args.unshift(bin) +} + +cli.run(args, binName).catch((err) => { + cli.logError(err.message) + process.exit(1) +}) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..30150f8 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,41 @@ +packages: + - packages/api + - packages/api-e2e + - packages/cli + - crates/tauri-schema-worker + - examples/api + - examples/resources + - examples/file-associations + +allowBuilds: + # appium's postinstall only auto-installs drivers for global installs; ours + # are regular devDependencies of packages/api-e2e, which appium picks up on + # its own, so there is nothing for it to do + appium: false + # transitive deps of @wdio/utils; their postinstall downloads browser drivers + # the Tauri suite never uses (it drives the app via tauri-driver) + edgedriver: false + esbuild: true + geckodriver: false + sharp: true + workerd: true + +overrides: + devalue@<5.9.1: ^5.9.1 + morgan@<1.12.0: ^1.12.0 + serialize-javascript@<7.0.5: ^7.0.5 + +auditConfig: + ignoreGhsas: + # extract-zip symlink path traversal; no patched release exists (latest is + # still 2.0.1). Reached via @wdio/utils > @puppeteer/browsers, which only + # unpacks the browser drivers we never download (see allowBuilds above) + - GHSA-jmr9-qjv8-65gv + - GHSA-7pqw-9j4j-h8q3 + +minimumReleaseAgeExclude: + - '@crabnebula/test-runner-backend-darwin-arm64@0.3.0' + - '@crabnebula/test-runner-backend-darwin-x64@0.3.0' + - '@crabnebula/test-runner-backend-linux-x64-gnu@0.3.0' + - '@crabnebula/test-runner-backend-win32-x64-msvc@0.3.0' + - '@crabnebula/test-runner-backend@0.3.0' diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..fd9b82d --- /dev/null +++ b/renovate.json @@ -0,0 +1,60 @@ +{ + "extends": ["config:recommended"], + "baseBranches": ["dev"], + "labels": ["type: chore"], + "enabledManagers": ["cargo", "npm"], + "rangeStrategy": "replace", + "packageRules": [ + { + "matchPackageNames": ["*"], + "semanticCommitType": "chore", + "minimumReleaseAge": "3 days" + }, + { + "description": "Disable node/pnpm version updates", + "matchPackageNames": ["node", "pnpm"], + "matchDepTypes": ["engines", "packageManager"], + "enabled": false + }, + { + "description": "Disable oxc_* crates because of MSRV and PR spam", + "groupName": "oxc crates", + "matchPackageNames": ["oxc_*"], + "enabled": false + }, + { + "description": "Group windows-rs / webview2-com crates", + "groupName": "windows-rs and webview2 crates", + "matchSourceUrls": [ + "https://github.com/microsoft/windows-rs", + "https://github.com/wravery/webview2-rs" + ] + }, + { + "description": "Group worker-rs crates", + "groupName": "worker-rs crates", + "matchSourceUrls": ["https://github.com/cloudflare/workers-rs"] + }, + { + "description": "Bump CLI packages", + "matchFileNames": [ + "crates/tauri-cli/Cargo.toml", + "packages/cli/Cargo.toml", + "packages/cli/package.json" + ], + "rangeStrategy": "auto" + }, + { + "description": "Group napi-rs packages", + "groupName": "napi-rs packages", + "matchPackageNames": [ + "napi", + "napi-build", + "napi-derive", + "@napi-rs/cli" + ], + "rangeStrategy": "bump" + } + ], + "postUpdateOptions": ["pnpmDedupe"] +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..873136b --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,15 @@ +max_width = 100 +hard_tabs = false +tab_spaces = 2 +newline_style = "Unix" +use_small_heuristics = "Default" +reorder_imports = true +reorder_modules = true +remove_nested_parens = true +edition = "2024" +merge_derives = true +use_try_shorthand = false +use_field_init_shorthand = false +force_explicit_abi = true +# normalize_comments = true +# wrap_comments = true diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml new file mode 100644 index 0000000..233d085 --- /dev/null +++ b/supply-chain/audits.toml @@ -0,0 +1,874 @@ + +# cargo-vet audits file + +[audits] + +[[trusted.aead]] +criteria = "safe-to-deploy" +user-id = 267 # Tony Arcieri (tarcieri) +start = "2020-06-05" +end = "2025-05-08" + +[[trusted.aes]] +criteria = "safe-to-deploy" +user-id = 267 # Tony Arcieri (tarcieri) +start = "2020-06-05" +end = "2025-05-08" + +[[trusted.aes-gcm]] +criteria = "safe-to-deploy" +user-id = 267 # Tony Arcieri (tarcieri) +start = "2019-08-16" +end = "2025-05-08" + +[[trusted.aho-corasick]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-03-28" +end = "2025-03-06" + +[[trusted.anyhow]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-10-05" +end = "2025-03-06" + +[[trusted.autocfg]] +criteria = "safe-to-deploy" +user-id = 539 # Josh Stone (cuviper) +start = "2019-05-22" +end = "2025-05-08" + +[[trusted.backtrace]] +criteria = "safe-to-deploy" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2023-06-29" +end = "2025-03-06" + +[[trusted.byteorder]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-06-09" +end = "2025-03-06" + +[[trusted.bytes]] +criteria = "safe-to-deploy" +user-id = 359 # Sean McArthur (seanmonstar) +start = "2019-11-27" +end = "2025-03-06" + +[[trusted.bytes]] +criteria = "safe-to-deploy" +user-id = 6741 # Alice Ryhl (Darksonn) +start = "2021-01-11" +end = "2025-03-06" + +[[trusted.cargo-platform]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-09-27" +end = "2025-03-06" + +[[trusted.cc]] +criteria = "safe-to-deploy" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2024-02-20" +end = "2025-03-06" + +[[trusted.cipher]] +criteria = "safe-to-deploy" +user-id = 267 # Tony Arcieri (tarcieri) +start = "2020-10-15" +end = "2025-05-08" + +[[trusted.cpufeatures]] +criteria = "safe-to-deploy" +user-id = 267 # Tony Arcieri (tarcieri) +start = "2021-04-26" +end = "2025-05-08" + +[[trusted.ctr]] +criteria = "safe-to-deploy" +user-id = 267 # Tony Arcieri (tarcieri) +start = "2020-06-06" +end = "2025-05-08" + +[[trusted.digest]] +criteria = "safe-to-deploy" +user-id = 267 # Tony Arcieri (tarcieri) +start = "2020-06-10" +end = "2025-05-08" + +[[trusted.dtoa]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-05-02" +end = "2025-03-06" + +[[trusted.dyn-clone]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-12-23" +end = "2025-03-06" + +[[trusted.flate2]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-03-14" +end = "2025-03-06" + +[[trusted.ghash]] +criteria = "safe-to-deploy" +user-id = 267 # Tony Arcieri (tarcieri) +start = "2019-09-19" +end = "2025-05-08" + +[[trusted.h2]] +criteria = "safe-to-deploy" +user-id = 359 # Sean McArthur (seanmonstar) +start = "2019-03-13" +end = "2025-03-06" + +[[trusted.hashbrown]] +criteria = "safe-to-deploy" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2019-04-02" +end = "2025-03-06" + +[[trusted.http]] +criteria = "safe-to-deploy" +user-id = 359 # Sean McArthur (seanmonstar) +start = "2019-04-05" +end = "2025-03-06" + +[[trusted.http-body]] +criteria = "safe-to-deploy" +user-id = 359 # Sean McArthur (seanmonstar) +start = "2019-10-01" +end = "2025-03-06" + +[[trusted.http-body-util]] +criteria = "safe-to-deploy" +user-id = 359 # Sean McArthur (seanmonstar) +start = "2022-10-25" +end = "2025-05-08" + +[[trusted.httparse]] +criteria = "safe-to-deploy" +user-id = 359 # Sean McArthur (seanmonstar) +start = "2019-07-03" +end = "2025-03-06" + +[[trusted.hyper]] +criteria = "safe-to-deploy" +user-id = 359 # Sean McArthur (seanmonstar) +start = "2019-03-01" +end = "2025-03-06" + +[[trusted.hyper-tls]] +criteria = "safe-to-deploy" +user-id = 359 # Sean McArthur (seanmonstar) +start = "2019-03-19" +end = "2025-03-06" + +[[trusted.hyper-util]] +criteria = "safe-to-deploy" +user-id = 359 # Sean McArthur (seanmonstar) +start = "2022-01-15" +end = "2025-05-08" + +[[trusted.indexmap]] +criteria = "safe-to-deploy" +user-id = 539 # Josh Stone (cuviper) +start = "2020-01-15" +end = "2025-03-06" + +[[trusted.itoa]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-05-02" +end = "2025-03-06" + +[[trusted.javascriptcore-rs]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2021-10-04" +end = "2025-05-08" + +[[trusted.javascriptcore-rs-sys]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2021-10-04" +end = "2025-05-08" + +[[trusted.js-sys]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-03-04" +end = "2025-03-06" + +[[trusted.libappindicator]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2021-10-11" +end = "2025-05-08" + +[[trusted.libappindicator-sys]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2021-10-11" +end = "2025-05-08" + +[[trusted.libc]] +criteria = "safe-to-deploy" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2021-01-27" +end = "2025-03-06" + +[[trusted.libc]] +criteria = "safe-to-deploy" +user-id = 51017 # Yuki Okushi (JohnTitor) +start = "2020-03-17" +end = "2025-05-08" + +[[trusted.libm]] +criteria = "safe-to-deploy" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2022-02-06" +end = "2025-03-06" + +[[trusted.linux-raw-sys]] +criteria = "safe-to-deploy" +user-id = 6825 # Dan Gohman (sunfishcode) +start = "2021-06-12" +end = "2025-03-06" + +[[trusted.lock_api]] +criteria = "safe-to-deploy" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2019-05-04" +end = "2025-03-06" + +[[trusted.loom]] +criteria = "safe-to-deploy" +user-id = 6741 # Alice Ryhl (Darksonn) +start = "2021-04-12" +end = "2025-03-06" + +[[trusted.memchr]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-07-07" +end = "2025-03-06" + +[[trusted.mime]] +criteria = "safe-to-deploy" +user-id = 359 # Sean McArthur (seanmonstar) +start = "2019-09-09" +end = "2025-03-06" + +[[trusted.mio]] +criteria = "safe-to-deploy" +user-id = 10 # Carl Lerche (carllerche) +start = "2019-05-15" +end = "2025-03-06" + +[[trusted.muda]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2022-12-06" +end = "2025-05-08" + +[[trusted.new_debug_unreachable]] +criteria = "safe-to-deploy" +user-id = 2017 # Matt Brubeck (mbrubeck) +start = "2019-02-26" +end = "2025-05-08" + +[[trusted.num_cpus]] +criteria = "safe-to-deploy" +user-id = 359 # Sean McArthur (seanmonstar) +start = "2019-06-10" +end = "2025-03-06" + +[[trusted.openssl-src]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-02-25" +end = "2025-03-06" + +[[trusted.parking_lot]] +criteria = "safe-to-deploy" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2019-05-04" +end = "2025-03-06" + +[[trusted.parking_lot_core]] +criteria = "safe-to-deploy" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2019-05-04" +end = "2025-03-06" + +[[trusted.paste]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-03-19" +end = "2025-05-08" + +[[trusted.phf]] +criteria = "safe-to-deploy" +user-id = 51017 # Yuki Okushi (JohnTitor) +start = "2021-06-17" +end = "2025-05-08" + +[[trusted.phf_codegen]] +criteria = "safe-to-deploy" +user-id = 51017 # Yuki Okushi (JohnTitor) +start = "2021-06-17" +end = "2025-05-08" + +[[trusted.phf_generator]] +criteria = "safe-to-deploy" +user-id = 51017 # Yuki Okushi (JohnTitor) +start = "2021-06-17" +end = "2025-05-08" + +[[trusted.phf_macros]] +criteria = "safe-to-deploy" +user-id = 51017 # Yuki Okushi (JohnTitor) +start = "2021-06-17" +end = "2025-05-08" + +[[trusted.phf_shared]] +criteria = "safe-to-deploy" +user-id = 51017 # Yuki Okushi (JohnTitor) +start = "2021-06-17" +end = "2025-05-08" + +[[trusted.polyval]] +criteria = "safe-to-deploy" +user-id = 267 # Tony Arcieri (tarcieri) +start = "2019-08-13" +end = "2025-05-08" + +[[trusted.proc-macro-hack]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-04-16" +end = "2025-03-06" + +[[trusted.proc-macro2]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-04-23" +end = "2025-03-06" + +[[trusted.quickcheck]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-05-13" +end = "2025-03-06" + +[[trusted.regex]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-02-27" +end = "2025-03-06" + +[[trusted.regex-automata]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-02-25" +end = "2025-03-06" + +[[trusted.regex-syntax]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-03-30" +end = "2025-03-06" + +[[trusted.reqwest]] +criteria = "safe-to-deploy" +user-id = 359 # Sean McArthur (seanmonstar) +start = "2019-03-04" +end = "2025-03-06" + +[[trusted.rustc-demangle]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-04-12" +end = "2025-05-08" + +[[trusted.rustix]] +criteria = "safe-to-deploy" +user-id = 6825 # Dan Gohman (sunfishcode) +start = "2021-10-29" +end = "2025-03-06" + +[[trusted.rustversion]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-07-08" +end = "2025-05-08" + +[[trusted.ryu]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-05-02" +end = "2025-03-06" + +[[trusted.same-file]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-07-16" +end = "2025-03-06" + +[[trusted.scoped-tls]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-02-26" +end = "2025-03-06" + +[[trusted.scopeguard]] +criteria = "safe-to-deploy" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2020-02-16" +end = "2025-03-06" + +[[trusted.semver]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2021-05-25" +end = "2025-03-06" + +[[trusted.serde]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-03-01" +end = "2025-03-06" + +[[trusted.serde_derive]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-03-01" +end = "2025-05-08" + +[[trusted.serde_derive_internals]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-09-08" +end = "2025-03-06" + +[[trusted.serde_json]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-02-28" +end = "2025-03-06" + +[[trusted.serde_repr]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-04-26" +end = "2025-03-06" + +[[trusted.serde_spanned]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2023-01-20" +end = "2025-03-06" + +[[trusted.serialize-to-javascript]] +criteria = "safe-to-deploy" +user-id = 28029 # chip (chippers) +start = "2022-02-08" +end = "2025-05-08" + +[[trusted.serialize-to-javascript-impl]] +criteria = "safe-to-deploy" +user-id = 28029 # chip (chippers) +start = "2022-02-08" +end = "2025-05-08" + +[[trusted.sha2]] +criteria = "safe-to-deploy" +user-id = 267 # Tony Arcieri (tarcieri) +start = "2020-05-24" +end = "2025-05-08" + +[[trusted.slab]] +criteria = "safe-to-deploy" +user-id = 6741 # Alice Ryhl (Darksonn) +start = "2021-10-13" +end = "2025-03-06" + +[[trusted.smallvec]] +criteria = "safe-to-deploy" +user-id = 2017 # Matt Brubeck (mbrubeck) +start = "2019-10-28" +end = "2025-03-06" + +[[trusted.socket2]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-05-06" +end = "2025-03-06" + +[[trusted.syn]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-03-01" +end = "2025-03-06" + +[[trusted.tao]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2021-04-30" +end = "2025-05-08" + +[[trusted.tao-macros]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2023-01-11" +end = "2025-05-08" + +[[trusted.target-lexicon]] +criteria = "safe-to-deploy" +user-id = 6825 # Dan Gohman (sunfishcode) +start = "2019-03-06" +end = "2025-03-06" + +[[trusted.tauri]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2020-07-20" +end = "2025-05-08" + +[[trusted.tauri-build]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2021-04-14" +end = "2025-05-08" + +[[trusted.tauri-codegen]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2021-04-14" +end = "2025-05-08" + +[[trusted.tauri-macros]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2021-04-14" +end = "2025-05-08" + +[[trusted.tauri-plugin]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2024-02-03" +end = "2025-05-08" + +[[trusted.tauri-runtime]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2021-05-10" +end = "2025-05-08" + +[[trusted.tauri-runtime-wry]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2021-05-10" +end = "2025-05-08" + +[[trusted.tauri-utils]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2021-04-14" +end = "2025-05-08" + +[[trusted.tauri-winres]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2023-01-19" +end = "2025-05-08" + +[[trusted.thiserror]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-10-09" +end = "2025-03-06" + +[[trusted.thiserror-impl]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-10-09" +end = "2025-03-06" + +[[trusted.thread_local]] +criteria = "safe-to-deploy" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2019-09-07" +end = "2025-03-06" + +[[trusted.tokio]] +criteria = "safe-to-deploy" +user-id = 6741 # Alice Ryhl (Darksonn) +start = "2020-12-25" +end = "2025-03-06" + +[[trusted.tokio-macros]] +criteria = "safe-to-deploy" +user-id = 6741 # Alice Ryhl (Darksonn) +start = "2020-10-26" +end = "2025-03-06" + +[[trusted.tokio-macros]] +criteria = "safe-to-deploy" +user-id = 10 # Carl Lerche (carllerche) +start = "2019-04-24" +end = "2025-03-06" + +[[trusted.tokio-util]] +criteria = "safe-to-deploy" +user-id = 6741 # Alice Ryhl (Darksonn) +start = "2021-01-12" +end = "2025-03-06" + +[[trusted.toml]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-05-16" +end = "2025-03-06" + +[[trusted.toml]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2022-12-14" +end = "2025-03-06" + +[[trusted.toml_edit]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2021-09-13" +end = "2025-03-06" + +[[trusted.tower]] +criteria = "safe-to-deploy" +user-id = 10 # Carl Lerche (carllerche) +start = "2019-04-27" +end = "2025-05-08" + +[[trusted.tower-layer]] +criteria = "safe-to-deploy" +user-id = 10 # Carl Lerche (carllerche) +start = "2019-04-27" +end = "2025-05-08" + +[[trusted.tray-icon]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2022-12-01" +end = "2025-05-08" + +[[trusted.ucd-trie]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-07-21" +end = "2025-03-06" + +[[trusted.walkdir]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-06-09" +end = "2025-03-06" + +[[trusted.wasi]] +criteria = "safe-to-deploy" +user-id = 6825 # Dan Gohman (sunfishcode) +start = "2019-07-22" +end = "2025-03-06" + +[[trusted.wasi]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2020-06-03" +end = "2025-03-06" + +[[trusted.wasm-bindgen]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-03-04" +end = "2025-03-06" + +[[trusted.wasm-bindgen-backend]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-03-04" +end = "2025-03-06" + +[[trusted.wasm-bindgen-futures]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-03-04" +end = "2025-03-06" + +[[trusted.wasm-bindgen-macro]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-03-04" +end = "2025-03-06" + +[[trusted.wasm-bindgen-macro-support]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-03-04" +end = "2025-03-06" + +[[trusted.wasm-bindgen-shared]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-03-04" +end = "2025-03-06" + +[[trusted.web-sys]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-03-04" +end = "2025-03-06" + +[[trusted.webkit2gtk]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2021-10-05" +end = "2025-05-08" + +[[trusted.webkit2gtk-sys]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2021-10-05" +end = "2025-05-08" + +[[trusted.winapi-util]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2020-01-11" +end = "2025-03-06" + +[[trusted.window-vibrancy]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2022-03-05" +end = "2025-05-08" + +[[trusted.windows]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-01-15" +end = "2025-03-06" + +[[trusted.windows-core]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-11-15" +end = "2025-03-06" + +[[trusted.windows-implement]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-01-27" +end = "2025-03-06" + +[[trusted.windows-interface]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-02-18" +end = "2025-03-06" + +[[trusted.windows-result]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2024-02-02" +end = "2025-03-06" + +[[trusted.windows-sys]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-11-15" +end = "2025-03-06" + +[[trusted.windows-targets]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-09-09" +end = "2025-03-06" + +[[trusted.windows-version]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2023-03-07" +end = "2025-03-06" + +[[trusted.windows_aarch64_gnullvm]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-09-01" +end = "2025-03-06" + +[[trusted.windows_aarch64_msvc]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-11-05" +end = "2025-03-06" + +[[trusted.windows_i686_gnu]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-10-28" +end = "2025-03-06" + +[[trusted.windows_i686_gnullvm]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2024-04-02" +end = "2025-05-08" + +[[trusted.windows_i686_msvc]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-10-27" +end = "2025-03-06" + +[[trusted.windows_x86_64_gnu]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-10-28" +end = "2025-03-06" + +[[trusted.windows_x86_64_gnullvm]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-09-01" +end = "2025-03-06" + +[[trusted.windows_x86_64_msvc]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-10-27" +end = "2025-03-06" + +[[trusted.winnow]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2023-02-22" +end = "2025-03-06" + +[[trusted.wry]] +criteria = "safe-to-deploy" +user-id = 95389 # tauri (tauri-bot) +start = "2021-03-11" +end = "2025-05-08" + +[[trusted.zeroize]] +criteria = "safe-to-deploy" +user-id = 267 # Tony Arcieri (tarcieri) +start = "2021-11-05" +end = "2025-05-08" diff --git a/supply-chain/config.toml b/supply-chain/config.toml new file mode 100644 index 0000000..ef23ca6 --- /dev/null +++ b/supply-chain/config.toml @@ -0,0 +1,955 @@ + +# cargo-vet config file + +[cargo-vet] +version = "0.9" + +[imports.bytecode-alliance] +url = "https://raw.githubusercontent.com/bytecodealliance/wasmtime/main/supply-chain/audits.toml" + +[imports.embark-studios] +url = "https://raw.githubusercontent.com/EmbarkStudios/rust-ecosystem/main/audits.toml" + +[imports.google] +url = "https://raw.githubusercontent.com/google/supply-chain/main/audits.toml" + +[imports.isrg] +url = "https://raw.githubusercontent.com/divviup/libprio-rs/main/supply-chain/audits.toml" + +[imports.mozilla] +url = "https://raw.githubusercontent.com/mozilla/supply-chain/main/audits.toml" + +[imports.zcash] +url = "https://raw.githubusercontent.com/zcash/rust-ecosystem/main/supply-chain/audits.toml" + +[policy.tauri] +audit-as-crates-io = true + +[policy.tauri-build] +audit-as-crates-io = true + +[policy.tauri-codegen] +audit-as-crates-io = true + +[policy.tauri-macros] +audit-as-crates-io = true + +[policy.tauri-plugin] +audit-as-crates-io = true + +[policy.tauri-runtime] +audit-as-crates-io = true + +[policy.tauri-runtime-wry] +audit-as-crates-io = true + +[policy.tauri-utils] +audit-as-crates-io = true + +[[exemptions.addr2line]] +version = "0.21.0" +criteria = "safe-to-deploy" + +[[exemptions.alloc-no-stdlib]] +version = "2.0.4" +criteria = "safe-to-deploy" + +[[exemptions.alloc-stdlib]] +version = "0.2.2" +criteria = "safe-to-deploy" + +[[exemptions.android-tzdata]] +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.as-raw-xcb-connection]] +version = "1.0.1" +criteria = "safe-to-deploy" + +[[exemptions.atk]] +version = "0.18.0" +criteria = "safe-to-deploy" + +[[exemptions.atk-sys]] +version = "0.18.0" +criteria = "safe-to-deploy" + +[[exemptions.base64]] +version = "0.21.7" +criteria = "safe-to-deploy" + +[[exemptions.bitflags]] +version = "1.3.2" +criteria = "safe-to-deploy" + +[[exemptions.block]] +version = "0.1.6" +criteria = "safe-to-deploy" + +[[exemptions.brotli]] +version = "3.4.0" +criteria = "safe-to-deploy" + +[[exemptions.brotli-decompressor]] +version = "2.5.1" +criteria = "safe-to-deploy" + +[[exemptions.bytemuck]] +version = "1.14.3" +criteria = "safe-to-deploy" + +[[exemptions.bytemuck_derive]] +version = "1.5.0" +criteria = "safe-to-deploy" + +[[exemptions.cairo-rs]] +version = "0.18.5" +criteria = "safe-to-deploy" + +[[exemptions.cairo-sys-rs]] +version = "0.18.2" +criteria = "safe-to-deploy" + +[[exemptions.camino]] +version = "1.1.6" +criteria = "safe-to-deploy" + +[[exemptions.cargo-platform]] +version = "0.1.7" +criteria = "safe-to-deploy" + +[[exemptions.cargo_toml]] +version = "0.17.2" +criteria = "safe-to-deploy" + +[[exemptions.cesu8]] +version = "1.1.0" +criteria = "safe-to-deploy" + +[[exemptions.cfb]] +version = "0.7.3" +criteria = "safe-to-deploy" + +[[exemptions.cfg_aliases]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.chrono]] +version = "0.4.34" +criteria = "safe-to-deploy" + +[[exemptions.cocoa]] +version = "0.25.0" +criteria = "safe-to-deploy" + +[[exemptions.cocoa-foundation]] +version = "0.1.2" +criteria = "safe-to-deploy" + +[[exemptions.combine]] +version = "4.6.6" +criteria = "safe-to-deploy" + +[[exemptions.console]] +version = "0.15.8" +criteria = "safe-to-run" + +[[exemptions.core-foundation]] +version = "0.9.4" +criteria = "safe-to-deploy" + +[[exemptions.core-graphics-types]] +version = "0.1.3" +criteria = "safe-to-deploy" + +[[exemptions.crc32fast]] +version = "1.4.0" +criteria = "safe-to-deploy" + +[[exemptions.crossbeam-channel]] +version = "0.5.12" +criteria = "safe-to-deploy" + +[[exemptions.crossbeam-utils]] +version = "0.8.19" +criteria = "safe-to-deploy" + +[[exemptions.cssparser]] +version = "0.27.2" +criteria = "safe-to-deploy" + +[[exemptions.ctor]] +version = "0.2.7" +criteria = "safe-to-deploy" + +[[exemptions.ctr]] +version = "0.9.2" +criteria = "safe-to-deploy" + +[[exemptions.darling]] +version = "0.20.8" +criteria = "safe-to-deploy" + +[[exemptions.darling_core]] +version = "0.20.8" +criteria = "safe-to-deploy" + +[[exemptions.darling_macro]] +version = "0.20.8" +criteria = "safe-to-deploy" + +[[exemptions.data-url]] +version = "0.3.1" +criteria = "safe-to-deploy" + +[[exemptions.deranged]] +version = "0.3.11" +criteria = "safe-to-deploy" + +[[exemptions.dirs-sys-next]] +version = "0.1.2" +criteria = "safe-to-deploy" + +[[exemptions.dispatch]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.dlib]] +version = "0.5.2" +criteria = "safe-to-deploy" + +[[exemptions.dlopen2]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.dlopen2_derive]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.downcast-rs]] +version = "1.2.0" +criteria = "safe-to-deploy" + +[[exemptions.drm]] +version = "0.11.1" +criteria = "safe-to-deploy" + +[[exemptions.drm-ffi]] +version = "0.7.1" +criteria = "safe-to-deploy" + +[[exemptions.drm-fourcc]] +version = "2.2.0" +criteria = "safe-to-deploy" + +[[exemptions.drm-sys]] +version = "0.6.1" +criteria = "safe-to-deploy" + +[[exemptions.dtoa-short]] +version = "0.3.4" +criteria = "safe-to-deploy" + +[[exemptions.dunce]] +version = "1.0.4" +criteria = "safe-to-deploy" + +[[exemptions.embed-resource]] +version = "2.4.1" +criteria = "safe-to-deploy" + +[[exemptions.embed_plist]] +version = "1.2.2" +criteria = "safe-to-deploy" + +[[exemptions.encode_unicode]] +version = "0.3.6" +criteria = "safe-to-run" + +[[exemptions.fdeflate]] +version = "0.3.4" +criteria = "safe-to-deploy" + +[[exemptions.field-offset]] +version = "0.3.6" +criteria = "safe-to-deploy" + +[[exemptions.flate2]] +version = "1.0.28" +criteria = "safe-to-deploy" + +[[exemptions.futf]] +version = "0.1.5" +criteria = "safe-to-deploy" + +[[exemptions.futures-executor]] +version = "0.3.30" +criteria = "safe-to-deploy" + +[[exemptions.futures-io]] +version = "0.3.30" +criteria = "safe-to-deploy" + +[[exemptions.futures-macro]] +version = "0.3.30" +criteria = "safe-to-deploy" + +[[exemptions.futures-sink]] +version = "0.3.30" +criteria = "safe-to-deploy" + +[[exemptions.futures-task]] +version = "0.3.30" +criteria = "safe-to-deploy" + +[[exemptions.futures-util]] +version = "0.3.30" +criteria = "safe-to-deploy" + +[[exemptions.gdk]] +version = "0.18.0" +criteria = "safe-to-deploy" + +[[exemptions.gdk-pixbuf]] +version = "0.18.5" +criteria = "safe-to-deploy" + +[[exemptions.gdk-pixbuf-sys]] +version = "0.18.0" +criteria = "safe-to-deploy" + +[[exemptions.gdk-sys]] +version = "0.18.0" +criteria = "safe-to-deploy" + +[[exemptions.gdkwayland-sys]] +version = "0.18.0" +criteria = "safe-to-deploy" + +[[exemptions.gdkx11]] +version = "0.18.0" +criteria = "safe-to-deploy" + +[[exemptions.gdkx11-sys]] +version = "0.18.0" +criteria = "safe-to-deploy" + +[[exemptions.generator]] +version = "0.7.6" +criteria = "safe-to-deploy" + +[[exemptions.generic-array]] +version = "0.14.7" +criteria = "safe-to-deploy" + +[[exemptions.gethostname]] +version = "0.4.3" +criteria = "safe-to-deploy" + +[[exemptions.getrandom]] +version = "0.1.16" +criteria = "safe-to-deploy" + +[[exemptions.getrandom]] +version = "0.2.12" +criteria = "safe-to-deploy" + +[[exemptions.gimli]] +version = "0.28.1" +criteria = "safe-to-deploy" + +[[exemptions.gio]] +version = "0.18.4" +criteria = "safe-to-deploy" + +[[exemptions.gio-sys]] +version = "0.18.1" +criteria = "safe-to-deploy" + +[[exemptions.glib]] +version = "0.18.5" +criteria = "safe-to-deploy" + +[[exemptions.glib-macros]] +version = "0.18.5" +criteria = "safe-to-deploy" + +[[exemptions.glib-sys]] +version = "0.18.1" +criteria = "safe-to-deploy" + +[[exemptions.gobject-sys]] +version = "0.18.0" +criteria = "safe-to-deploy" + +[[exemptions.gtk]] +version = "0.18.1" +criteria = "safe-to-deploy" + +[[exemptions.gtk-sys]] +version = "0.18.0" +criteria = "safe-to-deploy" + +[[exemptions.gtk3-macros]] +version = "0.18.0" +criteria = "safe-to-deploy" + +[[exemptions.hermit-abi]] +version = "0.3.8" +criteria = "safe-to-deploy" + +[[exemptions.html5ever]] +version = "0.26.0" +criteria = "safe-to-deploy" + +[[exemptions.http-range]] +version = "0.1.5" +criteria = "safe-to-deploy" + +[[exemptions.hyper-rustls]] +version = "0.24.2" +criteria = "safe-to-deploy" + +[[exemptions.iana-time-zone]] +version = "0.1.60" +criteria = "safe-to-deploy" + +[[exemptions.ico]] +version = "0.3.0" +criteria = "safe-to-deploy" + +[[exemptions.image]] +version = "0.24.9" +criteria = "safe-to-deploy" + +[[exemptions.infer]] +version = "0.15.0" +criteria = "safe-to-deploy" + +[[exemptions.insta]] +version = "1.35.1" +criteria = "safe-to-run" + +[[exemptions.instant]] +version = "0.1.12" +criteria = "safe-to-deploy" + +[[exemptions.ipnet]] +version = "2.9.0" +criteria = "safe-to-deploy" + +[[exemptions.jni-sys]] +version = "0.3.0" +criteria = "safe-to-deploy" + +[[exemptions.json-patch]] +version = "1.2.0" +criteria = "safe-to-deploy" + +[[exemptions.json5]] +version = "0.4.1" +criteria = "safe-to-deploy" + +[[exemptions.keyboard-types]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.kuchikiki]] +version = "0.8.2" +criteria = "safe-to-deploy" + +[[exemptions.libloading]] +version = "0.7.4" +criteria = "safe-to-deploy" + +[[exemptions.libloading]] +version = "0.8.1" +criteria = "safe-to-deploy" + +[[exemptions.libredox]] +version = "0.0.1" +criteria = "safe-to-deploy" + +[[exemptions.libxdo]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.libxdo-sys]] +version = "0.11.0" +criteria = "safe-to-deploy" + +[[exemptions.mac]] +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.markup5ever]] +version = "0.11.0" +criteria = "safe-to-deploy" + +[[exemptions.memmap2]] +version = "0.9.4" +criteria = "safe-to-deploy" + +[[exemptions.memoffset]] +version = "0.9.0" +criteria = "safe-to-deploy" + +[[exemptions.miniz_oxide]] +version = "0.7.2" +criteria = "safe-to-deploy" + +[[exemptions.mio]] +version = "0.8.11" +criteria = "safe-to-deploy" + +[[exemptions.ndk]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.ndk-sys]] +version = "0.4.1+23.1.7779620" +criteria = "safe-to-deploy" + +[[exemptions.nodrop]] +version = "0.1.14" +criteria = "safe-to-deploy" + +[[exemptions.num-conv]] +version = "0.1.0" +criteria = "safe-to-deploy" + +[[exemptions.objc]] +version = "0.2.7" +criteria = "safe-to-deploy" + +[[exemptions.objc_exception]] +version = "0.1.2" +criteria = "safe-to-deploy" + +[[exemptions.objc_id]] +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.object]] +version = "0.32.2" +criteria = "safe-to-deploy" + +[[exemptions.once_cell]] +version = "1.19.0" +criteria = "safe-to-deploy" + +[[exemptions.openssl]] +version = "0.10.64" +criteria = "safe-to-deploy" + +[[exemptions.openssl-sys]] +version = "0.9.101" +criteria = "safe-to-deploy" + +[[exemptions.pango]] +version = "0.18.3" +criteria = "safe-to-deploy" + +[[exemptions.pango-sys]] +version = "0.18.0" +criteria = "safe-to-deploy" + +[[exemptions.pest]] +version = "2.7.7" +criteria = "safe-to-deploy" + +[[exemptions.pest_derive]] +version = "2.7.7" +criteria = "safe-to-deploy" + +[[exemptions.pest_generator]] +version = "2.7.7" +criteria = "safe-to-deploy" + +[[exemptions.pest_meta]] +version = "2.7.7" +criteria = "safe-to-deploy" + +[[exemptions.phf]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.phf_codegen]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.phf_generator]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.phf_macros]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.phf_shared]] +version = "0.8.0" +criteria = "safe-to-deploy" + +[[exemptions.pkg-config]] +version = "0.3.30" +criteria = "safe-to-deploy" + +[[exemptions.plist]] +version = "1.6.0" +criteria = "safe-to-deploy" + +[[exemptions.png]] +version = "0.17.13" +criteria = "safe-to-deploy" + +[[exemptions.powerfmt]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.ppv-lite86]] +version = "0.2.17" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro-crate]] +version = "1.3.1" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro-crate]] +version = "2.0.2" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro-error]] +version = "1.0.4" +criteria = "safe-to-deploy" + +[[exemptions.proptest]] +version = "1.4.0" +criteria = "safe-to-run" + +[[exemptions.quick-error]] +version = "1.2.3" +criteria = "safe-to-run" + +[[exemptions.quick-xml]] +version = "0.31.0" +criteria = "safe-to-deploy" + +[[exemptions.rand]] +version = "0.7.3" +criteria = "safe-to-deploy" + +[[exemptions.rand]] +version = "0.8.5" +criteria = "safe-to-deploy" + +[[exemptions.rand_chacha]] +version = "0.2.2" +criteria = "safe-to-deploy" + +[[exemptions.rand_core]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.rand_hc]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.rand_pcg]] +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.redox_syscall]] +version = "0.4.1" +criteria = "safe-to-deploy" + +[[exemptions.redox_users]] +version = "0.4.4" +criteria = "safe-to-deploy" + +[[exemptions.ring]] +version = "0.17.8" +criteria = "safe-to-deploy" + +[[exemptions.rustls]] +version = "0.21.10" +criteria = "safe-to-deploy" + +[[exemptions.rustls-pemfile]] +version = "1.0.4" +criteria = "safe-to-deploy" + +[[exemptions.rustls-webpki]] +version = "0.101.7" +criteria = "safe-to-deploy" + +[[exemptions.rusty-fork]] +version = "0.3.0" +criteria = "safe-to-run" + +[[exemptions.safemem]] +version = "0.3.3" +criteria = "safe-to-deploy" + +[[exemptions.schannel]] +version = "0.1.23" +criteria = "safe-to-deploy" + +[[exemptions.schemars]] +version = "0.8.16" +criteria = "safe-to-deploy" + +[[exemptions.schemars_derive]] +version = "0.8.16" +criteria = "safe-to-deploy" + +[[exemptions.sct]] +version = "0.7.1" +criteria = "safe-to-deploy" + +[[exemptions.security-framework]] +version = "2.9.2" +criteria = "safe-to-deploy" + +[[exemptions.security-framework-sys]] +version = "2.9.1" +criteria = "safe-to-deploy" + +[[exemptions.serde_urlencoded]] +version = "0.7.1" +criteria = "safe-to-deploy" + +[[exemptions.serde_with]] +version = "3.6.1" +criteria = "safe-to-deploy" + +[[exemptions.serde_with_macros]] +version = "3.6.1" +criteria = "safe-to-deploy" + +[[exemptions.sha2]] +version = "0.10.8" +criteria = "safe-to-deploy" + +[[exemptions.simd-adler32]] +version = "0.3.7" +criteria = "safe-to-deploy" + +[[exemptions.similar]] +version = "2.4.0" +criteria = "safe-to-run" + +[[exemptions.siphasher]] +version = "0.3.11" +criteria = "safe-to-deploy" + +[[exemptions.socket2]] +version = "0.5.6" +criteria = "safe-to-deploy" + +[[exemptions.softbuffer]] +version = "0.4.1" +criteria = "safe-to-deploy" + +[[exemptions.soup3]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.soup3-sys]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.spin]] +version = "0.9.8" +criteria = "safe-to-deploy" + +[[exemptions.stable_deref_trait]] +version = "1.2.0" +criteria = "safe-to-deploy" + +[[exemptions.state]] +version = "0.6.0" +criteria = "safe-to-deploy" + +[[exemptions.string_cache]] +version = "0.8.7" +criteria = "safe-to-deploy" + +[[exemptions.string_cache_codegen]] +version = "0.5.2" +criteria = "safe-to-deploy" + +[[exemptions.swift-rs]] +version = "1.0.6" +criteria = "safe-to-deploy" + +[[exemptions.sync_wrapper]] +version = "0.1.2" +criteria = "safe-to-deploy" + +[[exemptions.system-configuration]] +version = "0.5.1" +criteria = "safe-to-deploy" + +[[exemptions.system-configuration-sys]] +version = "0.5.0" +criteria = "safe-to-deploy" + +[[exemptions.system-deps]] +version = "6.2.0" +criteria = "safe-to-deploy" + +[[exemptions.tempfile]] +version = "3.10.1" +criteria = "safe-to-deploy" + +[[exemptions.tendril]] +version = "0.4.3" +criteria = "safe-to-deploy" + +[[exemptions.thin-slice]] +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.time]] +version = "0.3.34" +criteria = "safe-to-deploy" + +[[exemptions.time-macros]] +version = "0.2.17" +criteria = "safe-to-deploy" + +[[exemptions.tiny-xlib]] +version = "0.2.2" +criteria = "safe-to-deploy" + +[[exemptions.tokio-rustls]] +version = "0.24.1" +criteria = "safe-to-deploy" + +[[exemptions.tower-service]] +version = "0.3.2" +criteria = "safe-to-deploy" + +[[exemptions.tracing]] +version = "0.1.40" +criteria = "safe-to-deploy" + +[[exemptions.tracing-attributes]] +version = "0.1.27" +criteria = "safe-to-deploy" + +[[exemptions.tracing-core]] +version = "0.1.32" +criteria = "safe-to-deploy" + +[[exemptions.tracing-log]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.treediff]] +version = "4.0.3" +criteria = "safe-to-deploy" + +[[exemptions.typenum]] +version = "1.17.0" +criteria = "safe-to-deploy" + +[[exemptions.unarray]] +version = "0.1.4" +criteria = "safe-to-run" + +[[exemptions.untrusted]] +version = "0.9.0" +criteria = "safe-to-deploy" + +[[exemptions.utf-8]] +version = "0.7.6" +criteria = "safe-to-deploy" + +[[exemptions.uuid]] +version = "1.7.0" +criteria = "safe-to-deploy" + +[[exemptions.vswhom]] +version = "0.1.0" +criteria = "safe-to-deploy" + +[[exemptions.vswhom-sys]] +version = "0.1.2" +criteria = "safe-to-deploy" + +[[exemptions.wait-timeout]] +version = "0.2.0" +criteria = "safe-to-run" + +[[exemptions.wasm-streams]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.wayland-backend]] +version = "0.3.3" +criteria = "safe-to-deploy" + +[[exemptions.wayland-client]] +version = "0.31.2" +criteria = "safe-to-deploy" + +[[exemptions.wayland-scanner]] +version = "0.31.1" +criteria = "safe-to-deploy" + +[[exemptions.wayland-sys]] +version = "0.31.1" +criteria = "safe-to-deploy" + +[[exemptions.webpki-roots]] +version = "0.25.4" +criteria = "safe-to-deploy" + +[[exemptions.webview2-com]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.webview2-com-macros]] +version = "0.7.0" +criteria = "safe-to-deploy" + +[[exemptions.webview2-com-sys]] +version = "0.28.0" +criteria = "safe-to-deploy" + +[[exemptions.winapi]] +version = "0.3.9" +criteria = "safe-to-deploy" + +[[exemptions.winapi-i686-pc-windows-gnu]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.winapi-x86_64-pc-windows-gnu]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.winreg]] +version = "0.50.0" +criteria = "safe-to-deploy" + +[[exemptions.winreg]] +version = "0.51.0" +criteria = "safe-to-deploy" + +[[exemptions.x11]] +version = "2.21.0" +criteria = "safe-to-deploy" + +[[exemptions.x11-dl]] +version = "2.21.0" +criteria = "safe-to-deploy" + +[[exemptions.x11rb]] +version = "0.13.0" +criteria = "safe-to-deploy" + +[[exemptions.x11rb-protocol]] +version = "0.13.0" +criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock new file mode 100644 index 0000000..d0ff65d --- /dev/null +++ b/supply-chain/imports.lock @@ -0,0 +1,2983 @@ + +# cargo-vet imports lock + +[[publisher.aead]] +version = "0.5.2" +when = "2023-04-02" +user-id = 267 +user-login = "tarcieri" +user-name = "Tony Arcieri" + +[[publisher.aes]] +version = "0.8.4" +when = "2024-02-13" +user-id = 267 +user-login = "tarcieri" +user-name = "Tony Arcieri" + +[[publisher.aes-gcm]] +version = "0.10.3" +when = "2023-09-21" +user-id = 267 +user-login = "tarcieri" +user-name = "Tony Arcieri" + +[[publisher.aho-corasick]] +version = "1.1.2" +when = "2023-10-09" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.aho-corasick]] +version = "1.1.3" +when = "2024-03-20" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.anyhow]] +version = "1.0.80" +when = "2024-02-19" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.anyhow]] +version = "1.0.83" +when = "2024-05-06" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.autocfg]] +version = "1.3.0" +when = "2024-05-03" +user-id = 539 +user-login = "cuviper" +user-name = "Josh Stone" + +[[publisher.backtrace]] +version = "0.3.69" +when = "2023-08-22" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.backtrace]] +version = "0.3.71" +when = "2024-03-22" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.bumpalo]] +version = "3.15.3" +when = "2024-02-22" +user-id = 696 +user-login = "fitzgen" +user-name = "Nick Fitzgerald" + +[[publisher.bumpalo]] +version = "3.15.4" +when = "2024-03-07" +user-id = 696 +user-login = "fitzgen" +user-name = "Nick Fitzgerald" + +[[publisher.byteorder]] +version = "1.5.0" +when = "2023-10-06" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.bytes]] +version = "1.5.0" +when = "2023-09-07" +user-id = 6741 +user-login = "Darksonn" +user-name = "Alice Ryhl" + +[[publisher.bytes]] +version = "1.6.0" +when = "2024-03-22" +user-id = 6741 +user-login = "Darksonn" +user-name = "Alice Ryhl" + +[[publisher.cc]] +version = "1.0.88" +when = "2024-02-25" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.cfg-expr]] +version = "0.15.7" +when = "2024-02-09" +user-id = 52553 +user-login = "embark-studios" + +[[publisher.cfg-expr]] +version = "0.15.8" +when = "2024-04-10" +user-id = 52553 +user-login = "embark-studios" + +[[publisher.cipher]] +version = "0.3.0" +when = "2021-04-28" +user-id = 267 +user-login = "tarcieri" +user-name = "Tony Arcieri" + +[[publisher.core-foundation-sys]] +version = "0.8.4" +when = "2023-04-03" +user-id = 5946 +user-login = "jrmuizel" +user-name = "Jeff Muizelaar" + +[[publisher.core-graphics]] +version = "0.22.3" +when = "2021-11-02" +user-id = 5946 +user-login = "jrmuizel" +user-name = "Jeff Muizelaar" + +[[publisher.cpufeatures]] +version = "0.2.7" +when = "2023-04-20" +user-id = 267 +user-login = "tarcieri" +user-name = "Tony Arcieri" + +[[publisher.digest]] +version = "0.10.7" +when = "2023-05-19" +user-id = 267 +user-login = "tarcieri" +user-name = "Tony Arcieri" + +[[publisher.dtoa]] +version = "1.0.9" +when = "2023-07-15" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.dyn-clone]] +version = "1.0.17" +when = "2024-02-26" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.ghash]] +version = "0.5.1" +when = "2024-03-03" +user-id = 267 +user-login = "tarcieri" +user-name = "Tony Arcieri" + +[[publisher.hashbrown]] +version = "0.14.3" +when = "2023-11-26" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.hashbrown]] +version = "0.14.5" +when = "2024-04-28" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.http]] +version = "0.2.11" +when = "2023-11-13" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.http]] +version = "1.1.0" +when = "2024-03-04" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.http-body]] +version = "0.4.6" +when = "2023-12-08" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.http-body]] +version = "1.0.0" +when = "2023-11-15" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.http-body-util]] +version = "0.1.1" +when = "2024-03-11" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.httparse]] +version = "1.8.0" +when = "2022-08-30" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.hyper]] +version = "0.14.28" +when = "2023-12-18" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.hyper]] +version = "1.3.1" +when = "2024-04-16" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.hyper-tls]] +version = "0.5.0" +when = "2020-12-29" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.hyper-tls]] +version = "0.6.0" +when = "2023-11-27" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.hyper-util]] +version = "0.1.3" +when = "2024-01-31" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.indexmap]] +version = "1.9.3" +when = "2023-03-24" +user-id = 539 +user-login = "cuviper" +user-name = "Josh Stone" + +[[publisher.indexmap]] +version = "2.2.3" +when = "2024-02-11" +user-id = 539 +user-login = "cuviper" +user-name = "Josh Stone" + +[[publisher.indexmap]] +version = "2.2.6" +when = "2024-03-23" +user-id = 539 +user-login = "cuviper" +user-name = "Josh Stone" + +[[publisher.itoa]] +version = "0.4.8" +when = "2021-08-22" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.itoa]] +version = "1.0.10" +when = "2023-12-09" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.itoa]] +version = "1.0.11" +when = "2024-03-26" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.javascriptcore-rs]] +version = "1.1.2" +when = "2023-10-26" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.javascriptcore-rs-sys]] +version = "1.1.1" +when = "2023-10-26" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.js-sys]] +version = "0.3.68" +when = "2024-02-06" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.js-sys]] +version = "0.3.69" +when = "2024-03-04" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.libappindicator]] +version = "0.9.0" +when = "2023-10-01" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.libappindicator-sys]] +version = "0.9.0" +when = "2023-10-01" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.libc]] +version = "0.2.146" +when = "2023-06-06" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.libc]] +version = "0.2.154" +when = "2024-04-29" +user-id = 51017 +user-login = "JohnTitor" +user-name = "Yuki Okushi" + +[[publisher.libm]] +version = "0.2.8" +when = "2023-10-06" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.linux-raw-sys]] +version = "0.4.13" +when = "2024-01-16" +user-id = 6825 +user-login = "sunfishcode" +user-name = "Dan Gohman" + +[[publisher.linux-raw-sys]] +version = "0.6.4" +when = "2024-01-17" +user-id = 6825 +user-login = "sunfishcode" +user-name = "Dan Gohman" + +[[publisher.lock_api]] +version = "0.4.11" +when = "2023-10-17" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.lock_api]] +version = "0.4.12" +when = "2024-04-25" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.loom]] +version = "0.5.6" +when = "2022-05-19" +user-id = 6741 +user-login = "Darksonn" +user-name = "Alice Ryhl" + +[[publisher.memchr]] +version = "2.7.1" +when = "2023-12-28" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.memchr]] +version = "2.7.2" +when = "2024-03-27" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.mime]] +version = "0.3.17" +when = "2023-03-20" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.muda]] +version = "0.13.2" +when = "2024-05-07" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.new_debug_unreachable]] +version = "1.0.6" +when = "2024-03-15" +user-id = 2017 +user-login = "mbrubeck" +user-name = "Matt Brubeck" + +[[publisher.num_cpus]] +version = "1.16.0" +when = "2023-06-29" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.openssl-src]] +version = "300.2.3+3.2.1" +when = "2024-02-12" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.parking_lot]] +version = "0.12.1" +when = "2022-05-31" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.parking_lot]] +version = "0.12.2" +when = "2024-04-25" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.parking_lot_core]] +version = "0.9.9" +when = "2023-10-17" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.parking_lot_core]] +version = "0.9.10" +when = "2024-04-25" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.paste]] +version = "1.0.15" +when = "2024-05-07" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.phf]] +version = "0.10.1" +when = "2021-12-13" +user-id = 51017 +user-login = "JohnTitor" +user-name = "Yuki Okushi" + +[[publisher.phf]] +version = "0.11.2" +when = "2023-06-24" +user-id = 51017 +user-login = "JohnTitor" +user-name = "Yuki Okushi" + +[[publisher.phf_codegen]] +version = "0.10.0" +when = "2021-08-10" +user-id = 51017 +user-login = "JohnTitor" +user-name = "Yuki Okushi" + +[[publisher.phf_generator]] +version = "0.10.0" +when = "2021-08-10" +user-id = 51017 +user-login = "JohnTitor" +user-name = "Yuki Okushi" + +[[publisher.phf_generator]] +version = "0.11.2" +when = "2023-06-24" +user-id = 51017 +user-login = "JohnTitor" +user-name = "Yuki Okushi" + +[[publisher.phf_macros]] +version = "0.11.2" +when = "2023-06-24" +user-id = 51017 +user-login = "JohnTitor" +user-name = "Yuki Okushi" + +[[publisher.phf_shared]] +version = "0.10.0" +when = "2021-08-10" +user-id = 51017 +user-login = "JohnTitor" +user-name = "Yuki Okushi" + +[[publisher.phf_shared]] +version = "0.11.2" +when = "2023-06-24" +user-id = 51017 +user-login = "JohnTitor" +user-name = "Yuki Okushi" + +[[publisher.polyval]] +version = "0.6.2" +when = "2024-03-03" +user-id = 267 +user-login = "tarcieri" +user-name = "Tony Arcieri" + +[[publisher.proc-macro-hack]] +version = "0.5.20+deprecated" +when = "2022-12-19" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.proc-macro2]] +version = "1.0.78" +when = "2024-01-21" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.proc-macro2]] +version = "1.0.82" +when = "2024-05-07" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.quickcheck]] +version = "1.0.3" +when = "2021-01-15" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.regex]] +version = "1.10.3" +when = "2024-01-21" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.regex]] +version = "1.10.4" +when = "2024-03-23" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.regex-automata]] +version = "0.1.10" +when = "2021-06-01" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.regex-automata]] +version = "0.4.5" +when = "2024-01-25" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.regex-automata]] +version = "0.4.6" +when = "2024-03-04" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.regex-syntax]] +version = "0.6.29" +when = "2023-03-21" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.regex-syntax]] +version = "0.8.2" +when = "2023-10-14" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.regex-syntax]] +version = "0.8.3" +when = "2024-03-26" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.reqwest]] +version = "0.11.24" +when = "2024-01-31" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.reqwest]] +version = "0.12.4" +when = "2024-04-19" +user-id = 359 +user-login = "seanmonstar" +user-name = "Sean McArthur" + +[[publisher.rustix]] +version = "0.38.31" +when = "2024-02-01" +user-id = 6825 +user-login = "sunfishcode" +user-name = "Dan Gohman" + +[[publisher.rustix]] +version = "0.38.34" +when = "2024-04-22" +user-id = 6825 +user-login = "sunfishcode" +user-name = "Dan Gohman" + +[[publisher.rustversion]] +version = "1.0.16" +when = "2024-05-07" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.ryu]] +version = "1.0.17" +when = "2024-02-19" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.ryu]] +version = "1.0.18" +when = "2024-05-07" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.same-file]] +version = "1.0.6" +when = "2020-01-11" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.scoped-tls]] +version = "1.0.1" +when = "2022-10-31" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.scopeguard]] +version = "1.2.0" +when = "2023-07-17" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.semver]] +version = "1.0.22" +when = "2024-02-19" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.semver]] +version = "1.0.23" +when = "2024-05-07" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde]] +version = "1.0.197" +when = "2024-02-20" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde]] +version = "1.0.201" +when = "2024-05-08" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde_derive]] +version = "1.0.201" +when = "2024-05-08" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde_derive_internals]] +version = "0.26.0" +when = "2021-04-09" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde_derive_internals]] +version = "0.29.0" +when = "2023-09-06" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde_json]] +version = "1.0.114" +when = "2024-02-20" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde_json]] +version = "1.0.117" +when = "2024-05-08" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde_repr]] +version = "0.1.18" +when = "2024-01-02" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde_repr]] +version = "0.1.19" +when = "2024-04-08" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde_spanned]] +version = "0.6.5" +when = "2023-12-19" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.serialize-to-javascript]] +version = "0.1.1" +when = "2022-02-08" +user-id = 28029 +user-login = "chippers" +user-name = "chip" + +[[publisher.serialize-to-javascript-impl]] +version = "0.1.1" +when = "2022-02-08" +user-id = 28029 +user-login = "chippers" +user-name = "chip" + +[[publisher.slab]] +version = "0.4.9" +when = "2023-08-22" +user-id = 6741 +user-login = "Darksonn" +user-name = "Alice Ryhl" + +[[publisher.smallvec]] +version = "1.13.1" +when = "2024-01-19" +user-id = 2017 +user-login = "mbrubeck" +user-name = "Matt Brubeck" + +[[publisher.smallvec]] +version = "1.13.2" +when = "2024-03-20" +user-id = 2017 +user-login = "mbrubeck" +user-name = "Matt Brubeck" + +[[publisher.syn]] +version = "1.0.109" +when = "2023-02-24" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.syn]] +version = "2.0.51" +when = "2024-02-26" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.syn]] +version = "2.0.61" +when = "2024-05-07" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.tao]] +version = "0.28.0" +when = "2024-05-07" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.tao-macros]] +version = "0.1.2" +when = "2023-08-13" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.target-lexicon]] +version = "0.12.14" +when = "2024-02-22" +user-id = 6825 +user-login = "sunfishcode" +user-name = "Dan Gohman" + +[[publisher.tauri]] +version = "2.0.0-beta.18" +when = "2024-05-06" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.tauri-build]] +version = "2.0.0-beta.14" +when = "2024-05-06" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.tauri-codegen]] +version = "2.0.0-beta.14" +when = "2024-05-06" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.tauri-macros]] +version = "2.0.0-beta.14" +when = "2024-05-06" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.tauri-plugin]] +version = "2.0.0-beta.14" +when = "2024-05-06" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.tauri-runtime]] +version = "2.0.0-beta.15" +when = "2024-05-06" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.tauri-runtime-wry]] +version = "2.0.0-beta.15" +when = "2024-05-06" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.tauri-utils]] +version = "2.0.0-beta.14" +when = "2024-05-06" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.tauri-winres]] +version = "0.1.1" +when = "2023-05-04" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.thiserror]] +version = "1.0.57" +when = "2024-02-11" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.thiserror]] +version = "1.0.60" +when = "2024-05-07" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.thiserror-impl]] +version = "1.0.57" +when = "2024-02-11" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.thiserror-impl]] +version = "1.0.60" +when = "2024-05-07" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.thread_local]] +version = "1.1.8" +when = "2024-02-20" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.tokio]] +version = "1.36.0" +when = "2024-02-02" +user-id = 6741 +user-login = "Darksonn" +user-name = "Alice Ryhl" + +[[publisher.tokio]] +version = "1.37.0" +when = "2024-03-28" +user-id = 6741 +user-login = "Darksonn" +user-name = "Alice Ryhl" + +[[publisher.tokio-macros]] +version = "2.2.0" +when = "2023-11-09" +user-id = 10 +user-login = "carllerche" +user-name = "Carl Lerche" + +[[publisher.tokio-util]] +version = "0.7.10" +when = "2023-10-25" +user-id = 6741 +user-login = "Darksonn" +user-name = "Alice Ryhl" + +[[publisher.tokio-util]] +version = "0.7.11" +when = "2024-05-04" +user-id = 6741 +user-login = "Darksonn" +user-name = "Alice Ryhl" + +[[publisher.toml]] +version = "0.7.8" +when = "2023-09-09" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.toml]] +version = "0.8.2" +when = "2023-10-03" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.toml_edit]] +version = "0.19.15" +when = "2023-09-08" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.toml_edit]] +version = "0.20.2" +when = "2023-10-03" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.tray-icon]] +version = "0.13.5" +when = "2024-05-02" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.ucd-trie]] +version = "0.1.6" +when = "2023-07-07" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.unicode-normalization]] +version = "0.1.23" +when = "2024-02-20" +user-id = 1139 +user-login = "Manishearth" +user-name = "Manish Goregaokar" + +[[publisher.unicode-segmentation]] +version = "1.11.0" +when = "2024-02-07" +user-id = 1139 +user-login = "Manishearth" +user-name = "Manish Goregaokar" + +[[publisher.walkdir]] +version = "2.4.0" +when = "2023-09-05" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.walkdir]] +version = "2.5.0" +when = "2024-03-01" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.wasi]] +version = "0.9.0+wasi-snapshot-preview1" +when = "2019-12-02" +user-id = 6825 +user-login = "sunfishcode" +user-name = "Dan Gohman" + +[[publisher.wasi]] +version = "0.11.0+wasi-snapshot-preview1" +when = "2022-01-19" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wasm-bindgen]] +version = "0.2.91" +when = "2024-02-06" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wasm-bindgen-backend]] +version = "0.2.91" +when = "2024-02-06" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wasm-bindgen-backend]] +version = "0.2.92" +when = "2024-03-04" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wasm-bindgen-futures]] +version = "0.4.41" +when = "2024-02-06" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wasm-bindgen-futures]] +version = "0.4.42" +when = "2024-03-04" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wasm-bindgen-macro]] +version = "0.2.91" +when = "2024-02-06" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wasm-bindgen-macro]] +version = "0.2.92" +when = "2024-03-04" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wasm-bindgen-macro-support]] +version = "0.2.91" +when = "2024-02-06" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wasm-bindgen-shared]] +version = "0.2.91" +when = "2024-02-06" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.wasm-bindgen-shared]] +version = "0.2.92" +when = "2024-03-04" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.web-sys]] +version = "0.3.68" +when = "2024-02-06" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.web-sys]] +version = "0.3.69" +when = "2024-03-04" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.webkit2gtk]] +version = "2.0.1" +when = "2023-10-26" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.webkit2gtk-sys]] +version = "2.0.1" +when = "2023-10-26" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.winapi-util]] +version = "0.1.6" +when = "2023-09-20" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.winapi-util]] +version = "0.1.8" +when = "2024-04-25" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.window-vibrancy]] +version = "0.5.0" +when = "2024-02-07" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.windows]] +version = "0.48.0" +when = "2023-03-31" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows]] +version = "0.52.0" +when = "2023-11-15" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows]] +version = "0.53.0" +when = "2024-02-22" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows]] +version = "0.56.0" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-core]] +version = "0.52.0" +when = "2023-11-15" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-core]] +version = "0.53.0" +when = "2024-02-22" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-core]] +version = "0.56.0" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-implement]] +version = "0.52.0" +when = "2023-11-15" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-implement]] +version = "0.56.0" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-interface]] +version = "0.52.0" +when = "2023-11-15" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-interface]] +version = "0.56.0" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-result]] +version = "0.1.0" +when = "2024-02-22" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-result]] +version = "0.1.1" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-sys]] +version = "0.45.0" +when = "2023-01-21" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-sys]] +version = "0.48.0" +when = "2023-03-31" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-sys]] +version = "0.52.0" +when = "2023-11-15" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-targets]] +version = "0.42.2" +when = "2023-03-13" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-targets]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-targets]] +version = "0.52.3" +when = "2024-02-22" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-targets]] +version = "0.52.5" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-version]] +version = "0.1.0" +when = "2023-11-15" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-version]] +version = "0.1.1" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_aarch64_gnullvm]] +version = "0.42.2" +when = "2023-03-13" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_aarch64_gnullvm]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_aarch64_gnullvm]] +version = "0.52.3" +when = "2024-02-22" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_aarch64_gnullvm]] +version = "0.52.5" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_aarch64_msvc]] +version = "0.42.2" +when = "2023-03-13" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_aarch64_msvc]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_aarch64_msvc]] +version = "0.52.3" +when = "2024-02-22" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_aarch64_msvc]] +version = "0.52.5" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_gnu]] +version = "0.42.2" +when = "2023-03-13" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_gnu]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_gnu]] +version = "0.52.3" +when = "2024-02-22" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_gnu]] +version = "0.52.5" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_gnullvm]] +version = "0.52.5" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_msvc]] +version = "0.42.2" +when = "2023-03-13" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_msvc]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_msvc]] +version = "0.52.3" +when = "2024-02-22" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_i686_msvc]] +version = "0.52.5" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_gnu]] +version = "0.42.2" +when = "2023-03-13" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_gnu]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_gnu]] +version = "0.52.3" +when = "2024-02-22" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_gnu]] +version = "0.52.5" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_gnullvm]] +version = "0.42.2" +when = "2023-03-13" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_gnullvm]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_gnullvm]] +version = "0.52.3" +when = "2024-02-22" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_gnullvm]] +version = "0.52.5" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_msvc]] +version = "0.42.2" +when = "2023-03-13" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_msvc]] +version = "0.48.5" +when = "2023-08-18" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_msvc]] +version = "0.52.3" +when = "2024-02-22" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows_x86_64_msvc]] +version = "0.52.5" +when = "2024-04-12" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.winnow]] +version = "0.5.40" +when = "2024-02-12" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.wry]] +version = "0.39.4" +when = "2024-05-07" +user-id = 95389 +user-login = "tauri-bot" +user-name = "tauri" + +[[publisher.zeroize]] +version = "1.7.0" +when = "2023-11-16" +user-id = 267 +user-login = "tarcieri" +user-name = "Tony Arcieri" + +[[audits.bytecode-alliance.wildcard-audits.bumpalo]] +who = "Nick Fitzgerald " +criteria = "safe-to-deploy" +user-id = 696 # Nick Fitzgerald (fitzgen) +start = "2019-03-16" +end = "2024-03-10" + +[[audits.bytecode-alliance.audits.adler]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "1.0.2" +notes = "This is a small crate which forbids unsafe code and is a straightforward implementation of the adler hashing algorithm." + +[[audits.bytecode-alliance.audits.block-buffer]] +who = "Benjamin Bouvier " +criteria = "safe-to-deploy" +delta = "0.9.0 -> 0.10.2" + +[[audits.bytecode-alliance.audits.cargo_metadata]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.15.3" +notes = "no build, no unsafe, inputs to cargo command are reasonably sanitized" + +[[audits.bytecode-alliance.audits.cargo_metadata]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.17.0 -> 0.18.1" +notes = "No major changes, no unsafe code here." + +[[audits.bytecode-alliance.audits.cfg-if]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "1.0.0" +notes = "I am the author of this crate." + +[[audits.bytecode-alliance.audits.core-foundation-sys]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +delta = "0.8.4 -> 0.8.6" +notes = """ +The changes here are all typical bindings updates: new functions, types, and +constants. I have not audited all the bindings for ABI conformance. +""" + +[[audits.bytecode-alliance.audits.crypto-common]] +who = "Benjamin Bouvier " +criteria = "safe-to-deploy" +version = "0.1.3" + +[[audits.bytecode-alliance.audits.errno]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +version = "0.3.0" +notes = "This crate uses libc and windows-sys APIs to get and set the raw OS error value." + +[[audits.bytecode-alliance.audits.errno]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +delta = "0.3.0 -> 0.3.1" +notes = "Just a dependency version bump and a bug fix for redox" + +[[audits.bytecode-alliance.audits.fastrand]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "2.0.0 -> 2.0.1" +notes = """ +This update had a few doc updates but no otherwise-substantial source code +updates. +""" + +[[audits.bytecode-alliance.audits.foreign-types]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.2" +notes = "This crate defined a macro-rules which creates wrappers working with FFI types. The implementation of this crate appears to be safe, but each use of this macro would need to be vetted for correctness as well." + +[[audits.bytecode-alliance.audits.foreign-types-shared]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.1.1" + +[[audits.bytecode-alliance.audits.futures-channel]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.27" +notes = "build.rs is just detecting the target and setting cfg. unsafety is for implementing a concurrency primitives using atomics and unsafecell, and is not obviously incorrect (this is the sort of thing I wouldn't certify as correct without formal methods)" + +[[audits.bytecode-alliance.audits.futures-core]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.27" +notes = "Unsafe used to implement a concurrency primitive AtomicWaker. Well-commented and not obviously incorrect. Like my other audits of these concurrency primitives inside the futures family, I couldn't certify that it is correct without formal methods, but that is out of scope for this vetting." + +[[audits.bytecode-alliance.audits.heck]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.4.0" +notes = "Contains `forbid_unsafe` and only uses `std::fmt` from the standard library. Otherwise only contains string manipulation." + +[[audits.bytecode-alliance.audits.iana-time-zone-haiku]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +version = "0.1.2" + +[[audits.bytecode-alliance.audits.idna]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.3.0" +notes = """ +This is a crate without unsafe code or usage of the standard library. The large +size of this crate comes from the large generated unicode tables file. This +crate is broadly used throughout the ecosystem and does not contain anything +suspicious. +""" + +[[audits.bytecode-alliance.audits.libc]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.2.146 -> 0.2.147" +notes = "Only new type definitions and updating others for some platforms, no major changes" + +[[audits.bytecode-alliance.audits.libc]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.2.148 -> 0.2.149" +notes = "Lots of new functions and constants for new platforms and nothing out of the ordinary for what one would expect of the `libc` crate." + +[[audits.bytecode-alliance.audits.libc]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +delta = "0.2.149 -> 0.2.151" +notes = "More new functions, types, and constants, as is usual for the `libc` crate, as well as various minor code cleanups." + +[[audits.bytecode-alliance.audits.libc]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.2.151 -> 0.2.153" +notes = "More bindings for more platforms. I have not verified that everything is exactly as-is on the platform as specified but nothing major is otherwise introduced as part of this bump." + +[[audits.bytecode-alliance.audits.matchers]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.1.0" + +[[audits.bytecode-alliance.audits.native-tls]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.2.11" +notes = "build is only looking for environment variables to set cfg. only two minor uses of unsafe,on macos, with ffi bindings to digest primitives and libc atexit. otherwise, this is an abstraction over three very complex systems (schannel, security-framework, and openssl) which may end up having subtle differences, but none of those are apparent from the implementation of this crate" + +[[audits.bytecode-alliance.audits.nu-ansi-term]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.46.0" +notes = "one use of unsafe to call windows specific api to get console handle." + +[[audits.bytecode-alliance.audits.openssl-macros]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.1.0" + +[[audits.bytecode-alliance.audits.openssl-probe]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.1.5" +notes = "IO is only checking for the existence of paths in the filesystem" + +[[audits.bytecode-alliance.audits.overload]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.1.1" +notes = "small crate, only defines macro-rules!, nicely documented as well" + +[[audits.bytecode-alliance.audits.percent-encoding]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "2.2.0" +notes = """ +This crate is a single-file crate that does what it says on the tin. There are +a few `unsafe` blocks related to utf-8 validation which are locally verifiable +as correct and otherwise this crate is good to go. +""" + +[[audits.bytecode-alliance.audits.pin-utils]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.1.0" + +[[audits.bytecode-alliance.audits.quote]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +delta = "1.0.23 -> 1.0.27" + +[[audits.bytecode-alliance.audits.rustc-demangle]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.1.21" +notes = "I am the author of this crate." + +[[audits.bytecode-alliance.audits.sharded-slab]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.1.4" +notes = "I always really enjoy reading eliza's code, she left perfect comments at every use of unsafe." + +[[audits.bytecode-alliance.audits.signal-hook-registry]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "1.4.1" + +[[audits.bytecode-alliance.audits.tinyvec]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "1.6.0" +notes = """ +This crate, while it implements collections, does so without `std::*` APIs and +without `unsafe`. Skimming the crate everything looks reasonable and what one +would expect from idiomatic safe collections in Rust. +""" + +[[audits.bytecode-alliance.audits.tinyvec_macros]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.1.0" +notes = """ +This is a trivial crate which only contains a singular macro definition which is +intended to multiplex across the internal representation of a tinyvec, +presumably. This trivially doesn't contain anything bad. +""" + +[[audits.bytecode-alliance.audits.tokio-native-tls]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.1" +notes = "unsafety is used for smuggling std::task::Context as a raw pointer. Lifetime and type safety appears to be taken care of correctly." + +[[audits.bytecode-alliance.audits.tracing-subscriber]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.17" + +[[audits.bytecode-alliance.audits.try-lock]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.2.4" +notes = "Implements a concurrency primitive with atomics, and is not obviously incorrect" + +[[audits.bytecode-alliance.audits.unicode-bidi]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.3.8" +notes = """ +This crate has no unsafe code and does not use `std::*`. Skimming the crate it +does not attempt to out of the bounds of what it's already supposed to be doing. +""" + +[[audits.bytecode-alliance.audits.unicode-ident]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "1.0.8" + +[[audits.bytecode-alliance.audits.vcpkg]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.2.15" +notes = "no build.rs, no macros, no unsafe. It reads the filesystem and makes copies of DLLs into OUT_DIR." + +[[audits.bytecode-alliance.audits.want]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.0" + +[[audits.embark-studios.wildcard-audits.cfg-expr]] +who = "Jake Shadle " +criteria = "safe-to-deploy" +user-id = 52553 # embark-studios +start = "2020-01-01" +end = "2024-05-23" +notes = "Maintained by Embark. No unsafe usage or ambient capabilities" + +[[audits.embark-studios.audits.cargo_metadata]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +delta = "0.15.3 -> 0.15.4" +notes = "No notable changes" + +[[audits.embark-studios.audits.cargo_metadata]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +delta = "0.15.4 -> 0.17.0" +notes = "No notable changes" + +[[audits.embark-studios.audits.cfg_aliases]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.1.1" +notes = "No unsafe usage or ambient capabilities" + +[[audits.embark-studios.audits.convert_case]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.4.0" +notes = "No unsafe usage or ambient capabilities" + +[[audits.embark-studios.audits.derive_more]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.99.17" +notes = "No unsafe usage or ambient capabilities" + +[[audits.embark-studios.audits.ident_case]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "1.0.1" +notes = "No unsafe usage or ambient capabilities" + +[[audits.embark-studios.audits.idna]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +delta = "0.3.0 -> 0.4.0" +notes = "No unsafe usage or ambient capabilities" + +[[audits.embark-studios.audits.jni]] +who = "Robert Bragg " +criteria = "safe-to-deploy" +version = "0.21.1" +notes = """ +Aims to provide a safe JNI (Java Native Interface) API over the +unsafe `jni_sys` crate. + +This is a very general FFI abstraction for Java VMs with a lot of unsafe code +throughout the API. There are almost certainly some edge cases with its design +that could lead to unsound behaviour but it should still be considerably safer +than working with JNI directly. + +A lot of the unsafe usage relates to quite-simple use of `from_raw` APIs to +construct or cast wrapper types (around JNI pointers) which are fairly +straight-forward to verify/trust in context. + +Some unsafe code has good `// # Safety` documentation (this has been enforced for +newer code) but a lot of unsafe code doesn't document invariants that are +being relied on. + +The design depends on non-trivial named lifetimes across many APIs to associate +Java local references with JNI stack frames. + +The crate is not very actively maintained and was practically unmaintained for +over a year before the 0.20 release. + +Robert Bragg who now works at Embark Studios became the maintainer of this +crate in October 2022. + +In the process of working on the `jni` crate since becoming maintainer it's +worth noting that I came across multiple APIs that I found needed to be +re-worked to address safety issues, including ensuring that APIs that are not +implemented safely are correctly declared as `unsafe`. + +There has been a focus on improving safety in the last two release. + +The jni crate has been used in production with the Signal messaging application +for over two years: +https://github.com/signalapp/libsignal/blob/main/rust/bridge/jni/Cargo.toml + +# Some Notable Open Issues +- https://github.com/jni-rs/jni-rs/issues/422 - questions soundness of linking + multiple versions of jni crate into an application, considering the use + of (separately scoped) thread-local-storage to track thread attachments +- https://github.com/jni-rs/jni-rs/issues/405 - discusses the ease with which + code may expose the JVM to invalid booleans with undefined behaviour +""" + +[[audits.embark-studios.audits.line-wrap]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.1.1" +notes = "No unsafe usage or ambient capabilities" + +[[audits.embark-studios.audits.ndk-context]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.1.1" +notes = "Tiny crate that initializes Android with FFI, looks sane. No other ambient capabilities" + +[[audits.embark-studios.audits.num_enum]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.5.11" +notes = "No unsafe usage or ambient capabilities" + +[[audits.embark-studios.audits.num_enum_derive]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.5.11" +notes = "Proc macro that generates some unsafe code for conversion but looks sound, no ambient capabilities" + +[[audits.embark-studios.audits.quickcheck_macros]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "1.0.0" +notes = "Proc macro. No unsafe usage or ambient capabilities" + +[[audits.embark-studios.audits.toml_datetime]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +delta = "0.6.1 -> 0.6.2" +notes = "No notable changes" + +[[audits.embark-studios.audits.unic-ucd-version]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.9.0" +notes = "Only few constants. No unsafe usage or other ambient capabilities" + +[[audits.embark-studios.audits.valuable]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.1.0" +notes = "No unsafe usage or ambient capabilities, sane build script" + +[[audits.embark-studios.audits.version-compare]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.1.1" +notes = "No unsafe usage or ambient capabilities" + +[[audits.embark-studios.audits.yaml-rust]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.4.5" +notes = "No unsafe usage or ambient capabilities" + +[[audits.google.audits.bitflags]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "2.4.2" +notes = """ +Audit notes: + +* I've checked for any discussion in Google-internal cl/546819168 (where audit + of version 2.3.3 happened) +* `src/lib.rs` contains `#![cfg_attr(not(test), forbid(unsafe_code))]` +* There are 2 cases of `unsafe` in `src/external.rs` but they seem to be + correct in a straightforward way - they just propagate the marker trait's + impl (e.g. `impl bytemuck::Pod`) from the inner to the outer type +* Additional discussion and/or notes may be found in https://crrev.com/c/5238056 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.bitflags]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "2.4.2 -> 2.5.0" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.bytemuck]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.14.3" +notes = "Additional review notes may be found in https://crrev.com/c/5362675." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.bytemuck]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.14.3 -> 1.15.0" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.color_quant]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.1.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.env_logger]] +who = "George Burgess IV " +criteria = "safe-to-run" +version = "0.9.3" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.env_logger]] +who = "George Burgess IV " +criteria = "safe-to-run" +delta = "0.9.3 -> 0.8.4" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.equivalent]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.1" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.fastrand]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.9.0" +notes = """ +`does-not-implement-crypto` is certified because this crate explicitly says +that the RNG here is not cryptographically secure. +""" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.glob]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "0.3.1" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.httpdate]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.3" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.openssl-macros]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +delta = "0.1.0 -> 0.1.1" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.pin-project-lite]] +who = "David Koloski " +criteria = "safe-to-deploy" +version = "0.2.9" +notes = "Reviewed on https://fxrev.dev/824504" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.pin-project-lite]] +who = "David Koloski " +criteria = "safe-to-deploy" +delta = "0.2.9 -> 0.2.13" +notes = "Audited at https://fxrev.dev/946396" +aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro-error-attr]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.4" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.quote]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.35" +notes = """ +Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits +(except for benign \"net\" hit in tests and \"fs\" hit in README.md) +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.quote]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.35 -> 1.0.36" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.serde_derive]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.197" +notes = "Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.strsim]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +version = "0.10.0" +notes = """ +Reviewed in https://crrev.com/c/5171063 + +Previously reviewed during security review and the audit is grandparented in. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.version_check]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "0.9.4" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.isrg.audits.block-buffer]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.9.0" + +[[audits.isrg.audits.getrandom]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.12 -> 0.2.14" + +[[audits.isrg.audits.getrandom]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.14 -> 0.2.15" + +[[audits.isrg.audits.ghash]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.5.0" + +[[audits.isrg.audits.num-traits]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.15 -> 0.2.16" + +[[audits.isrg.audits.num-traits]] +who = "Ameer Ghani " +criteria = "safe-to-deploy" +delta = "0.2.16 -> 0.2.17" + +[[audits.isrg.audits.num-traits]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.17 -> 0.2.18" + +[[audits.isrg.audits.num-traits]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.2.18 -> 0.2.19" + +[[audits.isrg.audits.opaque-debug]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.3.0" + +[[audits.isrg.audits.rand_chacha]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.3.1" + +[[audits.isrg.audits.rand_core]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.6.3" + +[[audits.isrg.audits.universal-hash]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.4.1" + +[[audits.isrg.audits.universal-hash]] +who = "David Cook " +criteria = "safe-to-deploy" +delta = "0.5.0 -> 0.5.1" + +[[audits.mozilla.wildcard-audits.core-foundation-sys]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +user-id = 5946 # Jeff Muizelaar (jrmuizel) +start = "2020-10-14" +end = "2023-05-04" +renew = false +notes = "I've reviewed every source contribution that was neither authored nor reviewed by Mozilla." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.wildcard-audits.core-graphics]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +user-id = 5946 # Jeff Muizelaar (jrmuizel) +start = "2020-12-08" +end = "2023-05-04" +renew = false +notes = "I've reviewed every source contribution that was neither authored nor reviewed by Mozilla." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.wildcard-audits.encoding_rs]] +who = "Henri Sivonen " +criteria = "safe-to-deploy" +user-id = 4484 +start = "2019-02-26" +end = "2024-08-28" +notes = "I, Henri Sivonen, wrote encoding_rs for Gecko and have reviewed contributions by others. There are two caveats to the certification: 1) The crate does things that are documented to be UB but that do not appear to actually be UB due to integer types differing from the general rule; https://github.com/hsivonen/encoding_rs/issues/79 . 2) It would be prudent to re-review the code that reinterprets buffers of integers as SIMD vectors; see https://github.com/hsivonen/encoding_rs/issues/87 ." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.wildcard-audits.unicode-normalization]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +user-id = 1139 # Manish Goregaokar (Manishearth) +start = "2019-11-06" +end = "2024-05-03" +notes = "All code written or reviewed by Manish" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.wildcard-audits.unicode-segmentation]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +user-id = 1139 # Manish Goregaokar (Manishearth) +start = "2019-05-15" +end = "2024-05-03" +notes = "All code written or reviewed by Manish" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.android_system_properties]] +who = "Nicolas Silva " +criteria = "safe-to-deploy" +version = "0.1.2" +notes = "I wrote this crate, reviewed by jimb. It is mostly a Rust port of some C++ code we already ship." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.android_system_properties]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.1.2 -> 0.1.4" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.android_system_properties]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.1.4 -> 0.1.5" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.autocfg]] +who = "Josh Stone " +criteria = "safe-to-deploy" +version = "1.1.0" +notes = "All code written or reviewed by Josh Stone." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-set]] +who = "Aria Beingessner " +criteria = "safe-to-deploy" +version = "0.5.2" +notes = "Another crate I own via contain-rs that is ancient and maintenance mode, no known issues." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-set]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.5.2 -> 0.5.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bit-vec]] +who = "Aria Beingessner " +criteria = "safe-to-deploy" +version = "0.6.3" +notes = "Another crate I own via contain-rs that is ancient and in maintenance mode but otherwise perfectly fine." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.block-buffer]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.10.2 -> 0.10.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.core-graphics]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.22.3 -> 0.23.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.cpufeatures]] +who = "Gabriele Svelto " +criteria = "safe-to-deploy" +delta = "0.2.7 -> 0.2.8" +notes = "This release contains a single fix for an issue that affected Firefox" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.crypto-common]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.1.3 -> 0.1.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.cssparser-macros]] +who = "Emilio Cobos Álvarez " +criteria = "safe-to-deploy" +version = "0.6.0" +notes = """ +Trivial crate with a single proc macro to compute the max length of the inputs +to a match expression. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.cssparser-macros]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.6.0 -> 0.6.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.errno]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.1 -> 0.3.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.fastrand]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.9.0 -> 2.0.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.fnv]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +version = "1.0.7" +notes = "Simple hasher implementation with no unsafe code." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.foreign-types]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.3.2 -> 0.5.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.foreign-types-macros]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +version = "0.2.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.foreign-types-shared]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.1.1 -> 0.3.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.form_urlencoded]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +version = "1.2.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.form_urlencoded]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +delta = "1.2.0 -> 1.2.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-channel]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.27 -> 0.3.28" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-core]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.27 -> 0.3.28" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.fxhash]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +version = "0.2.1" +notes = "Straightforward crate with no unsafe code, does what it says on the tin." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hashbrown]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +version = "0.12.3" +notes = "This version is used in rust's libstd, so effectively we're already trusting it" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.heck]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.4.0 -> 0.4.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hex]] +who = "Simon Friedberger " +criteria = "safe-to-deploy" +version = "0.4.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.idna]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +delta = "0.4.0 -> 0.5.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.lazy_static]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +version = "1.4.0" +notes = "I have read over the macros, and audited the unsafe code." +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.libc]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.2.147 -> 0.2.148" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.libloading]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.7.4 -> 0.8.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.linked-hash-map]] +who = "Aria Beingessner " +criteria = "safe-to-deploy" +version = "0.5.4" +notes = "I own this crate (I am contain-rs) and 0.5.4 passes miri. This code is very old and used by lots of people, so I'm pretty confident in it, even though it's in maintenance-mode and missing some nice-to-have APIs." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.linked-hash-map]] +who = "Mike Hommey " +criteria = "safe-to-run" +delta = "0.5.4 -> 0.5.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.log]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +version = "0.4.17" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.log]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "0.4.17 -> 0.4.18" +notes = "One dependency removed, others updated (which we don't rely on), some APIs (which we don't use) changed." +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.log]] +who = "Kagami Sascha Rosylight " +criteria = "safe-to-deploy" +delta = "0.4.18 -> 0.4.20" +notes = "Only cfg attribute and internal macro changes and module refactorings" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.malloc_buf]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +version = "0.0.6" +notes = """ +Very small crate for managing malloc-ed buffers, primarily for use in the objc crate. +There is an edge-case condition that passes slice::from_raw_parts(0x1, 0) which I'm +not entirely certain is technically sound, but in either case I am reasonably confident +it's not exploitable. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.matches]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +version = "0.1.9" +notes = "This is a trivial crate." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.matches]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.1.9 -> 0.1.10" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.new_debug_unreachable]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +version = "1.0.4" +notes = "This is a trivial crate." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.num-traits]] +who = "Josh Stone " +criteria = "safe-to-deploy" +version = "0.2.15" +notes = "All code written or reviewed by Josh Stone." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.percent-encoding]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +delta = "2.2.0 -> 2.3.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.percent-encoding]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +delta = "2.3.0 -> 2.3.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.precomputed-hash]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +version = "0.1.1" +notes = "This is a trivial crate." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.quote]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +version = "1.0.18" +notes = """ +`quote` is a utility crate used by proc-macros to generate TokenStreams +conveniently from source code. The bulk of the logic is some complex +interlocking `macro_rules!` macros which are used to parse and build the +`TokenStream` within the proc-macro. + +This crate contains no unsafe code, and the internal logic, while difficult to +read, is generally straightforward. I have audited the the quote macros, ident +formatter, and runtime logic. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.quote]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.18 -> 1.0.21" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.quote]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "1.0.21 -> 1.0.23" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.quote]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.27 -> 1.0.28" +notes = "Enabled on wasm targets" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.quote]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.28 -> 1.0.31" +notes = "Minimal changes and removal of the build.rs" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.rand_core]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.6.3 -> 0.6.4" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.raw-window-handle]] +who = "Jim Blandy " +criteria = "safe-to-deploy" +version = "0.5.0" +notes = "I looked through all the sources of the v0.5.0 crate." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.raw-window-handle]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.5.0 -> 0.5.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.raw-window-handle]] +who = "Nicolas Silva " +criteria = "safe-to-deploy" +delta = "0.5.2 -> 0.6.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.rustversion]] +who = "Bobby Holley " +criteria = "safe-to-deploy" +version = "1.0.9" +notes = """ +This crate has a build-time component and procedural macro logic, which I looked +at enough to convince myself it wasn't going to do anything dramatically wrong. +I don't think logic bugs in the version parsing etc can realistically introduce +a security vulnerability. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.rustversion]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.9 -> 1.0.14" +notes = "Doc updates, minimal CI changes and a fix to build-script reruns" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.selectors]] +who = "Emilio Cobos Álvarez " +criteria = "safe-to-deploy" +version = "0.22.0" +notes = """ +This crate is basically developed in-tree. Mozilla employees have either +reviewed or written virtually all of the code. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.servo_arc]] +who = "Emilio Cobos Álvarez " +criteria = "safe-to-deploy" +version = "0.1.1" +notes = "Developed in-tree, effectively." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.subtle]] +who = "Simon Friedberger " +criteria = "safe-to-deploy" +version = "2.5.0" +notes = "The goal is to provide some constant-time correctness for cryptographic implementations. The approach is reasonable, it is known to be insufficient but this is pointed out in the documentation." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +version = "0.1.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +delta = "0.1.0 -> 0.1.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.unicode-bidi]] +who = "Makoto Kato " +criteria = "safe-to-deploy" +delta = "0.3.8 -> 0.3.13" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.unicode-bidi]] +who = "Jonathan Kew " +criteria = "safe-to-deploy" +delta = "0.3.13 -> 0.3.14" +notes = "I am the author of the bulk of the upstream changes in this version, and also checked the remaining post-0.3.13 changes." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.unicode-bidi]] +who = "Jonathan Kew " +criteria = "safe-to-deploy" +delta = "0.3.14 -> 0.3.15" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.unicode-ident]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.8 -> 1.0.9" +notes = "Dependency updates only" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.url]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +version = "2.4.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.url]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +delta = "2.4.0 -> 2.4.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.url]] +who = "Valentin Gosu " +criteria = "safe-to-deploy" +delta = "2.4.1 -> 2.5.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.zcash.audits.block-buffer]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.10.3 -> 0.10.4" +notes = "Adds panics to prevent a block size of zero from causing unsoundness." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.bumpalo]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "3.15.4 -> 3.16.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.cipher]] +who = "Daira Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.0 -> 0.4.3" +notes = "Significant rework of (mainly RustCrypto-internal) APIs." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.cipher]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.4.3 -> 0.4.4" +notes = "Adds panics to prevent a block size of zero from causing unsoundness." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.cpufeatures]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.8 -> 0.2.9" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.cpufeatures]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.9 -> 0.2.11" +notes = """ +New `unsafe` block is to call `libc::getauxval(libc::AT_HWCAP)` on Linux for +LoongArch64 CPU feature detection support. This and the supporting macro code is +the same as the existing Linux code for AArch64. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.cpufeatures]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.2.11 -> 0.2.12" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.errno]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.3 -> 0.3.8" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.futures-channel]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.28 -> 0.3.29" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.futures-channel]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.29 -> 0.3.30" +notes = "Removes `build.rs` now that it can rely on the `target_has_atomic` attribute." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.futures-core]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.28 -> 0.3.29" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.futures-core]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.29 -> 0.3.30" +notes = "Removes `build.rs` now that it can rely on the `target_has_atomic` attribute." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.inout]] +who = "Daira Hopwood " +criteria = "safe-to-deploy" +version = "0.1.3" +notes = "Reviewed in full." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.libredox]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.0.1 -> 0.1.3" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.log]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.4.20 -> 0.4.21" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.opaque-debug]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.3.0 -> 0.3.1" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.pin-project-lite]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.2.13 -> 0.2.14" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.quote]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.31 -> 1.0.33" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.quote]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.33 -> 1.0.35" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.rand_xorshift]] +who = "Sean Bowe " +criteria = "safe-to-deploy" +version = "0.3.0" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.redox_users]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.4.4 -> 0.4.5" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.rustc-demangle]] +who = "Sean Bowe " +criteria = "safe-to-deploy" +delta = "0.1.21 -> 0.1.22" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.rustc-demangle]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.22 -> 0.1.23" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.rustc_version]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +version = "0.4.0" +notes = """ +Most of the crate is code to parse and validate the output of `rustc -vV`. The caller can +choose which `rustc` to use, or can use `rustc_version::{version, version_meta}` which will +try `$RUSTC` followed by `rustc`. + +If an adversary can arbitrarily set the `$RUSTC` environment variable then this crate will +execute arbitrary code. But when this crate is used within a build script, `$RUSTC` should +be set correctly by `cargo`. +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.sharded-slab]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.4 -> 0.1.7" +notes = "Only change to an `unsafe` block is to fix a clippy lint." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.time-core]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.1 -> 0.1.2" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.tinyvec_macros]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.1.0 -> 0.1.1" +notes = "Adds `#![forbid(unsafe_code)]` and license files." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.toml_datetime]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +version = "0.5.1" +notes = "Crate has `#![forbid(unsafe_code)]`, no `unwrap / expect / panic`, no ambient capabilities." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.toml_datetime]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.5.1 -> 0.6.1" +notes = "Fixes a bug in parsing negative minutes in datetime string offsets." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.toml_datetime]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.6.2 -> 0.6.3" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.tracing-subscriber]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.17 -> 0.3.18" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.try-lock]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.2.4 -> 0.2.5" +notes = "Bumps MSRV to remove unsafe code block." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.unicode-ident]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "1.0.9 -> 1.0.12" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.universal-hash]] +who = "Daira Hopwood " +criteria = "safe-to-deploy" +delta = "0.4.1 -> 0.5.0" +notes = "I checked correctness of to_blocks which uses unsafe code in a safe function." +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.want]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +delta = "0.3.0 -> 0.3.1" +notes = """ +Migrates to `try-lock 0.2.4` to replace some unsafe APIs that were not marked +`unsafe` (but that were being used safely). +""" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.wasm-bindgen-macro-support]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +version = "0.2.92" +aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml"

j#(n6`cTK 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 + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/MainActivity.kt b/crates/tauri-cli/templates/mobile/android/app/src/main/MainActivity.kt new file mode 100644 index 0000000..e414641 --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/app/src/main/MainActivity.kt @@ -0,0 +1,11 @@ +package {{escape-kotlin-keyword app.identifier}} + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge + +class MainActivity : TauriActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + } +} diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/crates/tauri-cli/templates/mobile/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/drawable/ic_launcher_background.xml b/crates/tauri-cli/templates/mobile/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/layout/activity_main.xml b/crates/tauri-cli/templates/mobile/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..4fc2444 --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..28f1aa119119336781390c2531c381499576ed59 GIT binary patch literal 3524 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 literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..28f1aa119119336781390c2531c381499576ed59 GIT binary patch literal 3524 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 Hu0mjfi}+A- literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..13dd21476b5c7f69cdfb82969bce7150244c127f GIT binary patch literal 9081 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 literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..73e48dbfb7dc31ed1e951b77820af88b93fcc418 GIT binary patch literal 3377 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 Hu0mjfi}+A- literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..1d98044f13bdd3afc7cb426644ae118688c03c14 GIT binary patch literal 7971 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 Z{{b+Ide7&7`HKJm002ovPDHLkV1iT#V`TsU literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..a888b336b5bc84b42c0c266ce9b8ec5d9eafed0b GIT binary patch literal 18900 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 I*#%+#2dtK;n*aa+ literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..1d98044f13bdd3afc7cb426644ae118688c03c14 GIT binary patch literal 7971 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 Z{{b+Ide7&7`HKJm002ovPDHLkV1iT#V`TsU literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..081832466b4a17180fe9255fa0d3fcba2010592c GIT binary patch literal 12392 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 K@hVZnfd2y&>ZwQo literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..a2a838e7b550ac82b12e73d71427c6ffea06a944 GIT binary patch literal 29506 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 K@hVZnfd2y&>ZwQo literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b18bceb64d257ed31d50e65d7f2b98abcfb21158 GIT binary patch literal 16751 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 XrJNsHnTG=pQeg0O^>bP0l+XkK^)z{o literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..3f8a57f38e0273699c8cdd1012042bffdc38abce GIT binary patch literal 40510 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

C z(CX6fIh8mroxe4Fy?l{bj%8R307X*fDQ#(v@y+x%4>XY?ax zC^p*eaCfrcq#aa-hT`A*7=V1wKdlx2BoX`y?{jb4s1j@)b7-etD6E@cKXx@8e&Vf zWehdcr8jm&_uNAX*$0NY%B&gn)>)@jQLcO5F3UZEr zds`B64=#T7qE6myk{5rNg+99@E{Zf&GEI$b?&PagH-fv#5k*V6EEmp!A*tE}RWk_SsM5#wN>{l7X5DBVl)bv~bNp*#NXY*I)h$Ng literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/crates/tauri-cli/templates/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..b18bceb64d257ed31d50e65d7f2b98abcfb21158 GIT binary patch literal 16751 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 XrJNsHnTG=pQeg0O^>bP0l+XkK^)z{o literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/values-night/themes.xml b/crates/tauri-cli/templates/mobile/android/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..75861db --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,6 @@ + + + + diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/values/colors.xml b/crates/tauri-cli/templates/mobile/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/values/strings.xml b/crates/tauri-cli/templates/mobile/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..b858fcb --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + "{{html-escape app.stylized-name}}" + "{{html-escape app.stylized-name}}" + \ No newline at end of file diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/values/themes.xml b/crates/tauri-cli/templates/mobile/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..75861db --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/app/src/main/res/values/themes.xml @@ -0,0 +1,6 @@ + + + + diff --git a/crates/tauri-cli/templates/mobile/android/app/src/main/res/xml/file_paths.xml b/crates/tauri-cli/templates/mobile/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..782d63b --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/crates/tauri-cli/templates/mobile/android/build.gradle.kts b/crates/tauri-cli/templates/mobile/android/build.gradle.kts new file mode 100644 index 0000000..50b46b6 --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/build.gradle.kts @@ -0,0 +1,24 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle:9.3.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.10") + {{~#each android-project-dependencies}} + classpath("{{this}}"){{/each}} + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +tasks.register("clean").configure { + delete("build") +} + diff --git a/crates/tauri-cli/templates/mobile/android/buildSrc/build.gradle.kts b/crates/tauri-cli/templates/mobile/android/buildSrc/build.gradle.kts new file mode 100644 index 0000000..cc18ea8 --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/buildSrc/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `kotlin-dsl` +} + +gradlePlugin { + plugins { + create("pluginsForCoolKids") { + id = "rust" + implementationClass = "RustPlugin" + } + } +} + +repositories { + google() + mavenCentral() +} + +dependencies { + compileOnly(gradleApi()) + implementation("com.android.tools.build:gradle:9.3.1") +} + diff --git a/crates/tauri-cli/templates/mobile/android/buildSrc/src/main/kotlin/BuildTask.kt b/crates/tauri-cli/templates/mobile/android/buildSrc/src/main/kotlin/BuildTask.kt new file mode 100644 index 0000000..023d96e --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/buildSrc/src/main/kotlin/BuildTask.kt @@ -0,0 +1,75 @@ +import java.io.File +import org.apache.tools.ant.taskdefs.condition.Os +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.logging.LogLevel +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import javax.inject.Inject +import org.gradle.process.ExecOperations + +abstract class BuildTask : DefaultTask() { + @get:Inject + abstract val execOperations: ExecOperations + + @Input + var rootDirRel: String? = null + @Input + var projectDir: String? = null + @Input + var target: String? = null + @Input + var release: Boolean? = null + + @TaskAction + fun assemble() { + val executable = """{{tauri-binary}}"""; + try { + runTauriCli(executable) + } catch (e: Exception) { + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + // Try different Windows-specific extensions + val fallbacks = listOf( + "$executable.exe", + "$executable.cmd", + "$executable.bat", + ) + + var lastException: Exception = e + for (fallback in fallbacks) { + try { + runTauriCli(fallback) + return + } catch (fallbackException: Exception) { + lastException = fallbackException + } + } + throw lastException + } else { + throw e; + } + } + } + + fun runTauriCli(executable: String) { + val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null") + val target = target ?: throw GradleException("target cannot be null") + val release = release ?: throw GradleException("release cannot be null") + val args = listOf({{quote-and-join tauri-binary-args}}); + + execOperations.exec { + workingDir(File(projectDir, rootDirRel)) + executable(executable) + args(args) + if (logger.isEnabled(LogLevel.DEBUG)) { + args("-vv") + } else if (logger.isEnabled(LogLevel.INFO)) { + args("-v") + } + if (release) { + args("--release") + } + args(listOf("--target", target)) + }.assertNormalExitValue() + } +} \ No newline at end of file diff --git a/crates/tauri-cli/templates/mobile/android/buildSrc/src/main/kotlin/RustPlugin.kt b/crates/tauri-cli/templates/mobile/android/buildSrc/src/main/kotlin/RustPlugin.kt new file mode 100644 index 0000000..34aba81 --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/buildSrc/src/main/kotlin/RustPlugin.kt @@ -0,0 +1,86 @@ +import com.android.build.api.dsl.ApplicationExtension +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.get + +const val TASK_GROUP = "rust" + +open class Config { + lateinit var rootDirRel: String +} + +open class RustPlugin : Plugin { + private lateinit var config: Config + + override fun apply(project: Project) = with(project) { + config = extensions.create("rust", Config::class.java) + + val defaultAbiList = listOf({{quote-and-join abi-list}}); + val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList + + val defaultArchList = listOf({{quote-and-join arch-list}}); + val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList + + val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf({{quote-and-join target-list}}) + + extensions.configure { + @Suppress("UnstableApiUsage") + flavorDimensions.add("abi") + productFlavors { + create("universal") { + dimension = "abi" + ndk { + abiFilters += abiList + } + } + defaultArchList.forEachIndexed { index, arch -> + create(arch) { + dimension = "abi" + ndk { + abiFilters.add(defaultAbiList[index]) + } + } + } + } + } + + afterEvaluate { + for (profile in listOf("debug", "release")) { + val profileCapitalized = profile.replaceFirstChar { it.uppercase() } + val buildTask = tasks.maybeCreate( + "rustBuildUniversal$profileCapitalized", + DefaultTask::class.java + ).apply { + group = TASK_GROUP + description = "Build dynamic library in $profile mode for all targets" + } + + tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask) + + for (targetPair in targetsList.withIndex()) { + val targetName = targetPair.value + val targetArch = archList[targetPair.index] + val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() } + val targetBuildTask = project.tasks.register( + "rustBuild$targetArchCapitalized$profileCapitalized", + BuildTask::class.java + ) { + group = TASK_GROUP + description = "Build dynamic library in $profile mode for $targetArch" + rootDirRel = config.rootDirRel + projectDir = project.projectDir.path + target = targetName + release = profile == "release" + } + + buildTask.dependsOn(targetBuildTask) + tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn( + targetBuildTask + ) + } + } + } + } +} \ No newline at end of file diff --git a/crates/tauri-cli/templates/mobile/android/gradle.properties b/crates/tauri-cli/templates/mobile/android/gradle.properties new file mode 100644 index 0000000..9e6e7b9 --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/gradle.properties @@ -0,0 +1,26 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.configuration-cache=true +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official + +# Our old plugin template had a bug that caused missing `consumer-rules.pro` files +# so we have to disable it for now. +# See https://github.com/tauri-apps/tauri/pull/15828#issuecomment-5199937796 +android.proguard.failOnMissingFiles=false + +# Keeping Gradle 9 compatible with current plugins +# TODO: Remove this in v3 +android.builtInKotlin=false +android.newDsl=false diff --git a/crates/tauri-cli/templates/mobile/android/gradle/wrapper/gradle-wrapper.jar b/crates/tauri-cli/templates/mobile/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..b1b8ef56b44f16b14dc800fa8103a6d89abb526f GIT binary patch literal 48462 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 R2np~f1o-$>Q}y+){|A@R9n$~+ literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/android/gradle/wrapper/gradle-wrapper.properties b/crates/tauri-cli/templates/mobile/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b2ee479 --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue May 10 19:22:52 CST 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/crates/tauri-cli/templates/mobile/android/gradlew b/crates/tauri-cli/templates/mobile/android/gradlew new file mode 100644 index 0000000..249efbb --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/crates/tauri-cli/templates/mobile/android/gradlew.bat b/crates/tauri-cli/templates/mobile/android/gradlew.bat new file mode 100644 index 0000000..a51ec4f --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/crates/tauri-cli/templates/mobile/android/settings.gradle b/crates/tauri-cli/templates/mobile/android/settings.gradle new file mode 100644 index 0000000..672366b --- /dev/null +++ b/crates/tauri-cli/templates/mobile/android/settings.gradle @@ -0,0 +1,5 @@ +include ':app' +{{~#each asset-packs}} +include ':{{this}}'{{/each}} + +apply from: 'tauri.settings.gradle' diff --git a/crates/tauri-cli/templates/mobile/ios/.gitignore b/crates/tauri-cli/templates/mobile/ios/.gitignore new file mode 100644 index 0000000..6726e2f --- /dev/null +++ b/crates/tauri-cli/templates/mobile/ios/.gitignore @@ -0,0 +1,3 @@ +xcuserdata/ +build/ +Externals/ diff --git a/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png b/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6ac2a8ccf0aff8558f948fbc2c9befd5aa34361 GIT binary patch literal 1036 zcmZ{jdrVt(6vux^=qiX3s2gLFAdJBT+`Ui-HZI6Ug^odBfSZdL7%)L)4%X?Ib07hA zS>`ZBnHwl(Atsws7r|Mcqr4pBu@3Bm(Lu|r&5C7>4eS1K87N#!3gJRqnirZfRZ3AHJyk7{dUZAP~mklTt{3QZIC&moJ2BpI4 z)7T&diw1YU1Vb4vUHR;=0r6vgtwj2|TL9ZCk}tdQOj`ZqCG5=P*OVy-a+E z0NS`jJA(%&MRKhJpwnzwu+yNuilxtk^3%T6$4~btYG;k7ru5!2{I_ zixcC7vFV_}$r&4c+iWkLwkRLle>bdq_9*w=pLuAd?F>_{SwdaoZ~v|RN@sjVrrb85 zKBk(#pf!|h^fAL4_dAO0iCLNfsfpSz*KY1p`}8Vf?rARRj10iYy44Awro;N#GrJv4 zNlHXtdITo3#U`;RRk%sd@%MhpEpPC&iXDfD5)-og$Az`ohuleC^}9c&>t8++S=6pS zTBB3w%&cmHqs-6Z`huHOE!FsH?8z`UUg^26Oi6q$m&+fTp{lAHK^p;oE=WZVs2eNxl-5Q%8oxqy*O0HcGt6UDF)iYE4;Xps+u*IXq%9 zqH^f^dmsJKG@$+Y=Dto*5em5a!EnnpY4bPDEpJUG2n6j#TdU09zti+3`-tlv81*OI19yi`MyisoB?7scSXN{UY5$gE3tm}TY*YfMv zf0Ja!Cbl}%bbqMFCEN0qC`qVp!DHU6AAfG*btdI9 zw6tTtZ%Nz$oQM-luiB38gB1^n0uf)EhInd}DZ ze}Iw?3yP1G|3Bcx-M*g%!k=sq7nc;C6z7$kc)~>Uv&a|$5iKCdf0q=`nkbe*B4JwM JFMD&V{sG_UJ5T@s literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png b/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000000000000000000000000000000000000..2869541f735d2f3f890a5b400fcced58698cbac4 GIT binary patch literal 2216 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 R+5rQcJL_y$amF|Ce*nJiS(X3* literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png b/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a4e68c8d6d5c4a564be190fdd200d7214eda2db2 GIT binary patch literal 3394 zcmZ{nc{J3G_s2iR8e#@R@({|t4cW^UF)?Yf3}r${9%6`$X2Otas8O~?Wd_-jtQpIU zkz`Ok3MK21?XgQ3W6jU^{LcBF^ZVm>?z#7U&b{|_-v8d4jC6K5EPPZL0D!~i;C3zt zYW|-T0v*(_L3aFsDEh-)&I15W6#xiH0I+?~BFqB7Ef@gI`yT9O1At6;4*lZkgMomb zlY;F_CIzUv_GSx@Pun4gU*iw=#V4JLc%meWMDl z>KhHP%>FrkFxVO#BLBXO3^8%RvUouA0-RN~7nVHMHzrTKgN(p)+yJy^dhcg-X5ze= zz}qiB;QW;$AJ4-d=g*)5!(68NY%qKko=B0ODIKsa(X|Jca$hD9Kt_HBg4KZCEysDB z>^987vN)aBsd9hpr?4WSCd(vzBf^L)Ne9gNr%eKWG{xK}7q7Dgdb# z%4SV-I5sHy>itK?t%Gl#EvqFi%*$rbp4}k8g;IN_Ln&=1sQmtl;X-lIGeh^qv?pu+w2w~_) zam21G{id+D-Qw|_`WKePVYo*Vx}0tE$$V2_Ii8AoZ|DXJVz%YB%g%M!}v6H-Y$!PCuMYxn(XjtH0?=8YjnnrVsT4P(z5eOV-k7c9 z_V`+!P_YkPj}OldPH4>!s2$PJGD7r--GQ@r+lu&_s8SAmNsJic+?9dVa{*k(8Fe!G zNuQiy$3U?<8=NVqPoD{7Y-QkfH8z=G5q8n)gQ9H#*Hc3ZaqQy2GD$=zH18@?d19db zssD8~8=g-kwW_lhmI5g?3hw z*wfUeX9xy-RdO-9jRINsA8A4yH^2+6N&KU{kJU9X3Xh{ zY`H)zW}0~jnDX|`{ABlD79`X9x$5Sp>Bm4zZ|D;sdsebi&Tqn@!rt|=e&|v!4Rqep z#P!Z5LRU(3iweqxZB15fc8@j!n zq}3Fw6upGdV0*|;?_btX`Sr?YyT5+D=obm%PDG(oD9Mgik|o4ZRviX(wi{wOJe=1mM%v+kgGk{vU=;m znqkrX;hQhb+ZMd_mr@$URyxsO%P8ECha*IKtOuQN_i%~umw9Roe9m3 z>k>i9WuRc0DKoC?Y}9V~kpQkl#6LcgVJB)Zh=2Ks8+C~%et#c;OD?2T8>A&K+&oK~ zLcT#KJ}qn|H0M~oTF8i)*G5H|2#hlPs*8Kv zlv{D~qh)@j3KT=vLwLP2DD{=eZj!PP1!KCGXJnfTeIHAnx=#WSw&cU$AqX z3TN*)6KKkMgUS;+?^dpJ?><4*183r`iH|C7?N`X>BTnWB5q0AK3OU2*v6od|xLsX4 z>uN?+)@qNt^Xk+BJ}o39u&-?{c5cIM&_w zZN#9PbB+q=8_vKlb<9 zSb7+7g;DdLe6LBvNKnI8?%EVNYU$2h_YZw&ziftO{Fay-egy5i+HRj{nCN|h{lpQ3 zJlrFkcSvis@oi(uhrCK#2yl+2yJTUkzrRfVzQit-_N!)jMEu#&L@oA5-I=dJ7|r>B1<)AbSPM-e-6$KB{%}00h36@yOO+lXTOeIswizc!WPWy8i(Vm?0-amvIaVF-Y)t>P8s}@tL+srB)*l3)mM81Ak$pE=BKLXH#wGw;eKeB*~_d`i1V{x;vqFmTAGhcF`yH@DBu+ zW#?G)nJAto>)DLG+-`+sx2g>e2|-$tdHY<(cv@IE({#ie3N-X?xSLvBRi##W&{@7&wCNS1 z*tgSW>D9afl7E5>Yz#EBJ*p+T-O}u?C$4M_t~O6je(ic%!0VF{MK&JTpWht6X;Co8 zCk>a_zq$5fiSp4sKV}YED+DDK_9dUr(4t-zjdL&EryoTNA;>7b(K>(AIUfUb z_V*HdTzOm9kzDmlTk8w1`H>2P^Zf(5h zHTgH>Hd4*&Q#&WIfv)`i7QI3Sm-lL1-tExV9vb76mwUPD*6J^XK=HSAx$CTnABAHJ za|#z8Btm@R10#=h>y|bMkHpyiti*~1(uC~{!6^Y2u?=oJ%*jayC-=vXn%+RFl~KO@ zs5YW0fpFmq-*~K=?rF12vmEyUb?)aM3IqDxGkE&|=DuJP+DWgsXXd&SH@7L9LxZC+ zsPf}g@vz7-cr=z?u*7iXtUN>i_Ht#-bq89=PcK@UL9+!N-o|sDj;oMst~La1mX_%K z6aR>31p5d>ZwaBeic50`V@jHB``DBKC%kK)JAS252Kca&+ z!~>}Gt5W2-86OedmzRl~qpV$-FI_Py8bvQp^(8^wD16RRKOuaG0^6)!&G!T1_9tg- z@xQ{klMMUrx^}kuh>Gm~o2r%0XCsZzKjJ=xBj-X?p-^?tYWwenyxA6dSdFsR(U5V6f|dTQI!vs4Q<#rBYuId1@4H<~xR2b^EqpwwUW z`rqNy)r7KePq}0{61*3rbmVMh@dqJ*71F$OxGl7(gZ8ZFa{K+gsM#(7Qaff){Ouo^ z*A_`Oy2$IOgO@@j>a1r}z}2X0=KeRY9SC3mGceSF8SB6d-C!o>hWh4)CR#9;^3SF)c@-c^>^sC$f&EK;s0d}4Bhlkn!^mtVfz1T^7rJ@ R+5rQcJL_y$amF|Ce*nJiS(X3* literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png b/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e4adcbcedc3780375b853ef315244b28a735f047 GIT binary patch literal 5228 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 W{J%q-7Pk)mF@QP-t6ZsQ9q~WT1(vM< literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png b/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..2869541f735d2f3f890a5b400fcced58698cbac4 GIT binary patch literal 2216 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 literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png b/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..704c929133f23779ee37099bf32d400b01599be1 GIT binary patch literal 121323 zcmeEug{m5g6SiNOwy&0@C$HTBK8?Bt|o8>^J=U z0l$CXd%<;WY%g9r&pGG5&pG$=K$Q9$C43xe8~^}-ucEA=2>^gmPr(2z5bEJ1@cS>+ z1EYnqrWye7ksSblh5`V$sF$F70Dz|e0I+Wg06b3z04Q9t+chLnZ(x|eR#E^UAHVWD z%aT!70#p=awS1Ni5%XzQ^PcP<1Kewx;ZmZ8&%c3{gV%ydRiZ3a3OQE|v|Bxt;B>2q zUr6KbE6P=im1%8Fm6U?AJ$yj$iIma8pQ|?9BjX0s(lGL<-%_XJdDN_uHqtdXo*NAa__0JDls!X0|&__N>fAEq;v;vayHQ zy3R`IWN|)oXSn@MmvfddOkByCe@9{n?+Klk_E|Yh@+sfQmWcX?Q+R<#Eek6){SeTN zl!yoUHojzC2WdbA+zJ=_ElP)2zwt?+JMiV97mt?y7LDO!WXSSUn_~(=)h-QD80ZJ= zu8eE~QEO;}pTz4#OW*V{0k-!J`Hrte+z{PfNMWd9Zd-?U9c<5^(g50|O5EVEbTdOJ zGnMlj8%rTL+6&X;Gc$7`vu^H!)eWx4u3ws~=w9kgfbV)gviB+Y0 zg2zvh{%Svd9KSg#>Tl}%A7-!B!<_&P!TpHkHNLYLBN}V0a(H&)46SfbqUzT-ZGx{? zopawUx5PN8Y~7MSWFbZ6s8xvK>Z9v%3IH;rg$10qf~m|P4eSz|NT3E0X?rx->2Cs~ zBo{m6FyM9F9bV$-f{a4w$6wQ1&7j>C|DH#(lQ?oxHLm@9+0dTEJ(_(Zl!S<952%xv zBC%fmx&Wq9wO({%RtweOc~vR!b4>G9^Y$E%-uyF5ktMV`ASvrlP+J&VVm?!=4MklO zp5R)i2QTR96eF-+PJ@YbwI~>a)ISsivI!EfEN=ius~5fc1<@|D0miWlzKfINVlA(D z1qHtfEw|f~;>IbL0sp4UqoOzdu!lo63VpSvL2%mB^UPbv7dO#;BFD~f=PC`kw-&x# z;5A#%KfG+Yc1i>#x1r`)GHew&mpj*ToDg>0S1~%?T_I&k*fdwWdOgb(!+do)nAU@`{f^!9w**eY&n=5l&QDkxn1X7YkYp-T|> zeSf9Zz-HFuiiIdD-zd$sjQ8!9rp#>}q`~T`kQ{T9LV3P_D6k)kXPVqJNL*oBep+dd zC6;K2Q;C=xJB$obY8Y}A35c}GC#7Ozs|e5MqSnfqvpSh_+G_WR(>1xx)1@Kun2?)KV$Dx~fA5o(HfgqQt>NIh z6z9AB$av)CRA$X<7R>tDY=kF+O$e8_k6F!(RfU8)u_H6Y%iDvn|MP)0lpYtbHm|L( zM+dLP1rQ`GRr(ZW-LL>IPR?aCqKPwKs->vgNZJAAfC@=MHotV)=y~Y!-hVOHGX19anYdY3v^B*zxT>UF zKK=C?;d?ZUX{&eNu@nk$uwn&(%=ye9d>KRfZz@wZ0$eY3+V1{54Z;e5e$6)mxExl% z&Sqr1)Wn2ek_!?M&Gzpw@o?NXnev&AR`#wsZo}k2*o`%BaS9?SvF86{5|G(rA|RT} z{{WerqG_OQSTGfAa9bG44Fi_2w%f_GQNNT|51sW4E{a$ak@jR!`Rk35?cSQKgKAM= zLFU$#zW0aW(;mA9`J)%>={vPSzHv9^)|a_W^O~Kl<#1*Wdo$-A$6q?E$LKjho)hk) zD(PSJ7K2Bs$oS50O{LnixqoPT-%T5t7upN!i}ca*?is~VeE#EX_(G<4I$*=VtN(tQ z#u=Z-B4oS2y+CBwhPMY_?cHkGulG-7I`XtbGRt}SF)^o=qx5jXDTT{P>wv`(0uIqn z8#j~mMV~5S9sHDLs2Fdcy!ugL(%C3g*DhICg;(_+!o!Ug@S*1!y!T&t-qd+}UGWpH zR;<>;PeRj!EbAP`;_BYB*8yqlIm+QyU2z{Vk3=*5e|0ord+2^Os;o-nx3x6N;|`6S1{0%{|1rcnJ`Fr#u4E4 zOkFwhfQzj%5a*?`9>ImA;fFcIJ3=w^ccvObzZl2Q2nX>-Ofm4O{sWRXT(CMRf1+#v zQnA!nxXS*(Pq9cX`n7CH8~HxGuPHc%nhReR{bQAo3uU%p;0IF?w_mOJ;=4|9|KelG z?6CN@TT;avb4mLx(lf*sFP%;GL(I)#CU4D<$Fm(r6$vsp#y4`u;Y_+wcjedCqj9>? zszE9XKZKh3Az^zBG`lX(r`!m-s`%FEU$=By@Xty7yoT@MPMP7Gd**hH1oP8pZTmLS z|6a(8fl{d{>!nKlVzuUNAfHm^ed))g(VS1P?9(+{U6obsAUuekx;y%CV;oqtKlrK^ zsb9l|W~p<~>qaH`%bQqY*D2|LHj9f^BROe04^5=!#FBw5Zl&88Em$^a_B&J*66uZ> zUR0MN!f6>^td6)>MTd$RJ=OpskT74?H7@|C8qmCwW8v8b#3QBdbz)4w(pmK9Ii8`H zk}`xt*8>@lFK$)&8MqVd(8c^tBD}vu$^5rJ7wqvHk`|&w1bWFSBSTQtOC^)ewCFYs ze3cx>N;8GO$G+*CvOCoceIWNU_)57WkbhO~Z}&t$9I|gxEt@16Karxs*uE^HBsFd6!Dtt@R=@N4xE}IE5u7-D}r8geNXsW`aHR?pM?} znFk=8=<;Uz=~`2A@+Ad?t2S2dHMBckHd>7WaQ? zziP-%dHFKzVU{Y&ApjDiWva86fEXpw|f9%1l&8JdaSVQ%3199>1I#;0db{Q z(Y6st2o1YfSzv(YB>|bdiEv~Jmi>FFrM(RFmQ0tmTZWi^rQ49FA-NYd63aBEb^T0o zkLK|H3oNWNYwHd@(dE;X8it$VM3gVN9(P&W+U}N+by`UP&G>TFI%$XDhJK=63I?-G zDCra+)pW94kuAUSPbrkyt11L(1Mb|7I)={f-q6|l$VEA^CRJ^Mqcy|0Nxz5^TpG?9d6Ck?+63ZqiXau#Zue5;eURr zr5iujyM$Lp;y~izDPtrA*2#lg#2;$kA653>+gYV0weY?*d#B6mOH>^<=Yi(%KI7rf zatT3%K7C2v`YzPpJA!r@;pxFF{XJmXkx8H+hziP-K2TK>_SqHE#~3PTRXx}E)Za%V z;MZ*eJUoFQ!oL6PdOxBl6S_{~0u%n%Y^>5$-jCX$&0Qi8RLa(8oy1vK1?ioXx~yT%pD zrz9YYMHJRUsv@HFH3<7Bn=lpmh;fM8zI>JO=fOWV^z!aZ_!G8*KZO3%v@74NV9!?) z0e&_@48yU-z4d(_VF&|0Js|&%Q@%uZP;+pc$9{Nh;>cc08k6RCbM&JI+`<0$bBX^} zSTXyKo+(RR&1?j5yyzkQRVM%R%st2A@5q88k&)q4LduLVDprL?(kEKa2(-9oft%J` z40#k3yMrWcpPKfhC8?caQNfrRt2$6U^9N1j5dXth$8VpOmojAPeP(E0Vv`(Y1CqE^ zv!#ZH{cEqqeWGI068WQyxGniN0OpCr`m)RpGSM2?M(lTHmAMZG99Q2+4qXPOG%l?@ zKge_-&_ETMDYCrE_;05967dhWPni3QI2qFRHK?S9c0Mgj*Scmmzx`{gfuW5bQ5FOf zeI_=aBX(7Ox@iI|pcb=;`?T4f-Vh@GCIS4n$I%+0HamlY=^ zIzPO>iG5&FHAJD*KP^CS@-f?1n582aTD}f(f%X0&L-qI_?+7~Xtwd%qD=qXfc=~@T zoU5J>?7w(S3bl;R=pB&-`!EOjQZd!D5iHC3#dWpgzn>9+_)C|Lnc!dxAR`c~BNLDU z(ct1y#u193%eS3QtRkM&U@*J+#zUWWg}|JC1y$^tJse^?BA!SgP$Q{un2 z7BAjNG=xX#(OxUJu9we39L@vS2VMdS2MTXR$q z+;5Hqc)zP0AD?%0W0Vn~n*(c>^IWiL$blbs=C+Z<4X;RP_*9-A3S{xiduAN zQ|!S7O}t?I4nhe$t>dS?Qpc({t-J%)Ovg{v+P;O494rJ5JqebyJawRBkmi)0xsamANJxnIFzk;#N@$gzw2BYZzVwxy$kFV!_sy>J)*+DP(M zjZSnojODf^t@HS?nvu0iNuS)9b^DKBx3ojSaf$TiK<=ks2FwDx`!;R@ylJYOqC9ja zpA;wd=7WevD!wMRY=EB>jMN#k@E3j}>C~}24HZdfI%0Y6WJxFIXQlP!zzHjHXBM#g zhW6iNfZ4k8^%aTcOIDjSUBQj~Udx7M+V8(i?#)KU5E4j!b8oO<|E(1N5Rr35d9&Si&1UZ?>wgect&O(!Qv#cWq%HkdE(l zyLw!v%)e#Acj;(8g;WtB)G&B#*CvnXcXeD&*st=2H=m79V)LbyzmA$B*3`HarerLJ zu3`ngz3AvDq+#$__`yG<96Gy^PuqmQpp?r(RlUA%kHudE$N4!-lIL*yfwxM-uV9%e zf7Sm_{{he#-vtIfc?52!w6(Q0=v`2`Txz2~aotl}iaY+{5$C({#?JeOB`>Ed;qhom z?H0fj*(uK(leFLPPu(36SkLCHt%G6VG8iO5RoZjWgOI`0FBBBNSsFJ|R`pEXQ-6cP2GVryU(Ob+H*?ZjrhezKR4-xg5((|wR$#1OG zmTd@y`I#v|prD^QF`=V>_akN&b2=F4xUp!Bl>`NnGp1;NXvJ#zX^3U|xYi;|1~+iG z_eQ2&91Uysu^h(7I3XGuvnRa_aqcjJ6N$$*!d(pSavs7+B!iL3{&gHjqUZkep=8YJ z2gir(9i6+6RJ~C8rnfZKTugEmTUKe^?(oi{tX@EVxH9Jx5bkHKrC^MhCr_+8(eS`uih4LxB&Zhr`Zv_lIvv0-`r(~I?sAv2dfqo`Z zJS1*&!F$11)H!hV=V(vxy#e_=)5}<+eAQsqPU?-77r#-(X2n*i?)5!)IaeZpGu{d@ ze_#SXk7?^1XfkQJ>(&47Y!z!z!%z78^m>j^-|Y8%tC1Mq1>(zmWvtDG$w<&tx_6;l9${!*c83T)iJS#9^w9!@t_`zqjRS;E+DLa5sd(l2&YO<7r^v*c!{ znoxJ|_3g5lTwKrjY>~g|KAFrQOJ~AOkI?)S(U49$_MN1vf9h;u*=%))#qZm#z5uN z^9s@p8no<=@ke0!nG%81W)MYSWno6Nxw)fF{oJnT&a$@i^C`&}iLI=bHtPaHBtPGi z1W2Ns9?@EbVc0E}|JN+~N+>fe#JJB~c6s#d35{e*U#YF$z(S6OjUps*J22awA@kAO zgzS9tx7O&|WQEX{o?afc)<6b!X`{-(C1;bCTRhSVpUPe4=r^T zU=J)BYF}#l#x^+k@@Dbx#(^v1-G{k|m++WW8njP}iSDicGz7v-ZR3709!liRf79e; zk~RAAgt#S;B{?tcMoji97!NY%M$abKJ8Kwt%Vn|)gC1OAP_mTN%FVucmheaa zt@g`rs1o;+-0iLg9+Oq1QJW9za9#yjg*1FXBuqGQfCV{ETKxFWv(_ObY; zsi)WQF+3S8K$#lh5BF}{rFe%j1v{tbZ>DGMLX@2W&YeMi>e-;)^rSODkitm&kAbEPc(#guo zXe7(%c^6`Eq;fd8e^_<>%j-C+{j}Nr14DJH7*=MsO1a&M0m?6cN7Hj0BC`Co84DCz zRm*ecV{#<@(K@;}C*bDi9&f#gn43OOQoT&qZeT<^sX|pO>XHs07Q6RUe$4Mxa63X5 zv(WkeI#~Pu4Cf~=ig{8+VnqZ2=Q|U4!9pH;3mBNiHr2LtDem?OV9YMd5J7eoAY>CT zYi}ag$=jwam;}JdAAX*Iz@BQmi}v7^-P;E`^eV`b%xn5G!KE|^bM3-SDis-)X~|Y| z2}a{I6oN6j$u?H19|sPY!v6CIl?n{(Y3TEQH#xuRRb-LTovY-vE59^}C(Z@cTwIW) z47W1KXKuaiju<@@q-DX%g%vW!efa+J%EL8k=C!WF?nOz4|95F|2J?afO=zl_;;9j; zN)i7fVtP<&M|_|iCs3uQ8S$|m9bF6L?jKs41hfV-#7;Bin{QIUgyza3ti#1lH~HvZlI z4*w2&(BzQjJBxXdBlp@ZrAj?rAjYz%>`ubl{69M~F`}m*K?-V#M#2)EJNph&qrTSd zdGk`afAn7IDJal(kZp8;vhQXd(J!IWPKD$uy6ujQIyhwAs|2kfv2oM;pmVGIAf+E?# z;78bV0Ax>YsgP7nJmYN3zC$$+oNtgt{F7wQBk;=YG0?x-_)jFbbL4r#y&@9@85K&t z*2$a`AG{V?PqofHzv2n>5D=#^Z~IQxqd=^lN3gZ>PpS3>Sw0oQfAsv1)Kj}{8>((-CZ%$P%J@t?=myMMMz?T0o z}aFlo5J4^by7(r3!t!pGO% zups#$AY*Vn^KFG*JwqnN?d6vnhs>EV59!)KNs8Skos=3-#>Hx?bAg^Vvd2t-nXooy z2%xhyVdje(wp~OtorlB?CMX^;$amsvDsT=8{o!9-l|^xTEJOx~MN=mzOEb(V+T8w) zGQhw6%G>qG!r%obpfshyMjf9XO?>|JF?$1ku{{74wB=_ZuLi!a!(KSg2hhR;#Q*D# zejXGbdyqkD{gPM&lKW_GYbFsXp1*}jNwsCr72BNik54+9Z*C?Dhu&WJ(Y%bUj|A9c zkVd9~vLE#k*u%^bTx5q*ixtwYQ7kN`aO8&LvyF$&R1p-ddX|cbf1oH_p*2h z$m?A&R!hFjm)S+cSz||MH(pj?z{C9?Z80n!{#cy9uYmB|APMickD`<}0~3&1b{4`) zY$HS-)9G;%e2B=tTTd8oWbl-q)yrVKh!5O-!-j+Ra}|d(GM9_YPZQ~?ZX%)k zDokwu_2eB%L zsql2P7B{6IcIUPP8XN}HiXn5CmZu6vN|~VKtT(ZNyDL9_FPMn@rNYo4XS<5A_ICbQ z?eu{-{q&3Pr8<@sUL?TQnmvH6C+F28J+Asy+DbnwN9C3^zW3U#>l$M18)8wslQzp= z7l4xt0_mxtc}%o%H=E*!G?)IGCCu~HQe`R2#|7gpY z84F~pC76OA6gy=+zT7=I|M22XfKaORP-6a{(0YN|)Vq(v-R`irf<%c-jlubN7)}-+ zOUk|F?MJLhK7A6FB$1D;XH^*`CDRUk<60K+2vPXwjO}c5h{i}{hsaWA8ioW?S1rKu zsPZ#BMwn1tnYEA~j;hE)f5QVnkU7)fSFk+Pyxn2qU%7C7?!KEBivxHz9>qvKYXR`K zd(^bNofBEr6k>Gc67kMIW)c{u95MXY9LTdIZIb~}e3oD^1636H=Skk8NT8ioKOmS9 zv%uf+7i@gDQFM}{p58e?GE`_l3ZNexRJew}^#<)9j)I(iN@rN`0LFWoJ8hQ`i#BIS znu!N_avd{uV6&mq9TBQH?n+VqodaW$u%bEQAZ)+;Od&stpNr8@bS;|H5bIf<^8#T_ z|4K^@GvH~TT;%FI^{knT4b2XOM?JE(|E4?qp54;Y>?zg2r|w#Jn7mMs%z!$zMT*{l z<${!$bCyGO*qmrr;wWz+1zJiMD#|$jqO84$mkKVe$IKTr>_Q5&A@PeoZX=o3Ml}lvT^WI4e3j1By>MpzMphGV#y*St+uj$g*R;hfCK)4kWWt( zkxcx9FKbfRLn!GWXJ={hY&MXmOqyo!H8IF);T2i2wn{$7Lyo(5B(^fxLN-=7(~aSh z0n@i$+2-6MFOP~~VgM3D*})5hTzWCWq5n8-f;q$eeE|k&o5w=1v(Fx>d-!1g%&+A? zKb(;Fb+F4XYZoXV;$kt8LacCEmF>5e0xnB(rUUszyL}(VSV{mgZ!YbG6Ndq?-hX2j z-LLHj3-#!haM2QB|CinYp+|u{qIt#fP2vs`D#Ib%pfT(U$U5l`J%s*|$w@yyD=P<)f&AQy5oN<2$o1*1h^qB) zEBPnnqdHs_a-eF@wcgSX%1<7&__`y185MzouVAb zD_seBJYKMJq*QnCCxIzbS!j$r*$YYNde<@gAZ>rnT&%PzMDiKPoA?5bS~rENOe7^& zzrc^Gy3Xd?R>$S0_mlDxOZQa+xkFMw{nvmK$6naLIZ8-qTb|_k@cLzB;uIw9iv4J9 zQj7z+dwapr{tZN;EL$zZXX5XV8opt<%%spM#^R=DDY!38bWDC$ALG$Kcxk@}B<^Ma ziRDk8LnRjfRmPmu{)d+qRed(89kxSUmof4<{R;&pAwd`wAMR$bbT}@FS3&Xg*tKjII8aCXvuFvGod& zO!xQJMV6MgngKO4Qa~ggeqE?t+lOgKY(_u>doec6_(r4exx`0U;8C2YAHX5$s{+=( zAXMV(KkHs77iHK%q5zr`Go5M6M+|A0$o9Dh)pI`aFmNgl>6X&c~rKXb6h>UPE)qq zl+&*$qvePW?%?fysQ8=Rv5v(FiunnUi!q&+H=65iwm(PxjuT?u{Qg=%&dgfX^nwfs z1D~M|&B4tUh2RVwE8R&ms~?u_k@~dvcYw)?xdBEqh)5LcF{(PP!a0>!TTD$M!RT40 zw=}xRL{E|J^nq18T-aE1AjMj=Z^=W)DBGk+S!}V#n63bnu17zdmnxA$lGpQQ=fMp` z7c%Htp^xa0VEWchm`_Sp8N^yK=jTy>b+nj)WU29Ys}3!!k5!F&Y@p8zX8}c_N+!=GZ+o|@! zy&jGLn&5&gq>G||V(I5St}z4LV`uFua3pJ9XRqJAFlpQCYA@RR0RthcrErQ$TEWM( z@LO;13N)*91u$k9bzCH0?MugTtTVLKas&T*^Fwp*yjyopvbLOZv*QCl+(wOeaxkT6 z$+G+ejP%VtY-IV-AY0uIDtG69FlG$RRbhppNJZ-30DdP=o&GXL8z%j>HE`A#QD%OO z$>PUSK6Q(&Js_7T2|r%@V(N>hvq4v|IW0Ef$>FKjsp3RzU<@B0${HdQyL&kw$VsTT zs+IwTYH-bH)Hynu(5ao_{ZX{y{}doesr2$Us$$%;cFJ7h*jg}1)zdbHPegP#NyxkUZluk|&uve3D*>P|MlVup zL-;5V3JT(re)t_o7zn$TKRveO04QKtCYl1PUM_b|-Os=tEF+FQ5Zr{$v4xzrj#tm2 z9ksqF13I*aR7VThngFKb0ZR{IMmJ?XCo=_|R9cY%=2Yr_FeGX6Hg@Llo8!IswbiKH z20UOpnq->4d4wzwm{`|e^o^E&{3v|=AiN22`fc;%_kW%Vc2qC_JG*;F$nblASv#H{ z=zBesbsxVO7CiGRG0a+1GhFscKET59s$Z`nGaKHQHa|q8(WH(-z+EOvjcqe9XB`j4 z)}%JQ1o9)`n@iHpV|m)kBwSVCfA)BMvz_mPy!vl?vFlC;THMoa*x})PFjP4O1&^!C zfSL1yQebs~KCj~d3pqa$0uEqt;&UV(tNlcgG9H&Z{od-4Jb^8KiR=RCAXc8^Pnt56 zp<4fOnF7(bgbec|eH*8q%=yQ#*U3Q}PBeB08SCBn-k=`Wbbf^^x4a3MjkaPFNnUFdMfRf4_)<9|O9lJ6rfw{N5^ zJgY?C$$^1|zf3jRLsfy=3JSrYjD^h1kZ59bHm?spdP4xC3S$C;kEs#9HP53gXJuFc zs_E!d0#h~_tfq=AjZ+|Z(s$H6$r$NEAzFVcC~>AO)@Miedw+-Q1=M|{jfj({r;%J} z?;#2gK17ENUCa&Lb3Bc=>kYX7aclF!hpuOJ`0;dLCnk$6m1;0C;Ec!Q7PI3lVQ>@9 z#jE$Tum{wz`|vhoR@H;e!*g$&Yt$(iPox|OewxU17{37kyo`4wSj3stl;@|gcQ=yW zSdvttL=zzmQNNJi3xkA!?K1ip?FlupwMMK=x+5#RUV>$`!1)=j+RS_Oz`@XJeUd5- z(I)ohm~8u<(s9Qu3XM$HDy8R@hXC6-gqF-Zo1%g+Y6WQBz4JUNE09z-AG;*RdF$jI zQv;XDM!0DY(pXZty|%QvZ)UsDINHnEXBhgW`hg0D|{m#K2yrCsb?rE6whMejDq2J6d(8`t8hNoE&*Df2Xdz|(9?Mle5j7*59K%vb3gm?YZ}472 zXGBU-0DTCZF)5>rECxvx&_EZ>eu^*AUN__yGcO}=GB!ha*|V2DSW7}LauFxniTNTmDm%r=}OshBE*pw-{e0g41^s`5)_x8 z>rBSSXwv}%!3n?>|Gvjqk0)7MS7@-Bw;jNB{_aZ;buuV9$lo<#MKhuZEQm3p?Gm!w4o!WbsIe!5v^1PZf}B4yz-G50d&p{{uK$uZ7TM zFHj7&C%4%tFZ)TayaZ(LA@yayByd=Enf;bG zBWZvuY#Cx0BQm|z^FAF+&IlQt<^_2e$%%X6Tv+)MLiw=GH6bBAtLf`Y>4@@{OAMhH z6Iy&sW-L^%QGu#K0UM}IFc`aXCRIAYTci#@!lUgV&6#tsn7_F2as`RqdViL*3dnuD{F|AZ79Bo}`$_iIv8v89>*IwkZs?_leb)+x45NzQ*bzo(HnOsN4 zYDI3TAM^b`+ra6uee0!`clIsr?s4a%C5Chdvr^t6bdfv|>PRRsQbvbiYIZHeiIk`V z@;Y>$P{fU6!j3O7rLNg@equQ=?>`Uf7>vM{XEJ!gjjD>5s1gK!WTVx{t%_r$033PX zG=H4vtiYtp77l%%2)JY!#b#Yqf2a{N0uUKYjL2f(D>7Tgl$L}g*}jz{-}-=#BnsX_~HC-w@s;9>9JAIU3iv%$g05E`AIpl%>A!q zEJiBBZih*deoUT!dD(*8wp>rMNPA&-b_0btyv?5&O|nZ+LD)Yvf(5C zZcJZgoo07X=X&%z*5&*5kK-ZvbZg}KhRBl@Y*%rL@(#i}l5-mSbA~1f0~I+sX^ILQ zoN@doGAEFpRf999$yC&iN>u8&W+WMO?91aZGIz<{UHmh$~$~nG-!(8qHlZKE&p~eT0Mza^uU)N|#Lc((^aSYm$L z8+Iq`%G8b`>?S@QKPmn@)sjd(cJnBJ*95#-JAKk#AlzeRyJ>T&D|X&kNf{YkZmP$O zg(~pK6P7(lgMik&`f+|I*WZdJX91Hp-z$7MS|y&1n|YdY>2mJ|WiSV0VMCa!aK9dKMj5g1jFAq4-$J-{uMDa*Q8`e>%a@)OS2wfG4}$ z&WM)IEB=F+0 zXZ=-SB0m({pu9`gfr1@E>w(P~-MmL%x{Qo=m;DXV2mm1p`5;_<0TN9Gh0x&8B0U3R z5^)p?!DhTR`E~pCAOA=kEh0PO^A453e0WK}AWnas+TB}o9>pNKObrEd9w4RQs=uHk z(ytzgDELmDq99#xwt|du`EL8r{t#d-Ao#jLD1vKPjhCw$9G;zGIb9-o-&MNww{?a;c{0wk^5g?pbc4~`{Xly-pwdyA<&*AMI4==e=zpeQ z9G_vFLfHAba*tVcF7 z2|sY+32w^}S-)?gN_BR$%<)ozm3|bf(&JGDQG(b46BLiotrlI*UnQ(1`KijF} z64~gxrkO;F>75@h#WU%O4V~2YrYP{%)7sl)Mv6(l_UJYHko#HDiH<*T#fynt0Ck

+ + + diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/jsconfig.json b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/jsconfig.json new file mode 100644 index 0000000..0882056 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/jsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "moduleResolution": "bundler", + "target": "ESNext", + "module": "ESNext", + /** + * svelte-preprocess cannot figure out whether you have + * a value or a type, so tell TypeScript to enforce using + * `import type` instead of `import` for Types. + */ + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + /** + * To have warnings / errors of the Svelte compiler at the + * correct position, enable source maps by default. + */ + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable this if you'd like to use dynamic types. + */ + "checkJs": true + }, + /** + * Use global.d.ts instead of compilerOptions.types + * to avoid limiting type declarations. + */ + "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] +} 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 new file mode 100644 index 0000000..c660a10 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/package.json @@ -0,0 +1,22 @@ +{ + "name": "tauri-app", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2.0.0", + "tauri-plugin-{{ plugin_name }}-api": "file:../../" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "svelte": "^5.0.0", + "vite": "^8.0.0", + "@tauri-apps/cli": "^2.0.0" + } +} diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/public/svelte.svg b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/public/svelte.svg new file mode 100644 index 0000000..c5e0848 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/public/svelte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/public/tauri.svg b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/public/tauri.svg new file mode 100644 index 0000000..31b62c9 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/public/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/public/vite.svg b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/.gitignore b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/.gitignore new file mode 100644 index 0000000..f4dfb82 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/Cargo.crate-manifest b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/Cargo.crate-manifest new file mode 100644 index 0000000..285b349 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/Cargo.crate-manifest @@ -0,0 +1,23 @@ +[package] +name = "tauri-app" +version = "0.1.0" +description = "A Tauri App" +authors = ["you"] +license = "" +repository = "" +edition = "2024" +rust-version = "1.90" + +[lib] +name = "tauri_app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +tauri-build = {{ tauri_build_dep }} + +[dependencies] +tauri = {{ tauri_example_dep }} +tauri-plugin-{{ plugin_name }} = { path = "../../../" } + diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/build.rs b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/build.rs new file mode 100644 index 0000000..795b9b7 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/capabilities/default.json b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/capabilities/default.json new file mode 100644 index 0000000..2a0c36a --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/capabilities/default.json @@ -0,0 +1,12 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "enables the default permissions", + "windows": [ + "main" + ], + "permissions": [ + "core:default", + "{{ plugin_name }}:default" + ] +} diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/icons/128x128.png b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..77e7d2338e9d8ccffc731198dc584906627c903f GIT binary patch literal 11059 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

>qIa)CJDQwHr3JKGv}<_ybIUd;~Q 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 literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png b/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2a9fbc26ef1ef34cd7b0e4413f40d7912e3a777b GIT binary patch literal 11091 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 literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png b/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..2cdf18485f69316a5ae921f83997baa4c0042e50 GIT binary patch literal 4515 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 literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png b/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4723e4b45ea6377219fcf9475c2c75ae075fed5e GIT binary patch literal 9257 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 SjxJs>07Y3fXqB{C=zjqou7cM9 literal 0 HcmV?d00001 diff --git a/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png b/crates/tauri-cli/templates/mobile/ios/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f26fee45c06c140414d4de5ef4a6911edb665b3e GIT binary patch literal 10219 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 + + + + method + debugging + + diff --git a/crates/tauri-cli/templates/mobile/ios/LaunchScreen.storyboard b/crates/tauri-cli/templates/mobile/ios/LaunchScreen.storyboard new file mode 100644 index 0000000..81b5f90 --- /dev/null +++ b/crates/tauri-cli/templates/mobile/ios/LaunchScreen.storyboard @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/tauri-cli/templates/mobile/ios/Podfile b/crates/tauri-cli/templates/mobile/ios/Podfile new file mode 100644 index 0000000..0c0df4e --- /dev/null +++ b/crates/tauri-cli/templates/mobile/ios/Podfile @@ -0,0 +1,25 @@ +# Uncomment the next line to define a global platform for your project + +target '{{app.name}}_iOS' do +platform :ios, '{{apple.ios-version}}' + # Pods for {{app.name}}_iOS + {{~#each ios-pods}} + pod '{{this.name}}'{{#if this.version}}, '{{this.version}}'{{/if}}{{/each}} +end + +target '{{app.name}}_macOS' do +platform :osx, '{{apple.macos-version}}' + # Pods for {{app.name}}_macOS + {{~#each macos-pods}} + pod '{{this.name}}'{{#if this.version}}, '{{this.version}}'{{/if}}{{/each}} +end + +# Delete the deployment target for iOS and macOS, causing it to be inherited from the Podfile +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' + config.build_settings.delete 'MACOSX_DEPLOYMENT_TARGET' + end + end +end diff --git a/crates/tauri-cli/templates/mobile/ios/Sources/{{app.name}}/bindings/bindings.h b/crates/tauri-cli/templates/mobile/ios/Sources/{{app.name}}/bindings/bindings.h new file mode 100644 index 0000000..5152200 --- /dev/null +++ b/crates/tauri-cli/templates/mobile/ios/Sources/{{app.name}}/bindings/bindings.h @@ -0,0 +1,8 @@ +#pragma once + +namespace ffi { + extern "C" { + void start_app(); + } +} + diff --git a/crates/tauri-cli/templates/mobile/ios/Sources/{{app.name}}/main.mm b/crates/tauri-cli/templates/mobile/ios/Sources/{{app.name}}/main.mm new file mode 100644 index 0000000..7793a9d --- /dev/null +++ b/crates/tauri-cli/templates/mobile/ios/Sources/{{app.name}}/main.mm @@ -0,0 +1,6 @@ +#include "bindings/bindings.h" + +int main(int argc, char * argv[]) { + ffi::start_app(); + return 0; +} diff --git a/crates/tauri-cli/templates/mobile/ios/project.yml b/crates/tauri-cli/templates/mobile/ios/project.yml new file mode 100644 index 0000000..067f282 --- /dev/null +++ b/crates/tauri-cli/templates/mobile/ios/project.yml @@ -0,0 +1,174 @@ +name: {{app.name}} +options: + bundleIdPrefix: {{app.identifier}} + deploymentTarget: + iOS: {{apple.ios-version}} +fileGroups: [{{join file-groups}}] +configs: + debug: debug + release: release +settingGroups: + app: + base: + PRODUCT_NAME: {{app.stylized-name}} + PRODUCT_BUNDLE_IDENTIFIER: {{app.identifier}} + {{#if apple.development-team}} + DEVELOPMENT_TEAM: {{apple.development-team}} + {{/if}} +targetTemplates: + app: + type: application + sources: + - path: Sources + scheme: + environmentVariables: + RUST_BACKTRACE: full + RUST_LOG: info + settings: + groups: [app] +targets: + {{app.name}}_iOS: + type: application + platform: iOS + sources: + - path: Sources + - path: Assets.xcassets + # Exclude built static libs: regenerating with xcodegen after a build + # otherwise turns the per-configuration libapp.a copies into colliding + # resources ("Multiple commands produce …/libapp.a"). They are linked + # via LIBRARY_SEARCH_PATHS, never bundled as resources. + - path: Externals + excludes: ["**/*.a"] + - path: {{app.name}}_iOS + - path: {{app.asset-dir}} + buildPhase: resources + type: folder + {{~#each asset-catalogs}} + - {{prefix-path this}}{{/each}} + {{~#each ios-additional-targets}} + - path: {{prefix-path this}}{{/each}} + - path: LaunchScreen.storyboard + info: + path: {{app.name}}_iOS/Info.plist + properties: + LSRequiresIPhoneOS: true + UILaunchStoryboardName: LaunchScreen + UIRequiredDeviceCapabilities: [arm64, metal] + UISupportedInterfaceOrientations: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight + UISupportedInterfaceOrientations~ipad: + - UIInterfaceOrientationPortrait + - UIInterfaceOrientationPortraitUpsideDown + - UIInterfaceOrientationLandscapeLeft + - UIInterfaceOrientationLandscapeRight + CFBundleShortVersionString: {{apple.bundle-version-short}} + CFBundleVersion: "{{apple.bundle-version}}" + {{~#each apple.plist-pairs}} + {{this.key}}: {{this.value}}{{/each}} + entitlements: + path: {{app.name}}_iOS/{{app.name}}_iOS.entitlements + scheme: + environmentVariables: + RUST_BACKTRACE: full + RUST_LOG: info + {{~#if ios-command-line-arguments}} + commandLineArguments: + {{~#each ios-command-line-arguments}} + "{{this}}": true + {{/each}}{{~/if}} + settings: + base: + 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 $(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] + dependencies: + - framework: {{ lib-output-file-name }} + embed: false + {{~#each ios-libraries}} + - framework: {{this}} + embed: false{{/each}}{{#if ios-vendor-frameworks}}{{~#each ios-vendor-frameworks}} + - framework: {{this}}{{/each}}{{/if}}{{#if ios-vendor-sdks}}{{~#each ios-vendor-sdks}} + - sdk: {{prefix-path this}}{{/each}}{{/if}} + - sdk: CoreGraphics.framework + - sdk: Metal.framework + - sdk: MetalKit.framework + - sdk: QuartzCore.framework + - sdk: Security.framework + - sdk: UIKit.framework{{#if this.ios-frameworks}}{{~#each ios-frameworks}} + - sdk: {{this}}.framework{{/each}}{{/if}} + - sdk: WebKit.framework + preBuildScripts: + {{~#each ios-pre-build-scripts}}{{#if this.path}} + - path {{this.path}}{{/if}}{{#if this.script}} + - script: {{this.script}}{{/if}}{{#if this.name}} + name: {{this.name}}{{/if}}{{#if this.input-files}} + inputFiles: {{~#each this.input-files}} + - {{this}}{{/each}}{{/if}}{{#if this.output-files}} + outputFiles: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.input-file-lists}} + inputFileLists: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.output-file-lists}} + outputFileLists: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.shell}} + shell: {{this.shell}}{{/if}}{{#if this.show-env-vars}} + showEnvVars: {{this.show_env_vars}}{{/if}}{{#if this.run-only-when-installing}} + runOnlyWhenInstalling: {{this.run-only-when-installing}}{{/if}}{{#if this.based-on-dependency-analysis}} + basedOnDependencyAnalysis: {{this.based-on-dependency-analysis}}{{/if}}{{#if this.discovered-dependency-file}} + discoveredDependencyFile: {{this.discovered-dependency-file}}{{/if}} + {{~/each}} + + - script: {{ tauri-binary }} {{ tauri-binary-args-str }} -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?} + name: Build Rust Code + basedOnDependencyAnalysis: false + outputFiles: + - $(SRCROOT)/Externals/x86_64/${CONFIGURATION}/{{ lib-output-file-name }} + - $(SRCROOT)/Externals/arm64/${CONFIGURATION}/{{ lib-output-file-name }} + {{~#if ios-post-compile-scripts}} + postCompileScripts: + {{~#each ios-post-compile-scripts}}{{#if this.path}} + - path {{this.path}}{{/if}}{{#if this.script}} + - script: {{this.script}}{{/if}}{{#if this.name}} + name: {{this.name}}{{/if}}{{#if this.input-files}} + inputFiles: {{~#each this.input-files}} + - {{this}}{{/each}}{{/if}}{{#if this.output-files}} + outputFiles: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.input-file-lists}} + inputFileLists: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.output-file-lists}} + outputFileLists: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.shell}} + shell: {{this.shell}}{{/if}}{{#if this.show-env-vars}} + showEnvVars: {{this.show_env_vars}}{{/if}}{{#if this.run-only-when-installing}} + runOnlyWhenInstalling: {{this.run-only-when-installing}}{{/if}}{{#if this.based-on-dependency-analysis}} + basedOnDependencyAnalysis: {{this.based-on-dependency-analysis}}{{/if}}{{#if this.discovered-dependency-file}} + discoveredDependencyFile: {{this.discovered-dependency-file}}{{/if}} + {{~/each~}} + {{~/if~}} + {{~#if ios-post-build-scripts}} + postBuildScripts: + {{~#each ios-post-build-scripts}}{{#if this.path}} + - path {{this.path}}{{/if}}{{#if this.script}} + - script: {{this.script}}{{/if}}{{#if this.name}} + name: {{this.name}}{{/if}}{{#if this.input-files}} + inputFiles: {{~#each this.input-files}} + - {{this}}{{/each}}{{/if}}{{#if this.output-files}} + outputFiles: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.input-file-lists}} + inputFileLists: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.output-file-lists}} + outputFileLists: {{~#each this.output-files}} + - {{this}}{{/each}}{{/if}}{{#if this.shell}} + shell: {{this.shell}}{{/if}}{{#if this.show-env-vars}} + showEnvVars: {{this.show_env_vars}}{{/if}}{{#if this.run-only-when-installing}} + runOnlyWhenInstalling: {{this.run-only-when-installing}}{{/if}}{{#if this.based-on-dependency-analysis}} + basedOnDependencyAnalysis: {{this.based-on-dependency-analysis}}{{/if}}{{#if this.discovered-dependency-file}} + discoveredDependencyFile: {{this.discovered-dependency-file}}{{/if}} + {{~/each~}} + {{~/if}} diff --git a/crates/tauri-cli/templates/mobile/ios/{{app.name}}.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/crates/tauri-cli/templates/mobile/ios/{{app.name}}.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..0225e89 --- /dev/null +++ b/crates/tauri-cli/templates/mobile/ios/{{app.name}}.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,12 @@ + + + + +{{#if apple.use-legacy-build-system}} + BuildSystemType + Original + DisableBuildSystemDeprecationDiagnostic + +{{/if}} + + diff --git a/crates/tauri-cli/templates/plugin/.github/workflows/audit.yml b/crates/tauri-cli/templates/plugin/.github/workflows/audit.yml new file mode 100644 index 0000000..3bc3a45 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/.github/workflows/audit.yml @@ -0,0 +1,34 @@ +{{{{raw}}}} +name: Audit + +on: + schedule: + - cron: '0 0 * * *' + push: + branches: + - main + paths: + - ".github/workflows/audit.yml" + - "**/Cargo.lock" + - "**/Cargo.toml" + pull_request: + branches: + - main + paths: + - ".github/workflows/audit.yml" + - "**/Cargo.lock" + - "**/Cargo.toml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: rustsec/audit-check@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} +{{{{/raw}}}} diff --git a/crates/tauri-cli/templates/plugin/.github/workflows/clippy.yml b/crates/tauri-cli/templates/plugin/.github/workflows/clippy.yml new file mode 100644 index 0000000..eed7681 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/.github/workflows/clippy.yml @@ -0,0 +1,55 @@ +{{{{raw}}}} +name: Check + +on: + push: + branches: + - main + paths: + - ".github/workflows/check.yml" + - "**/*.rs" + - "**/Cargo.toml" + pull_request: + branches: + - main + paths: + - ".github/workflows/check.yml" + - "**/*.rs" + - "**/Cargo.toml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + fmt: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --all -- --check + + clippy: + strategy: + fail-fast: false + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.platform }} + + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - name: install webkit2gtk + if: matrix.platform == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y webkit2gtk-4.1 + - uses: Swatinem/rust-cache@v2 + - run: cargo clippy --all-targets --all-features -- -D warnings +{{{{/raw}}}} diff --git a/crates/tauri-cli/templates/plugin/.github/workflows/test.yml b/crates/tauri-cli/templates/plugin/.github/workflows/test.yml new file mode 100644 index 0000000..4b17e7f --- /dev/null +++ b/crates/tauri-cli/templates/plugin/.github/workflows/test.yml @@ -0,0 +1,35 @@ +{{{{raw}}}} +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + strategy: + fail-fast: false + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.platform }} + + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + - name: install webkit2gtk + if: matrix.platform == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y webkit2gtk-4.1 + - uses: Swatinem/rust-cache@v2 + - run: cargo test --all-targets --all-features -- -D warnings +{{{{/raw}}}} diff --git a/crates/tauri-cli/templates/plugin/.gitignore b/crates/tauri-cli/templates/plugin/.gitignore new file mode 100644 index 0000000..50d8e32 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/.gitignore @@ -0,0 +1,17 @@ +/.vs +.DS_Store +.Thumbs.db +*.sublime* +.idea/ +debug.log +package-lock.json +.vscode/settings.json +yarn.lock + +/.tauri +/target +Cargo.lock +node_modules/ + +dist-js +dist diff --git a/crates/tauri-cli/templates/plugin/Cargo.crate-manifest b/crates/tauri-cli/templates/plugin/Cargo.crate-manifest new file mode 100644 index 0000000..8157582 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/Cargo.crate-manifest @@ -0,0 +1,17 @@ +[package] +name = "tauri-plugin-{{ plugin_name }}" +version = "0.1.0" +authors = [ "{{ author }}" ] +description = "" +edition = "2024" +rust-version = "1.90" +exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"] +links = "tauri-plugin-{{ plugin_name }}" + +[dependencies] +tauri = {{ tauri_dep }} +serde = "1.0" +thiserror = "2" + +[build-dependencies] +tauri-plugin = {{{ tauri_plugin_dep }}} diff --git a/crates/tauri-cli/templates/plugin/README.md b/crates/tauri-cli/templates/plugin/README.md new file mode 100644 index 0000000..84a8a04 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/README.md @@ -0,0 +1 @@ +# Tauri Plugin {{ plugin_name_original }} diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/.gitignore b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/.vscode/extensions.json b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/.vscode/extensions.json new file mode 100644 index 0000000..61343e9 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "svelte.svelte-vscode", + "tauri-apps.tauri-vscode", + "rust-lang.rust-analyzer" + ] +} diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/README.md b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/README.md new file mode 100644 index 0000000..72726a1 --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/README.md @@ -0,0 +1,8 @@ +# Svelte + Vite + +This template should help get you started developing with Tauri and Svelte in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer). + diff --git a/crates/tauri-cli/templates/plugin/__example-api/tauri-app/index.html b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/index.html new file mode 100644 index 0000000..fad1c5d --- /dev/null +++ b/crates/tauri-cli/templates/plugin/__example-api/tauri-app/index.html @@ -0,0 +1,14 @@ + + + + + + + Tauri + Svelte + + + +