Jelajahi Sumber

Merge remote-tracking branch 'origin/dev' into next

Lucas Nogueira 2 tahun lalu
induk
melakukan
3d5cc7ab86
100 mengubah file dengan 1664 tambahan dan 366 penghapusan
  1. 2 0
      .cargo/config
  2. 5 0
      .changes/add-dylib-support-to-macos-frameworks.md
  3. 5 0
      .changes/additional-args-api.md
  4. 5 0
      .changes/additional-args-config.md
  5. 7 0
      .changes/additional-args.md
  6. 5 0
      .changes/bundler-msi-mini-version.md
  7. 6 0
      .changes/cli-info.md
  8. 5 0
      .changes/cli-rustc-not-found-msg.md
  9. 6 0
      .changes/cli-sign-non-interactive.md
  10. 7 0
      .changes/cli-windows-paths-print.md
  11. 5 0
      .changes/content-protection-api.md
  12. 5 0
      .changes/content-protection-utils.md
  13. 7 0
      .changes/content-protection.md
  14. 5 0
      .changes/core-js-Map.md
  15. 5 0
      .changes/core-navigation-handler.md
  16. 5 0
      .changes/core-sync-windows-metadata.md
  17. 5 0
      .changes/custom-buttons-api.md
  18. 5 0
      .changes/custom-buttons.md
  19. 6 0
      .changes/custom-icon.md
  20. 6 0
      .changes/empty-tauri-key-password.md
  21. 7 0
      .changes/expose-set_device_event_filter-in-tauri-api.md
  22. 5 0
      .changes/fix-2549.md
  23. 6 0
      .changes/fix-dev-server-html-serialization.md
  24. 5 0
      .changes/heck-unicode.md
  25. 8 0
      .changes/is-minimized.md
  26. 5 0
      .changes/linux-mouse.md
  27. 9 0
      .changes/nsis-linux.md
  28. 8 0
      .changes/nsis.md
  29. 5 0
      .changes/open-links-mail-tel.md
  30. 5 0
      .changes/pin-os-info.md
  31. 5 0
      .changes/runtime-navigation-handler.md
  32. 5 0
      .changes/tauri-build-windows-manifest.md
  33. 6 0
      .changes/template-default-features.md
  34. 8 0
      .changes/title-getter.md
  35. 6 0
      .changes/tray-tooltip-runtime.md
  36. 5 0
      .changes/tray-tooltip.md
  37. 7 0
      .changes/url-getter.md
  38. 6 0
      .changes/version-inheritance.md
  39. 7 0
      .changes/webview-theme.md
  40. 5 0
      .changes/window-from-config.md
  41. 6 0
      .changes/wix-launch-app-checked.md
  42. 5 0
      .changes/wry-navigation-handler.md
  43. 1 1
      .github/FUNDING.yml
  44. 2 1
      .github/workflows/bench.yml
  45. 8 0
      .github/workflows/check-generated-files.yml
  46. 25 20
      .github/workflows/publish-cli-rs.yml
  47. 1 1
      .scripts/update-lockfiles.sh
  48. 2 1
      Cargo.toml
  49. 10 1
      SECURITY.md
  50. 144 7
      core/config-schema/schema.json
  51. 6 6
      core/tauri-build/Cargo.toml
  52. 177 75
      core/tauri-build/src/lib.rs
  53. 1 1
      core/tauri-build/src/static_vcruntime.rs
  54. 14 0
      core/tauri-build/src/window-app-manifest.xml
  55. 4 4
      core/tauri-codegen/Cargo.toml
  56. 10 5
      core/tauri-codegen/src/context.rs
  57. 5 3
      core/tauri-codegen/src/embedded_assets.rs
  58. 115 5
      core/tauri-runtime-wry/src/lib.rs
  59. 14 0
      core/tauri-runtime-wry/src/system_tray.rs
  60. 1 0
      core/tauri-runtime/Cargo.toml
  61. 56 0
      core/tauri-runtime/src/lib.rs
  62. 1 0
      core/tauri-runtime/src/menu.rs
  63. 13 0
      core/tauri-runtime/src/webview.rs
  64. 6 0
      core/tauri-runtime/src/window.rs
  65. 12 12
      core/tauri-utils/Cargo.toml
  66. 113 5
      core/tauri-utils/src/config.rs
  67. 13 1
      core/tauri-utils/src/lib.rs
  68. 1 1
      core/tauri-utils/src/pattern/isolation.js
  69. 5 2
      core/tauri-utils/src/pattern/isolation.rs
  70. 20 0
      core/tauri/CHANGELOG.md
  71. 6 3
      core/tauri/Cargo.toml
  72. 28 0
      core/tauri/build.rs
  73. 0 0
      core/tauri/scripts/bundle.global.js
  74. 0 0
      core/tauri/scripts/bundle.js
  75. 6 11
      core/tauri/scripts/core.js
  76. 11 0
      core/tauri/scripts/stringify-ipc-message-fn.js
  77. 9 1
      core/tauri/src/api/dialog.rs
  78. 1 2
      core/tauri/src/api/file/extract.rs
  79. 3 4
      core/tauri/src/api/ipc.rs
  80. 3 3
      core/tauri/src/api/notification.rs
  81. 2 2
      core/tauri/src/api/process/command.rs
  82. 1 1
      core/tauri/src/api/shell.rs
  83. 66 6
      core/tauri/src/app.rs
  84. 38 0
      core/tauri/src/app/tray.rs
  85. 30 5
      core/tauri/src/endpoints/dialog.rs
  86. 1 1
      core/tauri/src/endpoints/file_system.rs
  87. 18 14
      core/tauri/src/endpoints/window.rs
  88. 1 8
      core/tauri/src/event.rs
  89. 2 1
      core/tauri/src/lib.rs
  90. 15 3
      core/tauri/src/manager.rs
  91. 2 2
      core/tauri/src/plugin.rs
  92. 2 3
      core/tauri/src/scope/fs.rs
  93. 1 1
      core/tauri/src/scope/shell.rs
  94. 28 2
      core/tauri/src/test/mock_runtime.rs
  95. 35 26
      core/tauri/src/updater/core.rs
  96. 4 4
      core/tauri/src/updater/mod.rs
  97. 134 2
      core/tauri/src/window.rs
  98. 8 1
      core/tests/app-updater/src/main.rs
  99. 188 108
      core/tests/app-updater/tests/update.rs
  100. 0 0
      examples/api/dist/assets/index.js

+ 2 - 0
.cargo/config

@@ -0,0 +1,2 @@
+[env]
+__TAURI_WORKSPACE__ = "true"

+ 5 - 0
.changes/add-dylib-support-to-macos-frameworks.md

@@ -0,0 +1,5 @@
+---
+"tauri-bundler": patch
+---
+
+Add dylib support to `tauri.bundle.macOS.frameworks`.

+ 5 - 0
.changes/additional-args-api.md

@@ -0,0 +1,5 @@
+---
+"api": minor
+---
+
+Added the `additionalBrowserArgs` option when creating a window.

+ 5 - 0
.changes/additional-args-config.md

@@ -0,0 +1,5 @@
+---
+"tauri-utils": minor
+---
+
+Added the `additional_browser_args` option to the window configuration.

+ 7 - 0
.changes/additional-args.md

@@ -0,0 +1,7 @@
+---
+"tauri": minor
+"tauri-runtime-wry": minor
+"tauri-runtime": minor
+---
+
+Added the `additional_browser_args` option when creating a window.

+ 5 - 0
.changes/bundler-msi-mini-version.md

@@ -0,0 +1,5 @@
+---
+'tauri-bundler': 'minor'
+---
+
+Added support for pre-release identifiers and build numbers for the `.msi` bundle target. Only one of each can be used and it must be numeric only. The version must still be semver compatible according to https://semver.org/.

+ 6 - 0
.changes/cli-info.md

@@ -0,0 +1,6 @@
+---
+"cli.rs": "patch"
+---
+
+Fix `tauri info` panicking when parsing crates version on a newly created project without a `Cargo.lock` file.
+

+ 5 - 0
.changes/cli-rustc-not-found-msg.md

@@ -0,0 +1,5 @@
+---
+"cli.rs": "patch"
+---
+
+Improve the error message when `rustc` couldn't be found.

+ 6 - 0
.changes/cli-sign-non-interactive.md

@@ -0,0 +1,6 @@
+---
+"cli.rs": patch
+"cli.js": patch
+---
+
+Add `--ci` flag and respect the `CI` environment variable on the `signer generate` command. In this case the default password will be an empty string and the CLI will not prompt for a value.

+ 7 - 0
.changes/cli-windows-paths-print.md

@@ -0,0 +1,7 @@
+---
+"cli.rs": "patch"
+"tauri-bundler": "patch"
+---
+
+On Windows, printing consistent paths on Windows with backslashs only.
+

+ 5 - 0
.changes/content-protection-api.md

@@ -0,0 +1,5 @@
+---
+"api": "minor"
+---
+
+Added the `WindowOptions::contentProtected` option and `WebviewWindow#setContentProtected` to change it at runtime.

+ 5 - 0
.changes/content-protection-utils.md

@@ -0,0 +1,5 @@
+---
+"tauri-utils": "patch"
+---
+
+Added the `content_protected` option to the window configuration.

+ 7 - 0
.changes/content-protection.md

@@ -0,0 +1,7 @@
+---
+"tauri": "minor"
+"tauri-runtime": "minor"
+"tauri-runtime-wry": "minor"
+---
+
+Added the `content_protected` option when creating a window and `Window::set_content_protected` to change it at runtime.

+ 5 - 0
.changes/core-js-Map.md

@@ -0,0 +1,5 @@
+---
+"tauri": "patch"
+---
+
+Fix serialization of js `Map` when used in `invoke`.

+ 5 - 0
.changes/core-navigation-handler.md

@@ -0,0 +1,5 @@
+---
+"tauri": minor
+---
+
+Added `Window::on_navigation`.

+ 5 - 0
.changes/core-sync-windows-metadata.md

@@ -0,0 +1,5 @@
+---
+"tauri": "patch"
+---
+
+Sync `__TAURI_METADATA__.__windows` across all windows.

+ 5 - 0
.changes/custom-buttons-api.md

@@ -0,0 +1,5 @@
+---
+"api": minor
+---
+
+Allow setting the text of the dialog buttons.

+ 5 - 0
.changes/custom-buttons.md

@@ -0,0 +1,5 @@
+---
+"tauri": minor
+---
+
+Added `OkWithLabel` and `OkCancelWithLabels` variants to the `api::dialog::MessageDialogButtons` enum to set the text of the dialog buttons.

+ 6 - 0
.changes/custom-icon.md

@@ -0,0 +1,6 @@
+---
+"cli.rs": minor
+"cli.js": minor
+---
+
+Add `--png` option for the `icon` command to generate custom icon sizes.

+ 6 - 0
.changes/empty-tauri-key-password.md

@@ -0,0 +1,6 @@
+---
+"cli.rs": patch
+"cli.js": patch
+---
+
+Skip the password prompt on the build command when `TAURI_KEY_PASSWORD` environment variable is empty and the `--ci` argument is provided or the `CI` environment variable is set.

+ 7 - 0
.changes/expose-set_device_event_filter-in-tauri-api.md

@@ -0,0 +1,7 @@
+---
+"tauri-runtime-wry": minor
+"tauri-runtime": minor
+"tauri": minor
+---
+
+Added `Builder::device_event_filter` and `App::set_device_event_filter` methods.

+ 5 - 0
.changes/fix-2549.md

@@ -0,0 +1,5 @@
+---
+"tauri": "patch"
+---
+
+Fix resize glitch when double clicking a custom titlebar in the top resize area.

+ 6 - 0
.changes/fix-dev-server-html-serialization.md

@@ -0,0 +1,6 @@
+---
+"cli.rs": patch
+"cli.js": patch
+---
+
+Fixes HTML serialization removing template tags on the dev server.

+ 5 - 0
.changes/heck-unicode.md

@@ -0,0 +1,5 @@
+---
+"cli.rs": "patch"
+---
+
+Fix building apps with unicode characters in their `productName`.

+ 8 - 0
.changes/is-minimized.md

@@ -0,0 +1,8 @@
+---
+"tauri": minor
+"tauri-runtime": minor
+"tauri-runtime-wry": minor
+"api": minor
+---
+
+Add `is_minimized()` window method.

+ 5 - 0
.changes/linux-mouse.md

@@ -0,0 +1,5 @@
+---
+"tauri-runtime-wry": patch
+---
+
+Disable cursor mouse events on Linux.

+ 9 - 0
.changes/nsis-linux.md

@@ -0,0 +1,9 @@
+---
+"tauri-bundler": minor
+"tauri-utils": minor
+"cli.rs": minor
+"cli.js": minor
+"tauri-build": minor
+---
+
+Add initial support for building `nsis` bundles on non-Windows platforms.

+ 8 - 0
.changes/nsis.md

@@ -0,0 +1,8 @@
+---
+"tauri-bundler": minor
+"tauri-utils": minor
+"cli.rs": minor
+"cli.js": minor
+---
+
+Add `nsis` bundle target

+ 5 - 0
.changes/open-links-mail-tel.md

@@ -0,0 +1,5 @@
+---
+"tauri": minor
+---
+
+Added support to `mailto:` and `tel:` links on the shell API.

+ 5 - 0
.changes/pin-os-info.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Pin `os_info` to `=3.5`.

+ 5 - 0
.changes/runtime-navigation-handler.md

@@ -0,0 +1,5 @@
+---
+"tauri-runtime": minor
+---
+
+Added `navigation_handler` field on `PendingWindow`.

+ 5 - 0
.changes/tauri-build-windows-manifest.md

@@ -0,0 +1,5 @@
+---
+"tauri-build": "minor"
+---
+
+Add `WindowsAttributes::app_manifest` to specify the application manifest on Windows.

+ 6 - 0
.changes/template-default-features.md

@@ -0,0 +1,6 @@
+---
+'cli.rs': minor
+'cli.js': minor
+---
+
+Remove default features from Cargo.toml template.

+ 8 - 0
.changes/title-getter.md

@@ -0,0 +1,8 @@
+---
+"tauri": "minor"
+"api": "minor"
+"tauri-runtime": "minor"
+"tauri-runtime-wry": "minor"
+---
+
+Add `title` getter on window.

+ 6 - 0
.changes/tray-tooltip-runtime.md

@@ -0,0 +1,6 @@
+---
+"tauri-runtime": minor
+"tauri-runtime-wry": minor
+---
+
+Added `TrayHandle::set_tooltip` and `SystemTray::with_tooltip`.

+ 5 - 0
.changes/tray-tooltip.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Implement `SystemTray::with_tooltip` and `SystemTrayHandle::set_tooltip` for Windows and macOS.

+ 7 - 0
.changes/url-getter.md

@@ -0,0 +1,7 @@
+---
+"tauri-runtime": minor
+"tauri-runtime-wry": minor
+"tauri": minor
+---
+
+Added window's `url()` getter.

+ 6 - 0
.changes/version-inheritance.md

@@ -0,0 +1,6 @@
+---
+'cli.rs': 'minor'
+'tauri-build': 'minor'
+---
+
+Added support for Cargo's workspace inheritance for package information. The cli now also detects inherited `tauri` and `tauri-build` dependencies and disables manifest rewrites accordingly.

+ 7 - 0
.changes/webview-theme.md

@@ -0,0 +1,7 @@
+---
+"tauri": "patch"
+"tauri-runtime": "patch"
+"tauri-runtime-wry": "patch"
+---
+
+On Windows, change webview theme based on Window theme for more accurate `prefers-color-scheme` support.

+ 5 - 0
.changes/window-from-config.md

@@ -0,0 +1,5 @@
+---
+'tauri': 'minor'
+---
+
+Add a method to the `WindowBuilder` struct to recreate windows from tauri.conf.json configurations.

+ 6 - 0
.changes/wix-launch-app-checked.md

@@ -0,0 +1,6 @@
+---
+"tauri-bundler": "patch"
+---
+
+On Windows, the `msi` installer's `Launch App` checkbox will be checked by default.
+

+ 5 - 0
.changes/wry-navigation-handler.md

@@ -0,0 +1,5 @@
+---
+"tauri-runtime-wry": minor
+---
+
+Implement the webview navigation handler.

+ 1 - 1
.github/FUNDING.yml

@@ -1,6 +1,6 @@
 # These are supported funding model platforms
 
-github: nothingismagick
+github: tauri-apps
 patreon: #
 open_collective: tauri
 ko_fi: # Replace with a single Ko-fi username

+ 2 - 1
.github/workflows/bench.yml

@@ -10,6 +10,7 @@ on:
 env:
   RUST_BACKTRACE: 1
   CARGO_PROFILE_DEV_DEBUG: 0 # This would add unnecessary bloat to the target folder, decreasing cache efficiency.
+  LC_ALL: en_US.UTF-8 # This prevents strace from changing it's number format to use commas.
 
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
@@ -22,7 +23,7 @@ jobs:
       matrix:
         rust: [nightly]
         platform:
-          - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest }
+          - { target: x86_64-unknown-linux-gnu, os: ubuntu-20.04 }
 
     runs-on: ${{ matrix.platform.os }}
 

+ 8 - 0
.github/workflows/check-generated-files.yml

@@ -5,6 +5,14 @@
 name: Check generated files
 
 on:
+  pull_request:
+    paths:
+      - '.github/workflows/check-generated-files.yml'
+      - 'tooling/api/src/**'
+      - 'core/tauri/scripts/bundle.global.js'
+      - 'core/tauri-utils/src/config.rs'
+      - 'tooling/cli/schema.json'
+      - 'core/config-schema/schema.json'
   push:
     paths:
       - '.github/workflows/check-generated-files.yml'

+ 25 - 20
.github/workflows/publish-cli-rs.yml

@@ -8,40 +8,45 @@ on:
 
 jobs:
   build:
-    runs-on: ${{ matrix.target.platform }}
+    runs-on: ${{ matrix.config.os }}
 
     strategy:
       fail-fast: false
       matrix:
-        target:
-          - {
-              name: 'x86_64-unknown-linux-gnu',
-              platform: 'ubuntu-18.04',
-              ext: ''
-            }
-          - { name: 'x86_64-apple-darwin', platform: 'macos-latest', ext: '' }
-          - {
-              name: 'x86_64-pc-windows-msvc',
-              platform: 'windows-latest',
-              ext: '.exe'
-            }
+        config:
+          - os: ubuntu-18.04
+            rust_target: x86_64-unknown-linux-gnu
+            ext: ''
+          - os: macos-latest
+            rust_target: x86_64-apple-darwin
+            ext: ''
+          - os: macos-latest
+            rust_target: aarch64-apple-darwin
+            ext: ''
+          - os: windows-latest
+            rust_target: x86_64-pc-windows-msvc
+            ext: '.exe'
 
     steps:
       - name: Checkout
         uses: actions/checkout@v3
 
-      - name: install stable
-        uses: actions-rs/toolchain@v1
+      - name: 'Setup Rust'
+        uses: dtolnay/rust-toolchain@stable
         with:
-          toolchain: stable
+          targets: ${{ matrix.config.rust_target }}
+          
+      - uses: Swatinem/rust-cache@v2
+        with:
+          key: ${{ matrix.config.rust_target }}
 
       - name: install Linux dependencies
-        if: matrix.target.platform == 'ubuntu-latest'
+        if: matrix.config.os == 'ubuntu-latest'
         run: |
           sudo apt-get update
           sudo apt-get install -y libgtk-3-dev
 
-      - name: build CLI
+      - name: Build CLI
         uses: actions-rs/cargo@v1
         with:
           command: build
@@ -50,8 +55,8 @@ jobs:
       - name: Upload CLI
         uses: actions/upload-artifact@v3
         with:
-          name: cargo-tauri-${{ matrix.target.name }}${{ matrix.target.ext }}
-          path: tooling/cli/target/release/cargo-tauri${{ matrix.target.ext }}
+          name: cargo-tauri-${{ matrix.config.rust_target }}${{ matrix.config.ext }}
+          path: tooling/cli/target/release/cargo-tauri${{ matrix.config.ext }}
           if-no-files-found: error
 
   upload:

+ 1 - 1
.scripts/update-lockfiles.sh

@@ -3,7 +3,7 @@
 # SPDX-License-Identifier: Apache-2.0
 # SPDX-License-Identifier: MIT
 
-declare -a examples=("api" "sidecar" "updater" "resources" "tauri-dynamic-lib")
+declare -a examples=("api" "sidecar" "updater" "resources" "tauri-dynamic-lib" "workspace")
 declare -a tooling=("bench" "cli" "webdriver")
 
 for example in "${examples[@]}"

+ 2 - 1
Cargo.toml

@@ -21,7 +21,8 @@ exclude = [
   "examples/updater/src-tauri",
   "examples/resources/src-tauri",
   "examples/sidecar/src-tauri",
-  "examples/web/core"
+  "examples/web/core",
+  "examples/workspace"
 ]
 
 # default to small, optimized workspace release binaries

+ 10 - 1
SECURITY.md

@@ -13,7 +13,16 @@ If you have found a potential security threat, vulnerability or exploit in Tauri
 or one of its upstream dependencies, please DON’T create a pull-request, DON’T
 file an issue on GitHub, DON’T mention it on Discord and DON’T create a forum thread.
 
-We will be adding contact information to this page very soon.
+Please submit your report via the GitHub Private Vulnerability Disclosure functionality.
+
+Find out more about the reporting process [here](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability).
+
+Our team will triage your report and keep you informed about the progress.
+We may ask questions or request further guidance on reproduction of the vulnerability in the comments of the advisory, which will be publicized.
+
+Additionally, we may ask you to independently verify our patch, which will be available in the private advisory branch. Please do not publish your vulnerability during the process or before coordinated public disclosure from our side. We try to adhere to common standards of publication within 90-Days of disclosure.
+
+Depending on your decision to accept or deny credit for the vulnerability, you will be publicly attributed to the vulnerability and may be mentioned in our announcements.
 
 At the current time we do not have the financial ability to reward bounties, 
 but in extreme cases will at our discretion consider a reward.

+ 144 - 7
core/config-schema/schema.json

@@ -105,6 +105,7 @@
             "print": false,
             "requestUserAttention": false,
             "setAlwaysOnTop": false,
+            "setContentProtected": false,
             "setCursorGrab": false,
             "setCursorIcon": false,
             "setCursorPosition": false,
@@ -147,6 +148,7 @@
             "allowDowngrades": true,
             "certificateThumbprint": null,
             "digestAlgorithm": null,
+            "nsis": null,
             "timestampUrl": null,
             "tsp": false,
             "webviewFixedRuntimePath": null,
@@ -170,7 +172,8 @@
           "dialog": true,
           "pubkey": "",
           "windows": {
-            "installMode": "passive"
+            "installMode": "passive",
+            "installerArgs": []
           }
         },
         "windows": []
@@ -284,6 +287,7 @@
               "allowDowngrades": true,
               "certificateThumbprint": null,
               "digestAlgorithm": null,
+              "nsis": null,
               "timestampUrl": null,
               "tsp": false,
               "webviewFixedRuntimePath": null,
@@ -381,6 +385,7 @@
               "print": false,
               "requestUserAttention": false,
               "setAlwaysOnTop": false,
+              "setContentProtected": false,
               "setCursorGrab": false,
               "setCursorIcon": false,
               "setCursorPosition": false,
@@ -429,7 +434,8 @@
             "dialog": true,
             "pubkey": "",
             "windows": {
-              "installMode": "passive"
+              "installMode": "passive",
+              "installerArgs": []
             }
           },
           "allOf": [
@@ -645,6 +651,11 @@
           "default": false,
           "type": "boolean"
         },
+        "contentProtected": {
+          "description": "Prevents the window contents from being captured by other apps.",
+          "default": false,
+          "type": "boolean"
+        },
         "skipTaskbar": {
           "description": "If `true`, hides the window icon from the taskbar on Windows and Linux.",
           "default": false,
@@ -687,6 +698,13 @@
             "null"
           ]
         },
+        "additionalBrowserArgs": {
+          "description": "Defines additional browser arguments on Windows. 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.",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
         "shadow": {
           "description": "Whether or not the window has shadow.\n\n## Platform-specific\n\n- **Windows:** - `false` has no effect on decorated window, shadow are always ON. - `true` will make ndecorated window have a 1px white border, and on Windows 11, it will have a rounded corners. - **Linux:** Unsupported.",
           "default": false,
@@ -1013,7 +1031,7 @@
           "type": "boolean"
         },
         "targets": {
-          "description": "The bundle targets, currently supports [\"deb\", \"appimage\", \"msi\", \"app\", \"dmg\", \"updater\"] or \"all\".",
+          "description": "The bundle targets, currently supports [\"deb\", \"appimage\", \"nsis\", \"msi\", \"app\", \"dmg\", \"updater\"] or \"all\".",
           "default": "all",
           "allOf": [
             {
@@ -1127,6 +1145,7 @@
             "allowDowngrades": true,
             "certificateThumbprint": null,
             "digestAlgorithm": null,
+            "nsis": null,
             "timestampUrl": null,
             "tsp": false,
             "webviewFixedRuntimePath": null,
@@ -1204,6 +1223,13 @@
             "msi"
           ]
         },
+        {
+          "description": "The NSIS bundle (.exe).",
+          "type": "string",
+          "enum": [
+            "nsis"
+          ]
+        },
         {
           "description": "The macOS application bundle (.app).",
           "type": "string",
@@ -1388,6 +1414,17 @@
               "type": "null"
             }
           ]
+        },
+        "nsis": {
+          "description": "Configuration for the installer generated with NSIS.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/NsisConfig"
+            },
+            {
+              "type": "null"
+            }
+          ]
         }
       },
       "additionalProperties": false
@@ -1636,6 +1673,91 @@
       },
       "additionalProperties": false
     },
+    "NsisConfig": {
+      "description": "Configuration for the Installer bundle using NSIS.",
+      "type": "object",
+      "properties": {
+        "license": {
+          "description": "The path to the license file to render on the installer.",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "headerImage": {
+          "description": "The path to a bitmap file to display on the header of installers pages.\n\nThe recommended dimensions are 150px x 57px.",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "sidebarImage": {
+          "description": "The path to a bitmap file for the Welcome page and the Finish page.\n\nThe recommended dimensions are 164px x 314px.",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "installerIcon": {
+          "description": "The path to an icon file used as the installer icon.",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "installMode": {
+          "description": "Whether the installation will be for all users or just the current user.",
+          "default": "currentUser",
+          "allOf": [
+            {
+              "$ref": "#/definitions/NSISInstallerMode"
+            }
+          ]
+        },
+        "languages": {
+          "description": "A list of installer languages. By default the OS language is used. If the OS language is not in the list of languages, the first language will be used. To allow the user to select the language, set `display_language_selector` to `true`.\n\nSee <https://github.com/kichik/nsis/tree/9465c08046f00ccb6eda985abbdbf52c275c6c4d/Contrib/Language%20files> for the complete list of languages.",
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "type": "string"
+          }
+        },
+        "displayLanguageSelector": {
+          "description": "Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not. By default the OS language is selected, with a fallback to the first language in the `languages` array.",
+          "default": false,
+          "type": "boolean"
+        }
+      },
+      "additionalProperties": false
+    },
+    "NSISInstallerMode": {
+      "description": "Install Modes for the NSIS installer.",
+      "oneOf": [
+        {
+          "description": "Default mode for the installer.\n\nInstall the app by default in a directory that doesn't require Administrator access.\n\nInstaller metadata will be saved under the `HKCU` registry path.",
+          "type": "string",
+          "enum": [
+            "currentUser"
+          ]
+        },
+        {
+          "description": "Install the app by default in the `Program Files` folder directory requires Administrator access for the installation.\n\nInstaller metadata will be saved under the `HKLM` registry path.",
+          "type": "string",
+          "enum": [
+            "perMachine"
+          ]
+        },
+        {
+          "description": "Combines both modes and allows the user to choose at install time whether to install for the current user or per machine. Note that this mode will require Administrator access even if the user wants to install it for the current user only.\n\nInstaller metadata will be saved under the `HKLM` or `HKCU` registry path based on the user's choice.",
+          "type": "string",
+          "enum": [
+            "both"
+          ]
+        }
+      ]
+    },
     "IosConfig": {
       "description": "General configuration for the iOS target.",
       "type": "object",
@@ -1651,7 +1773,7 @@
       "additionalProperties": false
     },
     "AllowlistConfig": {
-      "description": "Allowlist configuration.",
+      "description": "Allowlist configuration. The allowlist is a translation of the [Cargo allowlist features](https://docs.rs/tauri/latest/tauri/#cargo-allowlist-features).\n\n# Notes\n\n- Endpoints that don't have their own allowlist option are enabled by default. - There is only \"opt-in\", no \"opt-out\". Setting an option to `false` has no effect.\n\n# Examples\n\n- * [`\"app-all\": true`](https://tauri.app/v1/api/config/#appallowlistconfig.all) will make the [hide](https://tauri.app/v1/api/js/app#hide) endpoint be available regardless of whether `hide` is set to `false` or `true` in the allowlist.",
       "type": "object",
       "properties": {
         "all": {
@@ -1693,6 +1815,7 @@
             "print": false,
             "requestUserAttention": false,
             "setAlwaysOnTop": false,
+            "setContentProtected": false,
             "setCursorGrab": false,
             "setCursorIcon": false,
             "setCursorPosition": false,
@@ -2049,6 +2172,11 @@
           "default": false,
           "type": "boolean"
         },
+        "setContentProtected": {
+          "description": "Allows preventing the window contents from being captured by other apps.",
+          "default": false,
+          "type": "boolean"
+        },
         "setSize": {
           "description": "Allows setting the window size.",
           "default": false,
@@ -2249,7 +2377,7 @@
       "description": "Defines the `shell > open` api scope.",
       "anyOf": [
         {
-          "description": "If the shell open API should be enabled.\n\nIf enabled, the default validation regex (`^https?://`) is used.",
+          "description": "If the shell open API should be enabled.\n\nIf enabled, the default validation regex (`^((mailto:\\w+)|(tel:\\w+)|(https?://\\w+)).+`) is used.",
           "type": "boolean"
         },
         {
@@ -2597,7 +2725,8 @@
         "windows": {
           "description": "The Windows configuration for the updater.",
           "default": {
-            "installMode": "passive"
+            "installMode": "passive",
+            "installerArgs": []
           },
           "allOf": [
             {
@@ -2617,6 +2746,14 @@
       "description": "The updater configuration for Windows.",
       "type": "object",
       "properties": {
+        "installerArgs": {
+          "description": "Additional arguments given to the NSIS or WiX installer.",
+          "default": [],
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
         "installMode": {
           "description": "The installation mode for the update on Windows. Defaults to `passive`.",
           "default": "passive",
@@ -2640,7 +2777,7 @@
           ]
         },
         {
-          "description": "The quiet mode means there's no user interaction required. Requires admin privileges if the installer does.",
+          "description": "The quiet mode means there's no user interaction required. Requires admin privileges if the installer does (WiX).",
           "type": "string",
           "enum": [
             "quiet"

+ 6 - 6
core/tauri-build/Cargo.toml

@@ -21,20 +21,20 @@ anyhow = "1"
 quote = { version = "1", optional = true }
 tauri-codegen = { version = "2.0.0-alpha.1", path = "../tauri-codegen", optional = true }
 tauri-utils = { version = "2.0.0-alpha.1", path = "../tauri-utils", features = [ "build", "resources" ] }
-cargo_toml = "0.13"
+cargo_toml = "0.14"
+serde = "1"
 serde_json = "1"
 heck = "0.4"
-json-patch = "0.2"
+json-patch = "0.3"
 walkdir = "2"
 filetime = "0.2"
 
+# dependencies for Windows targets
+tauri-winres = "0.1"
+semver = "1"
 [target."cfg(target_os = \"macos\")".dependencies]
 swift-rs = { git = "https://github.com/Brendonovich/swift-rs", rev = "eb6de914ad57501da5019154d476d45660559999", features = ["build"] }
 
-[target."cfg(windows)".dependencies]
-winres = "0.1"
-semver = "1"
-
 [features]
 codegen = [ "tauri-codegen", "quote" ]
 isolation = [ "tauri-codegen/isolation", "tauri-utils/isolation" ]

+ 177 - 75
core/tauri-build/src/lib.rs

@@ -5,9 +5,13 @@
 #![cfg_attr(doc_cfg, feature(doc_cfg))]
 
 pub use anyhow::Result;
+use cargo_toml::{Dependency, Manifest};
 use heck::AsShoutySnakeCase;
 
-use tauri_utils::resources::{external_binaries, resource_relpath, ResourcePaths};
+use tauri_utils::{
+  config::Config,
+  resources::{external_binaries, resource_relpath, ResourcePaths},
+};
 
 use std::path::{Path, PathBuf};
 
@@ -15,7 +19,6 @@ use std::path::{Path, PathBuf};
 mod codegen;
 /// Mobile build functions.
 pub mod mobile;
-#[cfg(windows)]
 mod static_vcruntime;
 
 #[cfg(feature = "codegen")]
@@ -112,6 +115,15 @@ pub struct WindowsAttributes {
   ///
   /// If it is left unset, it will look up a path in the registry, i.e. HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots
   sdk_dir: Option<PathBuf>,
+  /// A string containing an [application manifest] to be included with the application on Windows.
+  ///
+  /// Defaults to:
+  /// ```ignore
+  #[doc = include_str!("window-app-manifest.xml")]
+  /// ```
+  ///
+  /// [application manifest]: https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests
+  app_manifest: Option<String>,
 }
 
 impl WindowsAttributes {
@@ -137,6 +149,45 @@ impl WindowsAttributes {
     self.sdk_dir = Some(sdk_dir.as_ref().into());
     self
   }
+
+  /// Sets the Windows app [manifest].
+  ///
+  /// # Example
+  ///
+  /// The following manifest will brand the exe as requesting administrator privileges.
+  /// Thus, everytime it is executed, a Windows UAC dialog will appear.
+  ///
+  /// Note that you can move the manifest contents to a separate file and use `include_str!("manifest.xml")`
+  /// instead of the inline string.
+  ///
+  /// ```rust,no_run
+  /// let mut windows = tauri_build::WindowsAttributes::new();
+  /// windows = windows.app_manifest(r#"
+  /// <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+  /// <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
+  ///     <security>
+  ///         <requestedPrivileges>
+  ///             <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
+  ///         </requestedPrivileges>
+  ///     </security>
+  /// </trustInfo>
+  /// </assembly>
+  /// "#);
+  /// tauri_build::try_build(
+  ///   tauri_build::Attributes::new().windows_attributes(windows)
+  /// ).expect("failed to run build script");
+  /// ```
+  ///
+  /// Defaults to:
+  /// ```ignore
+  #[doc = include_str!("window-app-manifest.xml")]
+  /// [manifest]: https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests
+  /// ```
+  #[must_use]
+  pub fn app_manifest<S: AsRef<str>>(mut self, manifest: S) -> Self {
+    self.app_manifest = Some(manifest.as_ref().to_string());
+    self
+  }
 }
 
 /// The attributes used on the build.
@@ -197,8 +248,6 @@ pub fn build() {
 #[allow(unused_variables)]
 pub fn try_build(attributes: Attributes) -> Result<()> {
   use anyhow::anyhow;
-  use cargo_toml::{Dependency, Manifest};
-  use tauri_utils::config::{Config, TauriConfig};
 
   println!("cargo:rerun-if-env-changed=TAURI_CONFIG");
   println!("cargo:rerun-if-changed=tauri.conf.json");
@@ -235,50 +284,33 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
 
   cfg_alias("dev", !has_feature("custom-protocol"));
 
-  let mut manifest = Manifest::from_path("Cargo.toml")?;
-  if let Some(tauri) = manifest.dependencies.remove("tauri") {
-    let features = match tauri {
-      Dependency::Simple(_) => Vec::new(),
-      Dependency::Detailed(dep) => dep.features,
-      Dependency::Inherited(dep) => dep.features,
-    };
-
-    let all_cli_managed_features = TauriConfig::all_features();
-    let diff = features_diff(
-      &features
-        .into_iter()
-        .filter(|f| all_cli_managed_features.contains(&f.as_str()))
-        .collect::<Vec<String>>(),
-      &config
-        .tauri
-        .features()
-        .into_iter()
-        .map(|f| f.to_string())
-        .collect::<Vec<String>>(),
-    );
-
-    let mut error_message = String::new();
-    if !diff.remove.is_empty() {
-      error_message.push_str("remove the `");
-      error_message.push_str(&diff.remove.join(", "));
-      error_message.push_str(if diff.remove.len() == 1 {
-        "` feature"
-      } else {
-        "` features"
-      });
-      if !diff.add.is_empty() {
-        error_message.push_str(" and ");
-      }
-    }
-    if !diff.add.is_empty() {
-      error_message.push_str("add the `");
-      error_message.push_str(&diff.add.join(", "));
-      error_message.push_str(if diff.add.len() == 1 {
-        "` feature"
-      } else {
-        "` features"
-      });
+  let ws_path = get_workspace_dir()?;
+  let mut manifest =
+    Manifest::<cargo_toml::Value>::from_slice_with_metadata(&std::fs::read("Cargo.toml")?)?;
+
+  if let Ok(ws_manifest) = Manifest::from_path(ws_path.join("Cargo.toml")) {
+    Manifest::complete_from_path_and_workspace(
+      &mut manifest,
+      Path::new("Cargo.toml"),
+      Some((&ws_manifest, ws_path.as_path())),
+    )?;
+  } else {
+    Manifest::complete_from_path(&mut manifest, Path::new("Cargo.toml"))?;
+  }
+
+  if let Some(tauri_build) = manifest.build_dependencies.remove("tauri-build") {
+    let error_message = check_features(&config, tauri_build, true);
+
+    if !error_message.is_empty() {
+      return Err(anyhow!("
+      The `tauri-build` dependency features on the `Cargo.toml` file does not match the allowlist defined under `tauri.conf.json`.
+      Please run `tauri dev` or `tauri build` or {}.
+    ", error_message));
     }
+  }
+
+  if let Some(tauri) = manifest.dependencies.remove("tauri") {
+    let error_message = check_features(&config, tauri, false);
 
     if !error_message.is_empty() {
       return Err(anyhow!("
@@ -313,25 +345,25 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
 
   #[allow(unused_mut, clippy::redundant_clone)]
   let mut resources = config.tauri.bundle.resources.clone().unwrap_or_default();
-  #[cfg(windows)]
-  if let Some(fixed_webview2_runtime_path) = &config.tauri.bundle.windows.webview_fixed_runtime_path
-  {
-    resources.push(fixed_webview2_runtime_path.display().to_string());
+  if target_triple.contains("windows") {
+    if let Some(fixed_webview2_runtime_path) =
+      &config.tauri.bundle.windows.webview_fixed_runtime_path
+    {
+      resources.push(fixed_webview2_runtime_path.display().to_string());
+    }
   }
   copy_resources(ResourcePaths::new(resources.as_slice(), true), target_dir)?;
 
-  #[cfg(target_os = "macos")]
-  {
-    if let Some(version) = config.tauri.bundle.macos.minimum_system_version {
+  if target_triple.contains("darwin") {
+    if let Some(version) = &config.tauri.bundle.macos.minimum_system_version {
       println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET={}", version);
     }
   }
 
-  #[cfg(windows)]
-  {
+  if target_triple.contains("windows") {
     use anyhow::Context;
     use semver::Version;
-    use winres::{VersionInfo, WindowsResource};
+    use tauri_winres::{VersionInfo, WindowsResource};
 
     fn find_icon<F: Fn(&&String) -> bool>(config: &Config, predicate: F, default: &str) -> PathBuf {
       let icon_path = config
@@ -354,24 +386,11 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
       if window_icon_path.exists() {
         let mut res = WindowsResource::new();
 
-        res.set_manifest(
-          r#"
-        <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
-          <dependency>
-              <dependentAssembly>
-                  <assemblyIdentity
-                      type="win32"
-                      name="Microsoft.Windows.Common-Controls"
-                      version="6.0.0.0"
-                      processorArchitecture="*"
-                      publicKeyToken="6595b64144ccf1df"
-                      language="*"
-                  />
-              </dependentAssembly>
-          </dependency>
-        </assembly>
-        "#,
-        );
+      if let Some(manifest) = attributes.windows_attributes.app_manifest {
+        res.set_manifest(&manifest);
+      } else {
+        res.set_manifest(include_str!("window-app-manifest.xml"));
+      }
 
         if let Some(sdk_dir) = &attributes.windows_attributes.sdk_dir {
           if let Some(sdk_dir_str) = sdk_dir.to_str() {
@@ -470,6 +489,89 @@ fn features_diff(current: &[String], expected: &[String]) -> Diff {
   Diff { remove, add }
 }
 
+fn check_features(config: &Config, dependency: Dependency, is_tauri_build: bool) -> String {
+  use tauri_utils::config::{PatternKind, TauriConfig};
+
+  let features = match dependency {
+    Dependency::Simple(_) => Vec::new(),
+    Dependency::Detailed(dep) => dep.features,
+    Dependency::Inherited(dep) => dep.features,
+  };
+
+  let all_cli_managed_features = if is_tauri_build {
+    vec!["isolation"]
+  } else {
+    TauriConfig::all_features()
+  };
+
+  let expected = if is_tauri_build {
+    match config.tauri.pattern {
+      PatternKind::Isolation { .. } => vec!["isolation".to_string()],
+      _ => vec![],
+    }
+  } else {
+    config
+      .tauri
+      .features()
+      .into_iter()
+      .map(|f| f.to_string())
+      .collect::<Vec<String>>()
+  };
+
+  let diff = features_diff(
+    &features
+      .into_iter()
+      .filter(|f| all_cli_managed_features.contains(&f.as_str()))
+      .collect::<Vec<String>>(),
+    &expected,
+  );
+
+  let mut error_message = String::new();
+  if !diff.remove.is_empty() {
+    error_message.push_str("remove the `");
+    error_message.push_str(&diff.remove.join(", "));
+    error_message.push_str(if diff.remove.len() == 1 {
+      "` feature"
+    } else {
+      "` features"
+    });
+    if !diff.add.is_empty() {
+      error_message.push_str(" and ");
+    }
+  }
+  if !diff.add.is_empty() {
+    error_message.push_str("add the `");
+    error_message.push_str(&diff.add.join(", "));
+    error_message.push_str(if diff.add.len() == 1 {
+      "` feature"
+    } else {
+      "` features"
+    });
+  }
+
+  error_message
+}
+
+#[derive(serde::Deserialize)]
+struct CargoMetadata {
+  workspace_root: PathBuf,
+}
+
+fn get_workspace_dir() -> Result<PathBuf> {
+  let output = std::process::Command::new("cargo")
+    .args(["metadata", "--no-deps", "--format-version", "1"])
+    .output()?;
+
+  if !output.status.success() {
+    return Err(anyhow::anyhow!(
+      "cargo metadata command exited with a non zero exit code: {}",
+      String::from_utf8(output.stderr)?
+    ));
+  }
+
+  Ok(serde_json::from_slice::<CargoMetadata>(&output.stdout)?.workspace_root)
+}
+
 #[cfg(test)]
 mod tests {
   use super::Diff;

+ 1 - 1
core/tauri-build/src/static_vcruntime.rs

@@ -48,7 +48,7 @@ fn override_msvcrt_lib() {
   let f = fs::OpenOptions::new()
     .write(true)
     .create_new(true)
-    .open(&path);
+    .open(path);
   if let Ok(mut f) = f {
     f.write_all(machine).unwrap();
     f.write_all(bytes).unwrap();

+ 14 - 0
core/tauri-build/src/window-app-manifest.xml

@@ -0,0 +1,14 @@
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+  <dependency>
+    <dependentAssembly>
+      <assemblyIdentity
+        type="win32"
+        name="Microsoft.Windows.Common-Controls"
+        version="6.0.0.0"
+        processorArchitecture="*"
+        publicKeyToken="6595b64144ccf1df"
+        language="*"
+      />
+    </dependentAssembly>
+  </dependency>
+</assembly>

+ 4 - 4
core/tauri-codegen/Cargo.toml

@@ -14,7 +14,7 @@ readme = "README.md"
 
 [dependencies]
 sha2 = "0.10"
-base64 = "0.13"
+base64 = "0.21"
 proc-macro2 = "1"
 quote = "1"
 serde = { version = "1", features = [ "derive" ] }
@@ -23,12 +23,12 @@ tauri-utils = { version = "2.0.0-alpha.1", path = "../tauri-utils", features = [
 thiserror = "1"
 walkdir = "2"
 brotli = { version = "3", optional = true, default-features = false, features = [ "std" ] }
-regex = { version = "1.7.0", optional = true }
+regex = { version = "1.7.1", optional = true }
 uuid = { version = "1", features = [ "v4" ] }
 semver = "1"
-ico = "0.2"
+ico = "0.3"
 png = "0.17"
-json-patch = "0.2"
+json-patch = "0.3"
 url = "2"
 
 [target."cfg(target_os = \"macos\")".dependencies]

+ 10 - 5
core/tauri-codegen/src/context.rs

@@ -5,6 +5,7 @@
 use std::path::{Path, PathBuf};
 use std::{ffi::OsStr, str::FromStr};
 
+use base64::Engine;
 use proc_macro2::TokenStream;
 use quote::quote;
 use sha2::{Digest, Sha256};
@@ -57,7 +58,10 @@ fn map_core_assets(
               let mut hasher = Sha256::new();
               hasher.update(&script);
               let hash = hasher.finalize();
-              scripts.push(format!("'sha256-{}'", base64::encode(hash)));
+              scripts.push(format!(
+                "'sha256-{}'",
+                base64::engine::general_purpose::STANDARD.encode(hash)
+              ));
             }
             csp_hashes
               .inline_scripts
@@ -74,9 +78,10 @@ fn map_core_assets(
             let mut hasher = Sha256::new();
             hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE);
             let hash = hasher.finalize();
-            csp_hashes
-              .styles
-              .push(format!("'sha256-{}'", base64::encode(hash)));
+            csp_hashes.styles.push(format!(
+              "'sha256-{}'",
+              base64::engine::general_purpose::STANDARD.encode(hash)
+            ));
           }
         }
 
@@ -413,7 +418,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
     let shell_scope_open = match &config.tauri.allowlist.shell.open {
       ShellAllowlistOpen::Flag(false) => quote!(::std::option::Option::None),
       ShellAllowlistOpen::Flag(true) => {
-        quote!(::std::option::Option::Some(#root::regex::Regex::new("^https?://").unwrap()))
+        quote!(::std::option::Option::Some(#root::regex::Regex::new(r#"^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+"#).unwrap()))
       }
       ShellAllowlistOpen::Validate(regex) => match Regex::new(regex) {
         Ok(_) => quote!(::std::option::Option::Some(#root::regex::Regex::new(#regex).unwrap())),

+ 5 - 3
core/tauri-codegen/src/embedded_assets.rs

@@ -2,6 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
+use base64::Engine;
 use proc_macro2::TokenStream;
 use quote::{quote, ToTokens, TokenStreamExt};
 use sha2::{Digest, Sha256};
@@ -181,9 +182,10 @@ impl CspHashes {
           })?,
         );
         let hash = hasher.finalize();
-        self
-          .scripts
-          .push(format!("'sha256-{}'", base64::encode(hash)));
+        self.scripts.push(format!(
+          "'sha256-{}'",
+          base64::engine::general_purpose::STANDARD.encode(hash)
+        ));
       }
     }
 

+ 115 - 5
core/tauri-runtime-wry/src/lib.rs

@@ -14,8 +14,8 @@ use tauri_runtime::{
     dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size},
     CursorIcon, DetachedWindow, FileDropEvent, JsEventListenerKey, PendingWindow, WindowEvent,
   },
-  Dispatch, Error, EventLoopProxy, ExitRequestedEventAction, Icon, Result, RunEvent, RunIteration,
-  Runtime, RuntimeHandle, UserAttentionType, UserEvent,
+  DeviceEventFilter, Dispatch, Error, EventLoopProxy, ExitRequestedEventAction, Icon, Result,
+  RunEvent, RunIteration, Runtime, RuntimeHandle, UserAttentionType, UserEvent,
 };
 
 use tauri_runtime::window::MenuEvent;
@@ -33,6 +33,8 @@ use wry::application::platform::macos::WindowBuilderExtMacOS;
 use wry::application::platform::unix::{WindowBuilderExtUnix, WindowExtUnix};
 #[cfg(windows)]
 use wry::application::platform::windows::{WindowBuilderExtWindows, WindowExtWindows};
+#[cfg(windows)]
+use wry::webview::WebViewBuilderExtWindows;
 
 #[cfg(target_os = "macos")]
 use tauri_utils::TitleBarStyle;
@@ -47,7 +49,8 @@ use wry::{
     },
     event::{Event, StartCause, WindowEvent as WryWindowEvent},
     event_loop::{
-      ControlFlow, EventLoop, EventLoopProxy as WryEventLoopProxy, EventLoopWindowTarget,
+      ControlFlow, DeviceEventFilter as WryDeviceEventFilter, EventLoop,
+      EventLoopProxy as WryEventLoopProxy, EventLoopWindowTarget,
     },
     menu::{
       AboutMetadata as WryAboutMetadata, CustomMenuItem as WryCustomMenuItem, MenuBar,
@@ -61,10 +64,9 @@ use wry::{
     },
   },
   http::{Request as WryRequest, Response as WryResponse},
-  webview::{FileDropEvent as WryFileDropEvent, WebContext, WebView, WebViewBuilder},
+  webview::{FileDropEvent as WryFileDropEvent, Url, WebContext, WebView, WebViewBuilder},
 };
 
-pub use wry;
 pub use wry::application::window::{Window, WindowBuilder as WryWindowBuilder, WindowId};
 #[cfg(windows)]
 use wry::webview::WebviewExtWindows;
@@ -367,6 +369,18 @@ impl From<MenuItem> for MenuItemWrapper {
   }
 }
 
+pub struct DeviceEventFilterWrapper(pub WryDeviceEventFilter);
+
+impl From<DeviceEventFilter> for DeviceEventFilterWrapper {
+  fn from(item: DeviceEventFilter) -> Self {
+    match item {
+      DeviceEventFilter::Always => Self(WryDeviceEventFilter::Always),
+      DeviceEventFilter::Never => Self(WryDeviceEventFilter::Never),
+      DeviceEventFilter::Unfocused => Self(WryDeviceEventFilter::Unfocused),
+    }
+  }
+}
+
 #[cfg(target_os = "macos")]
 pub struct NativeImageWrapper(pub WryNativeImage);
 
@@ -727,6 +741,12 @@ impl WindowBuilder for WindowBuilderWrapper {
       ");
     }
 
+    #[cfg(target_os = "linux")]
+    {
+      // Mouse event is disabled on Linux since sudden event bursts could block event loop.
+      window.inner = window.inner.with_cursor_moved_event(false);
+    }
+
     #[cfg(desktop)]
     {
       window = window
@@ -738,6 +758,7 @@ impl WindowBuilder for WindowBuilderWrapper {
         .decorations(config.decorations)
         .maximized(config.maximized)
         .always_on_top(config.always_on_top)
+        .content_protected(config.content_protected)
         .skip_taskbar(config.skip_taskbar)
         .theme(config.theme)
         .shadow(config.shadow);
@@ -848,6 +869,11 @@ impl WindowBuilder for WindowBuilderWrapper {
     self
   }
 
+  fn content_protected(mut self, protected: bool) -> Self {
+    self.inner = self.inner.with_content_protection(protected);
+    self
+  }
+
   fn shadow(#[allow(unused_mut)] mut self, _enable: bool) -> Self {
     #[cfg(windows)]
     {
@@ -1037,16 +1063,19 @@ pub enum WindowMessage {
   #[cfg(any(debug_assertions, feature = "devtools"))]
   IsDevToolsOpen(Sender<bool>),
   // Getters
+  Url(Sender<Url>),
   ScaleFactor(Sender<f64>),
   InnerPosition(Sender<Result<PhysicalPosition<i32>>>),
   OuterPosition(Sender<Result<PhysicalPosition<i32>>>),
   InnerSize(Sender<PhysicalSize<u32>>),
   OuterSize(Sender<PhysicalSize<u32>>),
   IsFullscreen(Sender<bool>),
+  IsMinimized(Sender<bool>),
   IsMaximized(Sender<bool>),
   IsDecorated(Sender<bool>),
   IsResizable(Sender<bool>),
   IsVisible(Sender<bool>),
+  Title(Sender<String>),
   IsMenuVisible(Sender<bool>),
   CurrentMonitor(Sender<Option<MonitorHandle>>),
   PrimaryMonitor(Sender<Option<MonitorHandle>>),
@@ -1078,6 +1107,7 @@ pub enum WindowMessage {
   SetDecorations(bool),
   SetShadow(bool),
   SetAlwaysOnTop(bool),
+  SetContentProtected(bool),
   SetSize(Size),
   SetMinSize(Option<Size>),
   SetMaxSize(Option<Size>),
@@ -1120,6 +1150,7 @@ pub enum TrayMessage {
   UpdateIconAsTemplate(bool),
   #[cfg(target_os = "macos")]
   UpdateTitle(String),
+  UpdateTooltip(String),
   Create(SystemTray, Sender<Result<()>>),
   Destroy(Sender<Result<()>>),
 }
@@ -1236,6 +1267,10 @@ impl<T: UserEvent> Dispatch<T> for WryDispatcher<T> {
 
   // Getters
 
+  fn url(&self) -> Result<Url> {
+    window_getter!(self, WindowMessage::Url)
+  }
+
   fn scale_factor(&self) -> Result<f64> {
     window_getter!(self, WindowMessage::ScaleFactor)
   }
@@ -1260,6 +1295,10 @@ impl<T: UserEvent> Dispatch<T> for WryDispatcher<T> {
     window_getter!(self, WindowMessage::IsFullscreen)
   }
 
+  fn is_minimized(&self) -> Result<bool> {
+    window_getter!(self, WindowMessage::IsMinimized)
+  }
+
   fn is_maximized(&self) -> Result<bool> {
     window_getter!(self, WindowMessage::IsMaximized)
   }
@@ -1278,6 +1317,10 @@ impl<T: UserEvent> Dispatch<T> for WryDispatcher<T> {
     window_getter!(self, WindowMessage::IsVisible)
   }
 
+  fn title(&self) -> Result<String> {
+    window_getter!(self, WindowMessage::Title)
+  }
+
   fn is_menu_visible(&self) -> Result<bool> {
     window_getter!(self, WindowMessage::IsMenuVisible)
   }
@@ -1454,6 +1497,16 @@ impl<T: UserEvent> Dispatch<T> for WryDispatcher<T> {
     )
   }
 
+  fn set_content_protected(&self, protected: bool) -> Result<()> {
+    send_user_message(
+      &self.context,
+      Message::Window(
+        self.window_id,
+        WindowMessage::SetContentProtected(protected),
+      ),
+    )
+  }
+
   fn set_size(&self, size: Size) -> Result<()> {
     send_user_message(
       &self.context,
@@ -2076,6 +2129,12 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
     self.event_loop.hide_application();
   }
 
+  fn set_device_event_filter(&mut self, filter: DeviceEventFilter) {
+    self
+      .event_loop
+      .set_device_event_filter(DeviceEventFilterWrapper::from(filter).0);
+  }
+
   #[cfg(desktop)]
   fn run_iteration<F: FnMut(RunEvent<T>) + 'static>(&mut self, mut callback: F) -> RunIteration {
     use wry::application::platform::run_return::EventLoopExtRunReturn;
@@ -2373,6 +2432,11 @@ fn handle_user_message<T: UserEvent>(
               }
             }
             // Getters
+            WindowMessage::Url(tx) => {
+              if let WindowHandle::Webview { inner: w, .. } = &window {
+                tx.send(w.url()).unwrap();
+              }
+            }
             WindowMessage::ScaleFactor(tx) => tx.send(window.scale_factor()).unwrap(),
             WindowMessage::InnerPosition(tx) => tx
               .send(
@@ -2397,10 +2461,12 @@ fn handle_user_message<T: UserEvent>(
               .send(PhysicalSizeWrapper(window.outer_size()).into())
               .unwrap(),
             WindowMessage::IsFullscreen(tx) => tx.send(window.fullscreen().is_some()).unwrap(),
+            WindowMessage::IsMinimized(tx) => tx.send(window.is_minimized()).unwrap(),
             WindowMessage::IsMaximized(tx) => tx.send(window.is_maximized()).unwrap(),
             WindowMessage::IsDecorated(tx) => tx.send(window.is_decorated()).unwrap(),
             WindowMessage::IsResizable(tx) => tx.send(window.is_resizable()).unwrap(),
             WindowMessage::IsVisible(tx) => tx.send(window.is_visible()).unwrap(),
+            WindowMessage::Title(tx) => tx.send(window.title()).unwrap(),
             WindowMessage::IsMenuVisible(tx) => tx.send(window.is_menu_visible()).unwrap(),
             WindowMessage::CurrentMonitor(tx) => tx.send(window.current_monitor()).unwrap(),
             WindowMessage::PrimaryMonitor(tx) => tx.send(window.primary_monitor()).unwrap(),
@@ -2451,6 +2517,9 @@ fn handle_user_message<T: UserEvent>(
               window.set_has_shadow(_enable);
             }
             WindowMessage::SetAlwaysOnTop(always_on_top) => window.set_always_on_top(always_on_top),
+            WindowMessage::SetContentProtected(protected) => {
+              window.set_content_protection(protected)
+            }
             WindowMessage::SetSize(size) => {
               window.set_inner_size(SizeWrapper::from(size).0);
             }
@@ -2637,6 +2706,11 @@ fn handle_user_message<T: UserEvent>(
               tray.set_title(&title);
             }
           }
+          TrayMessage::UpdateTooltip(tooltip) => {
+            if let Some(tray) = &mut *tray_context.tray.lock().unwrap() {
+              tray.set_tooltip(&tooltip);
+            }
+          }
           TrayMessage::Create(_tray, _tx) => {
             // already handled
           }
@@ -2852,6 +2926,19 @@ fn handle_event_loop<T: UserEvent>(
         }
 
         match event {
+          #[cfg(windows)]
+          WryWindowEvent::ThemeChanged(theme) => {
+            if let Some(window) = windows.borrow().get(&window_id) {
+              if let Some(WindowHandle::Webview { inner, .. }) = &window.inner {
+                let theme = match theme {
+                  WryTheme::Dark => wry::webview::Theme::Dark,
+                  WryTheme::Light => wry::webview::Theme::Light,
+                  _ => wry::webview::Theme::Light,
+                };
+                inner.set_theme(theme);
+              }
+            }
+          }
           WryWindowEvent::CloseRequested => {
             on_close_requested(callback, window_id, windows.clone());
           }
@@ -3028,6 +3115,9 @@ fn create_webview<T: UserEvent>(
       .with_drag_and_drop(webview_attributes.file_drop_handler_enabled);
   }
 
+  #[cfg(windows)]
+  let window_theme = window_builder.inner.window.preferred_theme;
+
   #[cfg(target_os = "macos")]
   {
     if window_builder.tabbing_identifier.is_none()
@@ -3064,9 +3154,29 @@ fn create_webview<T: UserEvent>(
     webview_builder = webview_builder
       .with_file_drop_handler(create_file_drop_handler(window_event_listeners.clone()));
   }
+  if let Some(navigation_handler) = pending.navigation_handler {
+    webview_builder = webview_builder.with_navigation_handler(move |url| {
+      Url::parse(&url).map(&navigation_handler).unwrap_or(true)
+    });
+  }
   if let Some(user_agent) = webview_attributes.user_agent {
     webview_builder = webview_builder.with_user_agent(&user_agent);
   }
+
+  #[cfg(windows)]
+  if let Some(additional_browser_args) = webview_attributes.additional_browser_args {
+    webview_builder = webview_builder.with_additional_browser_args(&additional_browser_args);
+  }
+
+  #[cfg(windows)]
+  if let Some(theme) = window_theme {
+    webview_builder = webview_builder.with_theme(match theme {
+      WryTheme::Dark => wry::webview::Theme::Dark,
+      WryTheme::Light => wry::webview::Theme::Light,
+      _ => wry::webview::Theme::Light,
+    });
+  }
+
   if let Some(handler) = ipc_handler {
     webview_builder = webview_builder.with_ipc_handler(create_ipc_handler(
       context,

+ 14 - 0
core/tauri-runtime-wry/src/system_tray.rs

@@ -114,6 +114,10 @@ pub fn create_tray<T>(
     }
   }
 
+  if let Some(tooltip) = system_tray.tooltip {
+    builder = builder.with_tooltip(&tooltip);
+  }
+
   let tray = builder
     .build(event_loop)
     .map_err(|e| Error::SystemTray(Box::new(e)))?;
@@ -172,6 +176,16 @@ impl<T: UserEvent> TrayHandle for SystemTrayHandle<T> {
       .map_err(|_| Error::FailedToSendMessage)
   }
 
+  fn set_tooltip(&self, tooltip: &str) -> Result<()> {
+    self
+      .proxy
+      .send_event(Message::Tray(
+        self.id,
+        TrayMessage::UpdateTooltip(tooltip.to_owned()),
+      ))
+      .map_err(|_| Error::FailedToSendMessage)
+  }
+
   fn destroy(&self) -> Result<()> {
     let (tx, rx) = std::sync::mpsc::channel();
     send_user_message(

+ 1 - 0
core/tauri-runtime/Cargo.toml

@@ -32,6 +32,7 @@ http = "0.2.4"
 http-range = "0.1.4"
 raw-window-handle = "0.5"
 rand = "0.8"
+url = { version = "2" }
 
 [target."cfg(windows)".dependencies]
 webview2-com = "0.22"

+ 56 - 0
core/tauri-runtime/src/lib.rs

@@ -10,6 +10,7 @@ use raw_window_handle::RawDisplayHandle;
 use serde::Deserialize;
 use std::{fmt::Debug, sync::mpsc::Sender};
 use tauri_utils::Theme;
+use url::Url;
 use uuid::Uuid;
 
 pub mod http;
@@ -53,6 +54,7 @@ pub struct SystemTray {
   #[cfg(target_os = "macos")]
   pub title: Option<String>,
   pub on_event: Option<Box<TrayEventHandler>>,
+  pub tooltip: Option<String>,
 }
 
 #[cfg(all(desktop, feature = "system-tray"))]
@@ -86,6 +88,7 @@ impl Clone for SystemTray {
       menu_on_left_click: self.menu_on_left_click,
       #[cfg(target_os = "macos")]
       title: self.title.clone(),
+      tooltip: self.tooltip.clone(),
     }
   }
 }
@@ -104,6 +107,7 @@ impl Default for SystemTray {
       #[cfg(target_os = "macos")]
       title: None,
       on_event: None,
+      tooltip: None,
     }
   }
 }
@@ -156,6 +160,17 @@ impl SystemTray {
     self
   }
 
+  /// Sets the tray icon tooltip.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported
+  #[must_use]
+  pub fn with_tooltip(mut self, tooltip: &str) -> Self {
+    self.tooltip = Some(tooltip.to_owned());
+    self
+  }
+
   /// Sets the menu to show when the system tray is right clicked.
   #[must_use]
   pub fn with_menu(mut self, menu: menu::SystemTrayMenu) -> Self {
@@ -184,6 +199,23 @@ pub enum UserAttentionType {
   Informational,
 }
 
+#[derive(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.
+  Unfocused,
+  /// Report all device events regardless of window focus.
+  Never,
+}
+
+impl Default for DeviceEventFilter {
+  fn default() -> Self {
+    Self::Unfocused
+  }
+}
+
 #[derive(Debug, thiserror::Error)]
 #[non_exhaustive]
 pub enum Error {
@@ -480,6 +512,19 @@ pub trait Runtime<T: UserEvent>: Debug + Sized + 'static {
   #[cfg_attr(doc_cfg, 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 the one step of the webview runtime event loop and returns control flow to the caller.
   #[cfg(desktop)]
   fn run_iteration<F: Fn(RunEvent<T>) + 'static>(&mut self, callback: F) -> RunIteration;
@@ -522,6 +567,9 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
 
   // GETTERS
 
+  /// Returns the webview's current URL.
+  fn url(&self) -> Result<Url>;
+
   /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa.
   fn scale_factor(&self) -> Result<f64>;
 
@@ -544,6 +592,9 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
   /// Gets the window's current fullscreen state.
   fn is_fullscreen(&self) -> Result<bool>;
 
+  /// Gets the window's current minimized state.
+  fn is_minimized(&self) -> Result<bool>;
+
   /// Gets the window's current maximized state.
   fn is_maximized(&self) -> Result<bool>;
 
@@ -555,6 +606,8 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
 
   /// Gets the window's current visibility state.
   fn is_visible(&self) -> Result<bool>;
+  /// Gets the window's current title.
+  fn title(&self) -> Result<String>;
 
   /// Gets the window menu current visibility state.
   fn is_menu_visible(&self) -> Result<bool>;
@@ -648,6 +701,9 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
   /// Updates the window alwaysOnTop flag.
   fn set_always_on_top(&self, always_on_top: bool) -> 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<()>;
 

+ 1 - 0
core/tauri-runtime/src/menu.rs

@@ -154,6 +154,7 @@ pub trait TrayHandle: fmt::Debug + Clone + Send + Sync {
   fn set_icon_as_template(&self, is_template: bool) -> crate::Result<()>;
   #[cfg(target_os = "macos")]
   fn set_title(&self, title: &str) -> crate::Result<()>;
+  fn set_tooltip(&self, tooltip: &str) -> crate::Result<()>;
   fn destroy(&self) -> crate::Result<()>;
 }
 

+ 13 - 0
core/tauri-runtime/src/webview.rs

@@ -28,6 +28,7 @@ pub struct WebviewAttributes {
   pub file_drop_handler_enabled: bool,
   pub clipboard: bool,
   pub accept_first_mouse: bool,
+  pub additional_browser_args: Option<String>,
 }
 
 impl WebviewAttributes {
@@ -41,6 +42,7 @@ impl WebviewAttributes {
       file_drop_handler_enabled: true,
       clipboard: false,
       accept_first_mouse: false,
+      additional_browser_args: None,
     }
   }
 
@@ -88,6 +90,13 @@ impl WebviewAttributes {
     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
+  }
 }
 
 /// Do **NOT** implement this trait except for use in a custom [`Runtime`](crate::Runtime).
@@ -172,6 +181,10 @@ pub trait WindowBuilder: WindowBuilderBase {
   #[must_use]
   fn always_on_top(self, always_on_top: 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<Self>;
 

+ 6 - 0
core/tauri-runtime/src/window.rs

@@ -12,6 +12,7 @@ use crate::{
 };
 use serde::{Deserialize, Deserializer, Serialize};
 use tauri_utils::{config::WindowConfig, Theme};
+use url::Url;
 
 use std::{
   collections::{HashMap, HashSet},
@@ -240,6 +241,9 @@ pub struct PendingWindow<T: UserEvent, R: Runtime<T>> {
   /// A HashMap mapping JS event names with associated listener ids.
   pub js_event_listeners: Arc<Mutex<HashMap<JsEventListenerKey, HashSet<u64>>>>,
 
+  /// A handler to decide if incoming url is allowed to navigate.
+  pub navigation_handler: Option<Box<dyn Fn(Url) -> bool + Send>>,
+
   #[cfg(target_os = "android")]
   pub on_webview_created:
     Option<Box<dyn Fn(CreationContext<'_>) -> Result<(), jni::errors::Error> + Send>>,
@@ -282,6 +286,7 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
         url: "tauri://localhost".to_string(),
         menu_ids: Arc::new(Mutex::new(menu_ids)),
         js_event_listeners: Default::default(),
+        navigation_handler: Default::default(),
         #[cfg(target_os = "android")]
         on_webview_created: None,
       })
@@ -313,6 +318,7 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
         url: "tauri://localhost".to_string(),
         menu_ids: Arc::new(Mutex::new(menu_ids)),
         js_event_listeners: Default::default(),
+        navigation_handler: Default::default(),
         #[cfg(target_os = "android")]
         on_webview_created: None,
       })

+ 12 - 12
core/tauri-utils/Cargo.toml

@@ -12,30 +12,30 @@ exclude = [ "CHANGELOG.md", "/target" ]
 readme = "README.md"
 
 [dependencies]
-serde = { version = "1.0", features = [ "derive" ] }
-serde_json = "1.0"
-thiserror = "1.0.30"
+serde = { version = "1", features = [ "derive" ] }
+serde_json = "1"
+thiserror = "1"
 phf = { version = "0.10", features = [ "macros" ] }
 brotli = { version = "3", optional = true, default-features = false, features = [ "std" ] }
-url = { version = "2.2", features = [ "serde" ] }
+url = { version = "2", features = [ "serde" ] }
 kuchiki = "0.8"
 html5ever = "0.25"
-proc-macro2 = { version = "1.0", optional = true }
-quote = { version = "1.0", optional = true }
+proc-macro2 = { version = "1", optional = true }
+quote = { version = "1", optional = true }
 schemars = { version = "0.8", features = [ "url" ], optional = true }
-serde_with = "1.12"
-aes-gcm = { version = "0.9", optional = true }
+serde_with = "1"
+aes-gcm = { version = "0.10", optional = true }
 getrandom = { version = "0.2", optional = true, features = [ "std" ] }
 serialize-to-javascript = { version = "=0.1.1", optional = true }
 ctor = "0.1"
 json5 = { version = "0.4", optional = true }
 toml = { version = "0.5", optional = true }
-json-patch = "0.2"
-glob = { version = "0.3.0", optional = true }
+json-patch = "0.3"
+glob = { version = "0.3", optional = true }
 walkdir = { version = "2", optional = true }
-memchr = "2.4"
+memchr = "2"
 semver = "1"
-infer = "0.7"
+infer = "0.12"
 
 [target."cfg(target_os = \"linux\")".dependencies]
 heck = "0.4"

+ 113 - 5
core/tauri-utils/src/config.rs

@@ -78,6 +78,8 @@ pub enum BundleType {
   AppImage,
   /// The Microsoft Installer bundle (.msi).
   Msi,
+  /// The NSIS bundle (.exe).
+  Nsis,
   /// The macOS application bundle (.app).
   App,
   /// The Apple Disk Image bundle (.dmg).
@@ -95,6 +97,7 @@ impl Display for BundleType {
         Self::Deb => "deb",
         Self::AppImage => "appimage",
         Self::Msi => "msi",
+        Self::Nsis => "nsis",
         Self::App => "app",
         Self::Dmg => "dmg",
         Self::Updater => "updater",
@@ -122,6 +125,7 @@ impl<'de> Deserialize<'de> for BundleType {
       "deb" => Ok(Self::Deb),
       "appimage" => Ok(Self::AppImage),
       "msi" => Ok(Self::Msi),
+      "nsis" => Ok(Self::Nsis),
       "app" => Ok(Self::App),
       "dmg" => Ok(Self::Dmg),
       "updater" => Ok(Self::Updater),
@@ -416,6 +420,71 @@ pub struct WixConfig {
   pub dialog_image_path: Option<PathBuf>,
 }
 
+/// Configuration for the Installer bundle using NSIS.
+#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+pub struct NsisConfig {
+  /// The path to the license file to render on the installer.
+  pub license: Option<PathBuf>,
+  /// The path to a bitmap file to display on the header of installers pages.
+  ///
+  /// The recommended dimensions are 150px x 57px.
+  #[serde(alias = "header-image")]
+  pub header_image: Option<PathBuf>,
+  /// The path to a bitmap file for the Welcome page and the Finish page.
+  ///
+  /// The recommended dimensions are 164px x 314px.
+  #[serde(alias = "sidebar-image")]
+  pub sidebar_image: Option<PathBuf>,
+  /// The path to an icon file used as the installer icon.
+  #[serde(alias = "install-icon")]
+  pub installer_icon: Option<PathBuf>,
+  /// Whether the installation will be for all users or just the current user.
+  #[serde(default, alias = "install-mode")]
+  pub install_mode: NSISInstallerMode,
+  /// A list of installer languages.
+  /// By default the OS language is used. If the OS language is not in the list of languages, the first language will be used.
+  /// To allow the user to select the language, set `display_language_selector` to `true`.
+  ///
+  /// See <https://github.com/kichik/nsis/tree/9465c08046f00ccb6eda985abbdbf52c275c6c4d/Contrib/Language%20files> for the complete list of languages.
+  pub languages: Option<Vec<String>>,
+  /// Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not.
+  /// By default the OS language is selected, with a fallback to the first language in the `languages` array.
+  #[serde(default, alias = "display-language-selector")]
+  pub display_language_selector: bool,
+}
+
+/// Install Modes for the NSIS installer.
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+#[cfg_attr(feature = "schema", derive(JsonSchema))]
+pub enum NSISInstallerMode {
+  /// Default mode for the installer.
+  ///
+  /// Install the app by default in a directory that doesn't require Administrator access.
+  ///
+  /// Installer metadata will be saved under the `HKCU` registry path.
+  CurrentUser,
+  /// Install the app by default in the `Program Files` folder directory requires Administrator
+  /// access for the installation.
+  ///
+  /// Installer metadata will be saved under the `HKLM` registry path.
+  PerMachine,
+  /// Combines both modes and allows the user to choose at install time
+  /// whether to install for the current user or per machine. Note that this mode
+  /// will require Administrator access even if the user wants to install it for the current user only.
+  ///
+  /// Installer metadata will be saved under the `HKLM` or `HKCU` registry path based on the user's choice.
+  Both,
+}
+
+impl Default for NSISInstallerMode {
+  fn default() -> Self {
+    Self::CurrentUser
+  }
+}
+
 /// Install modes for the Webview2 runtime.
 /// Note that for the updater bundle [`Self::DownloadBootstrapper`] is used.
 ///
@@ -508,6 +577,8 @@ pub struct WindowsConfig {
   pub allow_downgrades: bool,
   /// Configuration for the MSI generated with WiX.
   pub wix: Option<WixConfig>,
+  /// Configuration for the installer generated with NSIS.
+  pub nsis: Option<NsisConfig>,
 }
 
 impl Default for WindowsConfig {
@@ -521,6 +592,7 @@ impl Default for WindowsConfig {
       webview_fixed_runtime_path: None,
       allow_downgrades: default_true(),
       wix: None,
+      nsis: None,
     }
   }
 }
@@ -534,7 +606,7 @@ pub struct BundleConfig {
   /// Whether Tauri should bundle your application or just output the executable.
   #[serde(default)]
   pub active: bool,
-  /// The bundle targets, currently supports ["deb", "appimage", "msi", "app", "dmg", "updater"] or "all".
+  /// The bundle targets, currently supports ["deb", "appimage", "nsis", "msi", "app", "dmg", "updater"] or "all".
   #[serde(default)]
   pub targets: BundleTarget,
   /// The application identifier in reverse domain name notation (e.g. `com.tauri.example`).
@@ -854,6 +926,9 @@ pub struct WindowConfig {
   /// Whether the window should always be on top of other windows.
   #[serde(default, alias = "always-on-top")]
   pub always_on_top: bool,
+  /// Prevents the window contents from being captured by other apps.
+  #[serde(default, alias = "content-protected")]
+  pub content_protected: bool,
   /// If `true`, hides the window icon from the taskbar on Windows and Linux.
   #[serde(default, alias = "skip-taskbar")]
   pub skip_taskbar: bool,
@@ -876,6 +951,10 @@ pub struct WindowConfig {
   /// [tabbing identifier]: <https://developer.apple.com/documentation/appkit/nswindow/1644704-tabbingidentifier>
   #[serde(default, alias = "tabbing-identifier")]
   pub tabbing_identifier: Option<String>,
+  /// Defines additional browser arguments on Windows. 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.
+  #[serde(default, alias = "additional-browser-args")]
+  pub additional_browser_args: Option<String>,
   /// Whether or not the window has shadow.
   ///
   /// ## Platform-specific
@@ -914,12 +993,14 @@ impl Default for WindowConfig {
       visible: default_true(),
       decorations: default_true(),
       always_on_top: false,
+      content_protected: false,
       skip_taskbar: false,
       theme: None,
       title_bar_style: Default::default(),
       hidden_title: false,
       accept_first_mouse: false,
       tabbing_identifier: None,
+      additional_browser_args: None,
       shadow: false,
     }
   }
@@ -1323,6 +1404,9 @@ pub struct WindowAllowlistConfig {
   /// Allows setting the always_on_top flag of the window.
   #[serde(default, alias = "set-always-on-top")]
   pub set_always_on_top: bool,
+  /// Allows preventing the window contents from being captured by other apps.
+  #[serde(default, alias = "set-content-protected")]
+  pub set_content_protected: bool,
   /// Allows setting the window size.
   #[serde(default, alias = "set-size")]
   pub set_size: bool,
@@ -1388,6 +1472,7 @@ impl Allowlist for WindowAllowlistConfig {
       close: true,
       set_decorations: true,
       set_always_on_top: true,
+      set_content_protected: false,
       set_size: true,
       set_min_size: true,
       set_max_size: true,
@@ -1440,6 +1525,12 @@ impl Allowlist for WindowAllowlistConfig {
         set_always_on_top,
         "window-set-always-on-top"
       );
+      check_feature!(
+        self,
+        features,
+        set_content_protected,
+        "window-set-content-protected"
+      );
       check_feature!(self, features, set_size, "window-set-size");
       check_feature!(self, features, set_min_size, "window-set-min-size");
       check_feature!(self, features, set_max_size, "window-set-max-size");
@@ -1595,7 +1686,7 @@ pub struct ShellAllowlistScope(pub Vec<ShellAllowedCommand>);
 pub enum ShellAllowlistOpen {
   /// If the shell open API should be enabled.
   ///
-  /// If enabled, the default validation regex (`^https?://`) is used.
+  /// If enabled, the default validation regex (`^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`) is used.
   Flag(bool),
 
   /// Enable the shell open API, with a custom regex that the opened path must match against.
@@ -2058,7 +2149,16 @@ impl Allowlist for AppAllowlistConfig {
   }
 }
 
-/// Allowlist configuration.
+/// Allowlist configuration. The allowlist is a translation of the [Cargo allowlist features](https://docs.rs/tauri/latest/tauri/#cargo-allowlist-features).
+///
+/// # Notes
+///
+/// - Endpoints that don't have their own allowlist option are enabled by default.
+/// - There is only "opt-in", no "opt-out". Setting an option to `false` has no effect.
+///
+/// # Examples
+///
+/// - * [`"app-all": true`](https://tauri.app/v1/api/config/#appallowlistconfig.all) will make the [hide](https://tauri.app/v1/api/js/app#hide) endpoint be available regardless of whether `hide` is set to `false` or `true` in the allowlist.
 #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
 #[cfg_attr(feature = "schema", derive(JsonSchema))]
 #[serde(rename_all = "camelCase", deny_unknown_fields)]
@@ -2283,7 +2383,7 @@ pub enum WindowsUpdateInstallMode {
   /// Specifies there's a basic UI during the installation process, including a final dialog box at the end.
   BasicUi,
   /// The quiet mode means there's no user interaction required.
-  /// Requires admin privileges if the installer does.
+  /// Requires admin privileges if the installer does (WiX).
   Quiet,
   /// Specifies unattended mode, which means the installation only shows a progress bar.
   Passive,
@@ -2354,6 +2454,9 @@ impl<'de> Deserialize<'de> for WindowsUpdateInstallMode {
 #[cfg_attr(feature = "schema", derive(JsonSchema))]
 #[serde(rename_all = "camelCase", deny_unknown_fields)]
 pub struct UpdaterWindowsConfig {
+  /// Additional arguments given to the NSIS or WiX installer.
+  #[serde(default, alias = "installer-args")]
+  pub installer_args: Vec<String>,
   /// The installation mode for the update on Windows. Defaults to `passive`.
   #[serde(default, alias = "install-mode")]
   pub install_mode: WindowsUpdateInstallMode,
@@ -3033,12 +3136,14 @@ mod build {
       let visible = self.visible;
       let decorations = self.decorations;
       let always_on_top = self.always_on_top;
+      let content_protected = self.content_protected;
       let skip_taskbar = self.skip_taskbar;
       let theme = opt_lit(self.theme.as_ref());
       let title_bar_style = &self.title_bar_style;
       let hidden_title = self.hidden_title;
       let accept_first_mouse = self.accept_first_mouse;
       let tabbing_identifier = opt_str_lit(self.tabbing_identifier.as_ref());
+      let additional_browser_args = opt_str_lit(self.additional_browser_args.as_ref());
       let shadow = self.shadow;
 
       literal_struct!(
@@ -3066,12 +3171,14 @@ mod build {
         visible,
         decorations,
         always_on_top,
+        content_protected,
         skip_taskbar,
         theme,
         title_bar_style,
         hidden_title,
         accept_first_mouse,
         tabbing_identifier,
+        additional_browser_args,
         shadow
       );
     }
@@ -3330,7 +3437,8 @@ mod build {
   impl ToTokens for UpdaterWindowsConfig {
     fn to_tokens(&self, tokens: &mut TokenStream) {
       let install_mode = &self.install_mode;
-      literal_struct!(tokens, UpdaterWindowsConfig, install_mode);
+      let installer_args = vec_lit(&self.installer_args, str_lit);
+      literal_struct!(tokens, UpdaterWindowsConfig, install_mode, installer_args);
     }
   }
 

+ 13 - 1
core/tauri-utils/src/lib.rs

@@ -5,7 +5,10 @@
 //! Tauri utility helpers
 #![warn(missing_docs, rust_2018_idioms)]
 
-use std::fmt::Display;
+use std::{
+  fmt::Display,
+  path::{Path, PathBuf},
+};
 
 use semver::Version;
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
@@ -296,3 +299,12 @@ macro_rules! debug_eprintln {
     $crate::consume_unused_variable!($($arg)*);
   };
 }
+
+/// Reconstructs a path from its components using the platform separator then converts it to String
+pub fn display_path<P: AsRef<Path>>(p: P) -> String {
+  p.as_ref()
+    .components()
+    .collect::<PathBuf>()
+    .display()
+    .to_string()
+}

+ 1 - 1
core/tauri-utils/src/pattern/isolation.js

@@ -43,7 +43,7 @@
     algorithm.iv = window.crypto.getRandomValues(new Uint8Array(12))
 
     let encoder = new TextEncoder()
-    let payloadRaw = encoder.encode(JSON.stringify(data))
+    let payloadRaw = encoder.encode(__RAW_stringify_ipc_message_fn__(data))
 
     return window.crypto.subtle
       .encrypt(algorithm, aesGcmKey, payloadRaw)

+ 5 - 2
core/tauri-utils/src/pattern/isolation.rs

@@ -8,7 +8,7 @@ use std::fmt::{Debug, Formatter};
 use std::string::FromUtf8Error;
 
 use aes_gcm::aead::Aead;
-use aes_gcm::{aead::NewAead, Aes256Gcm, Nonce};
+use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
 use getrandom::{getrandom, Error as CsprngError};
 use serialize_to_javascript::{default_template, Template};
 
@@ -57,7 +57,7 @@ impl AesGcmPair {
   fn new() -> Result<Self, Error> {
     let mut raw = [0u8; 32];
     getrandom(&mut raw)?;
-    let key = aes_gcm::Key::from_slice(&raw);
+    let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&raw);
     Ok(Self {
       raw,
       key: Aes256Gcm::new(key),
@@ -141,6 +141,9 @@ pub struct IsolationJavascriptCodegen {
 pub struct IsolationJavascriptRuntime<'a> {
   /// The key used on the Rust backend and the Isolation Javascript
   pub runtime_aes_gcm_key: &'a [u8; 32],
+  /// The function that stringifies a IPC message.
+  #[raw]
+  pub stringify_ipc_message_fn: &'a str,
 }
 
 #[cfg(test)]

+ 20 - 0
core/tauri/CHANGELOG.md

@@ -42,6 +42,16 @@
 - Export types required by 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
 
+## \[1.2.4]
+
+- Pin `ignore` to `=0.4.18`.
+  - [adcb082b](https://www.github.com/tauri-apps/tauri/commit/adcb082b1651ecb2a6208b093e12f4185aa3fc98) chore(deps): pin `ignore` to =0.4.18 on 2023-01-17
+
+## \[1.2.3]
+
+- Fix the filesystem scope allowing sub-directories of the directory picked by the dialog when `recursive` option was `false`.
+  - [f1b0ad6e](https://www.github.com/tauri-apps/tauri/commit/f1b0ad6e8b721cf1420a9a4b9be5b05c39941d16) Merge pull request from GHSA-6mv3-wm7j-h4w5 on 2022-12-22
+
 ## \[1.2.2]
 
 - Invoke event listener in windows safely to avoid causing uncaught errors in windows that have loaded external urls
@@ -109,6 +119,11 @@
 - 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.3]
+
+- Fix the filesystem scope allowing sub-directories of the directory picked by the dialog when `recursive` option was `false`.
+  - [2654c0f4](https://www.github.com/tauri-apps/tauri/commit/2654c0f49da23434d36447d0908fa24e61ff5e4e) Merge pull request from GHSA-6mv3-wm7j-h4w5 on 2022-12-22
+
 ## \[1.1.2]
 
 - Escape glob special characters in files/directories when dropping files or using the open/save dialogs.
@@ -159,6 +174,11 @@
 - 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.8]
+
+- Fix the filesystem scope allowing sub-directories of the directory picked by the dialog when `recursive` option was `false`.
+  - [f0602e7c](https://www.github.com/tauri-apps/tauri/commit/f0602e7c294245ab6ef6fbf2a976ef398340ef58) Merge pull request from GHSA-6mv3-wm7j-h4w5 on 2022-12-22
+
 ## \[1.0.7]
 
 - Escape glob special characters in files/directories when dropping files or using the open/save dialogs.

+ 6 - 3
core/tauri/Cargo.toml

@@ -66,7 +66,7 @@ flate2 = "1.0"
 http = "0.2"
 dirs-next = "2.0"
 percent-encoding = "2.2"
-base64 = { version = "0.13", optional = true }
+base64 = { version = "0.21", optional = true }
 clap = { version = "3", optional = true }
 reqwest = { version = "0.11", default-features = false, features = [ "json", "stream" ], optional = true }
 bytes = { version = "1", features = [ "serde" ], optional = true }
@@ -77,7 +77,7 @@ os_pipe = { version = "1.0", optional = true }
 raw-window-handle = "0.5"
 minisign-verify = { version = "0.2", optional = true }
 time = { version = "0.3", features = [ "parsing", "formatting" ], optional = true }
-os_info = { version = "3.5.0", optional = true }
+os_info = { version = "=3.5.0", optional = true }
 regex = { version = "1.6.0", optional = true }
 glob = "0.3"
 data-url = { version = "0.2", optional = true }
@@ -88,7 +88,7 @@ ico = { version = "0.2.0", optional = true }
 encoding_rs = "0.8.31"
 
 [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
-rfd = { version = "0.11.0", optional = true }
+rfd = { version = "0.11", optional = true, features=["gtk3", "common-controls-v6"] }
 notify-rust = { version = "4.5", default-features = false, features = [ "d" ], optional = true }
 
 [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
@@ -128,6 +128,7 @@ tauri-build = { path = "../tauri-build/" }
 [dev-dependencies]
 mockito = "0.31"
 proptest = "1.0.0"
+unarray = "=0.1.3"
 quickcheck = "1.0.3"
 quickcheck_macros = "1.0.0"
 serde = { version = "1.0", features = [ "derive" ] }
@@ -256,6 +257,7 @@ window-all = [
   "window-set-decorations",
   "window-set-shadow",
   "window-set-always-on-top",
+  "window-set-content-protected",
   "window-set-size",
   "window-set-min-size",
   "window-set-max-size",
@@ -287,6 +289,7 @@ window-close = [ ]
 window-set-decorations = [ ]
 window-set-shadow = [ ]
 window-set-always-on-top = [ ]
+window-set-content-protected = [ ]
 window-set-size = [ ]
 window-set-min-size = [ ]
 window-set-max-size = [ ]

+ 28 - 0
core/tauri/build.rs

@@ -87,6 +87,7 @@ fn main() {
       "set-decorations",
       "set-shadow",
       "set-always-on-top",
+      "set-content-protected",
       "set-size",
       "set-min-size",
       "set-max-size",
@@ -146,6 +147,14 @@ fn main() {
   )
   .expect("failed to write checked_features file");
 
+  // workaround needed to prevent `STATUS_ENTRYPOINT_NOT_FOUND` error
+  // see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864
+  let target_env = std::env::var("CARGO_CFG_TARGET_ENV");
+  let is_tauri_workspace = std::env::var("__TAURI_WORKSPACE__").map_or(false, |v| v == "true");
+  if is_tauri_workspace && target_os == "windows" && Ok("msvc") == target_env.as_deref() {
+    add_manifest();
+  }
+
   if target_os == "android" {
     if let Some(project_dir) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
       tauri_build::mobile::inject_android_project(
@@ -207,3 +216,22 @@ fn alias_module(module: &str, apis: &[&str], api_all: bool) {
 
   alias(&format!("{}_any", AsSnakeCase(module)), any);
 }
+
+fn add_manifest() {
+  static WINDOWS_MANIFEST_FILE: &str = "window-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");
+}

File diff ditekan karena terlalu besar
+ 0 - 0
core/tauri/scripts/bundle.global.js


File diff ditekan karena terlalu besar
+ 0 - 0
core/tauri/scripts/bundle.js


+ 6 - 11
core/tauri/scripts/core.js

@@ -2,7 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-;(function () {
+; (function () {
   function uid() {
     return window.crypto.getRandomValues(new Uint32Array(1))[0]
   }
@@ -96,7 +96,7 @@
           if (target.matches('a')) {
             if (
               target.href &&
-              target.href.startsWith('http') &&
+              (['http://', 'https://', 'mailto:', 'tel:'].some(v => target.href.startsWith(v))) &&
               target.target === '_blank'
             ) {
               window.__TAURI_INVOKE__('tauri', {
@@ -136,6 +136,10 @@
     if (e.target.hasAttribute('data-tauri-drag-region') && e.buttons === 1) {
       // 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
       window.__TAURI_INVOKE__('tauri', {
         __tauriModule: 'Window',
@@ -151,15 +155,6 @@
     }
   })
 
-  listen('tauri://window-created', function (event) {
-    if (event.payload) {
-      var windowLabel = event.payload.label
-      window.__TAURI_METADATA__.__windows.push({
-        label: windowLabel
-      })
-    }
-  })
-
   let permissionSettable = false
   let permissionValue = 'default'
 

+ 11 - 0
core/tauri/scripts/stringify-ipc-message-fn.js

@@ -0,0 +1,11 @@
+(function (message) {
+  return JSON.stringify(message, (_k, val) => {
+    if (val instanceof Map) {
+      let o = {};
+      val.forEach((v, k) => o[k] = v);
+      return o;
+    } else {
+      return val;
+    }
+  })
+})

+ 9 - 1
core/tauri/src/api/dialog.rs

@@ -171,7 +171,7 @@ macro_rules! message_dialog_builder {
 
 /// Options for action buttons on message dialogs.
 #[non_exhaustive]
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub enum MessageDialogButtons {
   /// Ok button.
   Ok,
@@ -179,6 +179,10 @@ pub enum MessageDialogButtons {
   OkCancel,
   /// Yes and No buttons.
   YesNo,
+  /// OK button with customized text.
+  OkWithLabel(String),
+  /// Ok and Cancel buttons with customized text.
+  OkCancelWithLabels(String, String),
 }
 
 impl From<MessageDialogButtons> for rfd::MessageButtons {
@@ -187,6 +191,10 @@ impl From<MessageDialogButtons> for rfd::MessageButtons {
       MessageDialogButtons::Ok => Self::Ok,
       MessageDialogButtons::OkCancel => Self::OkCancel,
       MessageDialogButtons::YesNo => Self::YesNo,
+      MessageDialogButtons::OkWithLabel(ok_text) => Self::OkCustom(ok_text),
+      MessageDialogButtons::OkCancelWithLabels(ok_text, cancel_text) => {
+        Self::OkCancelCustom(ok_text, cancel_text)
+      }
     }
   }
 }

+ 1 - 2
core/tauri/src/api/file/extract.rs

@@ -279,9 +279,8 @@ fn set_perms(
 ) -> crate::api::Result<()> {
   _set_perms(dst, f, mode, preserve).map_err(|_| {
     crate::api::Error::Extract(format!(
-      "failed to set permissions to {:o} \
+      "failed to set permissions to {mode:o} \
                for `{}`",
-      mode,
       dst.display()
     ))
   })

+ 3 - 4
core/tauri/src/api/ipc.rs

@@ -57,7 +57,7 @@ const MIN_JSON_PARSE_LEN: usize = 10_240;
 ///   bar: String,
 /// }
 /// let foo = Foo { bar: "x".repeat(20_000).into() };
-/// let value = serialize_js_with(&foo, SerializeOptions::default(), |v| format!("console.log({})", v)).unwrap();
+/// let value = serialize_js_with(&foo, SerializeOptions::default(), |v| format!("console.log({v})")).unwrap();
 /// assert_eq!(value, format!("console.log(JSON.parse('{{\"bar\":\"{}\"}}'))", foo.bar));
 /// ```
 pub fn serialize_js_with<T: Serialize, F: FnOnce(&str) -> String>(
@@ -179,8 +179,7 @@ pub fn format_callback<T: Serialize>(
     }} else {{
       console.warn("[TAURI] Couldn't find callback id {fn} in window. This happens when the app is reloaded while Rust is running an asynchronous operation.")
     }}"#,
-      fn = function_name.0,
-      arg = arg
+      fn = function_name.0
     )
   })
 }
@@ -281,7 +280,7 @@ mod test {
 
   // check arbitrary strings in the format callback function
   #[quickcheck]
-  fn qc_formating(f: CallbackFn, a: String) -> bool {
+  fn qc_formatting(f: CallbackFn, a: String) -> bool {
     // call format callback
     let fc = format_callback(f, &a).unwrap();
     fc.contains(&format!(

+ 3 - 3
core/tauri/src/api/notification.rs

@@ -5,7 +5,7 @@
 //! Types and functions related to desktop notifications.
 
 #[cfg(windows)]
-use std::path::MAIN_SEPARATOR;
+use std::path::MAIN_SEPARATOR as SEP;
 
 /// The desktop notification definition.
 ///
@@ -114,8 +114,8 @@ impl Notification {
       let exe_dir = exe.parent().expect("failed to get exe directory");
       let curr_dir = exe_dir.display().to_string();
       // set the notification's System.AppUserModel.ID only when running the installed app
-      if !(curr_dir.ends_with(format!("{S}target{S}debug", S = MAIN_SEPARATOR).as_str())
-        || curr_dir.ends_with(format!("{S}target{S}release", S = MAIN_SEPARATOR).as_str()))
+      if !(curr_dir.ends_with(format!("{SEP}target{SEP}debug").as_str())
+        || curr_dir.ends_with(format!("{SEP}target{SEP}release").as_str()))
       {
         notification.app_id(&self.identifier);
       }

+ 2 - 2
core/tauri/src/api/process/command.rs

@@ -137,9 +137,9 @@ pub struct Output {
 fn relative_command_path(command: String) -> crate::Result<String> {
   match platform::current_exe()?.parent() {
     #[cfg(windows)]
-    Some(exe_dir) => Ok(format!("{}\\{}.exe", exe_dir.display(), command)),
+    Some(exe_dir) => Ok(format!("{}\\{command}.exe", exe_dir.display())),
     #[cfg(not(windows))]
-    Some(exe_dir) => Ok(format!("{}/{}", exe_dir.display(), command)),
+    Some(exe_dir) => Ok(format!("{}/{command}", exe_dir.display())),
     None => Err(crate::api::Error::Command("Could not evaluate executable dir".to_string()).into()),
   }
 }

+ 1 - 1
core/tauri/src/api/shell.rs

@@ -91,7 +91,7 @@ impl Program {
 
 /// Opens path or URL with the program specified in `with`, or system default if `None`.
 ///
-/// The path will be matched against the shell open validation regex, defaulting to `^https?://`.
+/// The path will be matched against the shell open validation regex, defaulting to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
 /// A custom validation regex may be supplied in the config in `tauri > allowlist > scope > open`.
 ///
 /// # Examples

+ 66 - 6
core/tauri/src/app.rs

@@ -23,8 +23,8 @@ use crate::{
   sealed::{ManagerBase, RuntimeOrDispatch},
   utils::config::Config,
   utils::{assets::Assets, resources::resource_relpath, Env},
-  Context, EventLoopMessage, Invoke, InvokeError, InvokeResponse, Manager, Runtime, Scopes,
-  StateManager, Theme, Window,
+  Context, DeviceEventFilter, EventLoopMessage, Invoke, InvokeError, InvokeResponse, Manager,
+  Runtime, Scopes, StateManager, Theme, Window,
 };
 
 #[cfg(shell_scope)]
@@ -1061,6 +1061,35 @@ impl<R: Runtime> App<R> {
       .set_activation_policy(activation_policy);
   }
 
+  /// 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);
+  }
+
   /// Gets the argument matches of the CLI definition configured in `tauri.conf.json`.
   ///
   /// # Examples
@@ -1275,6 +1304,9 @@ pub struct Builder<R: Runtime> {
   /// The updater configuration.
   #[cfg(updater)]
   updater_settings: UpdaterSettings,
+
+  /// The device event filter.
+  device_event_filter: DeviceEventFilter,
 }
 
 impl<R: Runtime> Builder<R> {
@@ -1287,7 +1319,7 @@ impl<R: Runtime> Builder<R> {
       invoke_handler: Box::new(|_| false),
       invoke_responder: Arc::new(window_invoke_responder),
       invoke_initialization_script:
-        "Object.defineProperty(window, '__TAURI_POST_MESSAGE__', { value: (message) => window.ipc.postMessage(JSON.stringify(message)) })".into(),
+        format!("Object.defineProperty(window, '__TAURI_POST_MESSAGE__', {{ value: (message) => window.ipc.postMessage({}(message)) }})", crate::manager::STRINGIFY_IPC_MESSAGE_FN),
       on_page_load: Box::new(|_, _| ()),
       pending_windows: Default::default(),
       plugins: PluginStore::default(),
@@ -1303,6 +1335,7 @@ impl<R: Runtime> Builder<R> {
       system_tray_event_listeners: Vec::new(),
       #[cfg(updater)]
       updater_settings: Default::default(),
+      device_event_filter: Default::default(),
     }
   }
 
@@ -1737,7 +1770,7 @@ impl<R: Runtime> Builder<R> {
   /// ```
   /// let kind = if cfg!(debug_assertions) { "debug" } else { "release" };
   /// tauri::Builder::default()
-  ///   .updater_target(format!("{}-{}", tauri::updater::target().unwrap(), kind));
+  ///   .updater_target(format!("{}-{kind}", tauri::updater::target().unwrap()));
   /// ```
   ///
   /// - Use the platform's target triple:
@@ -1752,6 +1785,28 @@ impl<R: Runtime> Builder<R> {
     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.
   #[allow(clippy::type_complexity)]
   pub fn build<A: Assets>(mut self, context: Context<A>) -> crate::Result<App<R>> {
@@ -1785,6 +1840,9 @@ impl<R: Runtime> Builder<R> {
       if let Some(ua) = &config.user_agent {
         webview_attributes = webview_attributes.user_agent(&ua.to_string());
       }
+      if let Some(args) = &config.additional_browser_args {
+        webview_attributes = webview_attributes.additional_browser_args(&args.to_string());
+      }
       if !config.file_drop_enabled {
         webview_attributes = webview_attributes.disable_file_drop_handler();
       }
@@ -1797,13 +1855,15 @@ impl<R: Runtime> Builder<R> {
     }
 
     #[cfg(any(windows, target_os = "linux"))]
-    let runtime = if self.runtime_any_thread {
+    let mut runtime = if self.runtime_any_thread {
       R::new_any_thread()?
     } else {
       R::new()?
     };
     #[cfg(not(any(windows, target_os = "linux")))]
-    let runtime = R::new()?;
+    let mut runtime = R::new()?;
+
+    runtime.set_device_event_filter(self.device_event_filter);
 
     let runtime_handle = runtime.handle();
 

+ 38 - 0
core/tauri/src/app/tray.rs

@@ -64,6 +64,7 @@ pub struct SystemTray {
   icon_as_template_set: bool,
   #[cfg(target_os = "macos")]
   title: Option<String>,
+  tooltip: Option<String>,
 }
 
 impl fmt::Debug for SystemTray {
@@ -98,6 +99,7 @@ impl Default for SystemTray {
       menu_on_left_click_set: false,
       #[cfg(target_os = "macos")]
       title: None,
+      tooltip: None,
     }
   }
 }
@@ -257,6 +259,29 @@ impl SystemTray {
     self
   }
 
+  /// Sets the tray icon tooltip.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported
+  ///
+  /// # Examples
+  ///
+  /// ```
+  /// use tauri::SystemTray;
+  ///
+  /// tauri::Builder::default()
+  ///   .setup(|app| {
+  ///     let tray_handle = SystemTray::new().with_tooltip("My App").build(app)?;
+  ///     Ok(())
+  ///   });
+  /// ```
+  #[must_use]
+  pub fn with_tooltip(mut self, tooltip: &str) -> Self {
+    self.tooltip = Some(tooltip.to_owned());
+    self
+  }
+
   /// Sets the event listener for this system tray.
   ///
   /// # Examples
@@ -414,6 +439,10 @@ impl SystemTray {
       }
     }
 
+    if let Some(tooltip) = self.tooltip {
+      runtime_tray = runtime_tray.with_tooltip(&tooltip);
+    }
+
     let id = runtime_tray.id;
     let tray_handler = match manager.runtime() {
       RuntimeOrDispatch::Runtime(r) => r.system_tray(runtime_tray),
@@ -610,6 +639,15 @@ impl<R: Runtime> SystemTrayHandle<R> {
     self.inner.set_title(title).map_err(Into::into)
   }
 
+  /// Set the tooltip for this tray icon.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported
+  pub fn set_tooltip(&self, tooltip: &str) -> crate::Result<()> {
+    self.inner.set_tooltip(tooltip).map_err(Into::into)
+  }
+
   /// Destroys this system tray.
   pub fn destroy(&self) -> crate::Result<()> {
     self.inner.destroy().map_err(Into::into)

+ 30 - 5
core/tauri/src/endpoints/dialog.rs

@@ -14,19 +14,21 @@ use tauri_macros::{command_enum, module_command_handler, CommandModule};
 use std::path::PathBuf;
 
 macro_rules! message_dialog {
-  ($fn_name: ident, $allowlist: ident, $buttons: expr) => {
+  ($fn_name: ident, $allowlist: ident, $button_labels_type: ty, $buttons: expr) => {
     #[module_command_handler($allowlist)]
     fn $fn_name<R: Runtime>(
       context: InvokeContext<R>,
       title: Option<String>,
       message: String,
       level: Option<MessageDialogType>,
+      button_labels: $button_labels_type,
     ) -> super::Result<bool> {
+      let determine_button = $buttons;
       let mut builder = crate::api::dialog::blocking::MessageDialogBuilder::new(
         title.unwrap_or_else(|| context.window.app_handle.package_info().name.clone()),
         message,
       )
-      .buttons($buttons);
+      .buttons(determine_button(button_labels));
       #[cfg(any(windows, target_os = "macos"))]
       {
         builder = builder.parent(&context.window);
@@ -139,6 +141,8 @@ pub enum Cmd {
     message: String,
     #[serde(rename = "type")]
     level: Option<MessageDialogType>,
+    #[serde(rename = "buttonLabel")]
+    button_label: Option<String>,
   },
   #[cmd(dialog_ask, "dialog > ask")]
   AskDialog {
@@ -146,6 +150,8 @@ pub enum Cmd {
     message: String,
     #[serde(rename = "type")]
     level: Option<MessageDialogType>,
+    #[serde(rename = "buttonLabels")]
+    button_label: Option<(String, String)>,
   },
   #[cmd(dialog_confirm, "dialog > confirm")]
   ConfirmDialog {
@@ -153,6 +159,8 @@ pub enum Cmd {
     message: String,
     #[serde(rename = "type")]
     level: Option<MessageDialogType>,
+    #[serde(rename = "buttonLabels")]
+    button_labels: Option<(String, String)>,
   },
 }
 
@@ -255,19 +263,36 @@ impl Cmd {
   message_dialog!(
     message_dialog,
     dialog_message,
-    crate::api::dialog::MessageDialogButtons::Ok
+    Option<String>,
+    |label: Option<String>| {
+      label
+        .map(crate::api::dialog::MessageDialogButtons::OkWithLabel)
+        .unwrap_or(crate::api::dialog::MessageDialogButtons::Ok)
+    }
   );
 
   message_dialog!(
     ask_dialog,
     dialog_ask,
-    crate::api::dialog::MessageDialogButtons::YesNo
+    Option<(String, String)>,
+    |labels: Option<(String, String)>| {
+      labels
+        .map(|(yes, no)| crate::api::dialog::MessageDialogButtons::OkCancelWithLabels(yes, no))
+        .unwrap_or(crate::api::dialog::MessageDialogButtons::YesNo)
+    }
   );
 
   message_dialog!(
     confirm_dialog,
     dialog_confirm,
-    crate::api::dialog::MessageDialogButtons::OkCancel
+    Option<(String, String)>,
+    |labels: Option<(String, String)>| {
+      labels
+        .map(|(ok, cancel)| {
+          crate::api::dialog::MessageDialogButtons::OkCancelWithLabels(ok, cancel)
+        })
+        .unwrap_or(crate::api::dialog::MessageDialogButtons::OkCancel)
+    }
   );
 }
 

+ 1 - 1
core/tauri/src/endpoints/file_system.rs

@@ -386,7 +386,7 @@ fn resolve_path<R: Runtime>(
       }
     }
     Err(e) => super::Result::<SafePathBuf>::Err(e.into())
-      .with_context(|| format!("path: {}, base dir: {:?}", path.display(), dir)),
+      .with_context(|| format!("path: {}, base dir: {dir:?}", path.display())),
   }
 }
 

+ 18 - 14
core/tauri/src/endpoints/window.rs

@@ -63,10 +63,12 @@ pub enum WindowManagerCmd {
   InnerSize,
   OuterSize,
   IsFullscreen,
+  IsMinimized,
   IsMaximized,
   IsDecorated,
   IsResizable,
   IsVisible,
+  Title,
   CurrentMonitor,
   PrimaryMonitor,
   AvailableMonitors,
@@ -103,6 +105,8 @@ pub enum WindowManagerCmd {
   #[cfg(window_set_always_on_top)]
   #[serde(rename_all = "camelCase")]
   SetAlwaysOnTop(bool),
+  #[cfg(window_set_content_protected)]
+  SetContentProtected(bool),
   #[cfg(window_set_size)]
   SetSize(Size),
   #[cfg(window_set_min_size)]
@@ -165,6 +169,9 @@ pub fn into_allowlist_error(variant: &str) -> crate::Error {
     "setDecorations" => crate::Error::ApiNotAllowlisted("window > setDecorations".to_string()),
     "setShadow" => crate::Error::ApiNotAllowlisted("window > setShadow".to_string()),
     "setAlwaysOnTop" => crate::Error::ApiNotAllowlisted("window > setAlwaysOnTop".to_string()),
+    "setContentProtected" => {
+      crate::Error::ApiNotAllowlisted("window > setContentProtected".to_string())
+    }
     "setSize" => crate::Error::ApiNotAllowlisted("window > setSize".to_string()),
     "setMinSize" => crate::Error::ApiNotAllowlisted("window > setMinSize".to_string()),
     "setMaxSize" => crate::Error::ApiNotAllowlisted("window > setMaxSize".to_string()),
@@ -183,9 +190,10 @@ pub fn into_allowlist_error(variant: &str) -> crate::Error {
     }
     "startDragging" => crate::Error::ApiNotAllowlisted("window > startDragging".to_string()),
     "print" => crate::Error::ApiNotAllowlisted("window > print".to_string()),
-    "internalToggleMaximize" => {
+    "__toggleMaximize" => {
       crate::Error::ApiNotAllowlisted("window > maximize and window > unmaximize".to_string())
     }
+    "__toggleDevtools" => crate::Error::ApiNotAllowlisted("devtools".to_string()),
     _ => crate::Error::ApiNotAllowlisted("window".to_string()),
   }
 }
@@ -210,19 +218,9 @@ impl Cmd {
     context: InvokeContext<R>,
     options: Box<WindowConfig>,
   ) -> super::Result<()> {
-    let label = options.label.clone();
-    let url = options.url.clone();
-    let file_drop_enabled = options.file_drop_enabled;
-
-    let mut builder = crate::window::Window::builder(&context.window, label, url);
-    if !file_drop_enabled {
-      builder = builder.disable_file_drop_handler();
-    }
-
-    builder.window_builder =
-      <<R::Dispatcher as Dispatch<crate::EventLoopMessage>>::WindowBuilder>::with_config(*options);
-    builder.build().map_err(crate::error::into_anyhow)?;
-
+    crate::window::WindowBuilder::from_config(&context.window, *options)
+      .build()
+      .map_err(crate::error::into_anyhow)?;
     Ok(())
   }
 
@@ -256,10 +254,12 @@ impl Cmd {
       WindowManagerCmd::InnerSize => return Ok(window.inner_size()?.into()),
       WindowManagerCmd::OuterSize => return Ok(window.outer_size()?.into()),
       WindowManagerCmd::IsFullscreen => return Ok(window.is_fullscreen()?.into()),
+      WindowManagerCmd::IsMinimized => return Ok(window.is_minimized()?.into()),
       WindowManagerCmd::IsMaximized => return Ok(window.is_maximized()?.into()),
       WindowManagerCmd::IsDecorated => return Ok(window.is_decorated()?.into()),
       WindowManagerCmd::IsResizable => return Ok(window.is_resizable()?.into()),
       WindowManagerCmd::IsVisible => return Ok(window.is_visible()?.into()),
+      WindowManagerCmd::Title => return Ok(window.title()?.into()),
       WindowManagerCmd::CurrentMonitor => return Ok(window.current_monitor()?.into()),
       WindowManagerCmd::PrimaryMonitor => return Ok(window.primary_monitor()?.into()),
       WindowManagerCmd::AvailableMonitors => return Ok(window.available_monitors()?.into()),
@@ -300,6 +300,10 @@ impl Cmd {
       WindowManagerCmd::SetShadow(enable) => window.set_shadow(enable)?,
       #[cfg(all(desktop, window_set_always_on_top))]
       WindowManagerCmd::SetAlwaysOnTop(always_on_top) => window.set_always_on_top(always_on_top)?,
+      #[cfg(all(desktop, window_set_content_protected))]
+      WindowManagerCmd::SetContentProtected(protected) => {
+        window.set_content_protected(protected)?
+      }
       #[cfg(all(desktop, window_set_size))]
       WindowManagerCmd::SetSize(size) => window.set_size(size)?,
       #[cfg(all(desktop, window_set_min_size))]

+ 1 - 8
core/tauri/src/event.rs

@@ -338,22 +338,15 @@ pub fn listen_js(
         windowLabel: {window_label},
         handler: {handler}
       }};
-      if ({event} == 'tauri://window-created') {{
-        eventListeners.splice(eventListeners.length - 1, 0, listener)
-      }} else {{
-        eventListeners.push(listener);
-      }}
+      eventListeners.push(listener);
     }})()
   ",
     listeners = listeners_object_name,
-    event = event,
-    event_id = event_id,
     window_label = if let Some(l) = window_label {
       crate::runtime::window::assert_label_is_valid(&l);
       format!("'{l}'")
     } else {
       "null".to_owned()
     },
-    handler = handler
   )
 }

+ 2 - 1
core/tauri/src/lib.rs

@@ -137,6 +137,7 @@
 //! - **window-set-decorations**: Enables the [`setDecorations` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setdecorations).
 //! - **window-set-shadow**: Enables the [`setShadow` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setshadow).
 //! - **window-set-always-on-top**: Enables the [`setAlwaysOnTop` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setalwaysontop).
+//! - **window-set-content-protected**: Enables the [`setContentProtected` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setcontentprotected).
 //! - **window-set-size**: Enables the [`setSize` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setsize).
 //! - **window-set-min-size**: Enables the [`setMinSize` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setminsize).
 //! - **window-set-max-size**: Enables the [`setMaxSize` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setmaxsize).
@@ -290,7 +291,7 @@ pub use {
       dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Pixel, Position, Size},
       CursorIcon, FileDropEvent,
     },
-    RunIteration, UserAttentionType,
+    DeviceEventFilter, RunIteration, UserAttentionType,
   },
   self::state::{State, StateManager},
   self::utils::{

+ 15 - 3
core/tauri/src/manager.rs

@@ -73,6 +73,9 @@ const WINDOW_FILE_DROP_HOVER_EVENT: &str = "tauri://file-drop-hover";
 const WINDOW_FILE_DROP_CANCELLED_EVENT: &str = "tauri://file-drop-cancelled";
 const MENU_EVENT: &str = "tauri://menu";
 
+pub(crate) const STRINGIFY_IPC_MESSAGE_FN: &str =
+  include_str!("../scripts/stringify-ipc-message-fn.js");
+
 // 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
@@ -624,7 +627,7 @@ impl<R: Runtime> WindowManager<R> {
                 .insert("Content-Length", real_length.to_string());
               data.headers.insert(
                 "Content-Range",
-                format!("bytes {}-{}/{}", range.start, last_byte, file_size),
+                format!("bytes {}-{last_byte}/{file_size}", range.start),
               );
 
               if let Err(e) = file.seek(std::io::SeekFrom::Start(range.start)).await {
@@ -719,8 +722,8 @@ impl<R: Runtime> WindowManager<R> {
     } = &self.inner.pattern
     {
       let assets = assets.clone();
-      let _schema_ = schema.clone();
-      let url_base = format!("{schema}://localhost");
+      let schema_ = schema.clone();
+      let url_base = format!("{schema_}://localhost");
       let aes_gcm_key = *crypto_keys.aes_gcm().raw();
 
       pending.register_uri_scheme_protocol(schema, move |request| {
@@ -730,6 +733,7 @@ impl<R: Runtime> WindowManager<R> {
               let asset = String::from_utf8_lossy(asset.as_ref());
               let template = tauri_utils::pattern::isolation::IsolationJavascriptRuntime {
                 runtime_aes_gcm_key: &aes_gcm_key,
+                stringify_ipc_message_fn: STRINGIFY_IPC_MESSAGE_FN,
               };
               match template.render(asset.as_ref(), &Default::default()) {
                 Ok(asset) => HttpResponseBuilder::new()
@@ -1435,6 +1439,14 @@ impl<R: Runtime> WindowManager<R> {
       .try_for_each(|window| window.emit_internal(event, source_window_label, payload.clone()))
   }
 
+  pub fn eval_script_all<S: Into<String>>(&self, script: S) -> crate::Result<()> {
+    let script = script.into();
+    self
+      .windows_lock()
+      .values()
+      .try_for_each(|window| window.eval(&script))
+  }
+
   pub fn labels(&self) -> HashSet<String> {
     self.windows_lock().keys().cloned().collect()
   }

+ 2 - 2
core/tauri/src/plugin.rs

@@ -35,7 +35,7 @@ pub trait Plugin<R: Runtime>: Send {
   /// it's recommended to check the `window.location` to guard your script from running on unexpected origins.
   ///
   /// The script is wrapped into its own context with `(function () { /* your script here */ })();`,
-  /// so global variables must be assigned to `window` instead of implicity declared.
+  /// so global variables must be assigned to `window` instead of implicitly declared.
   fn initialization_script(&self) -> Option<String> {
     None
   }
@@ -205,7 +205,7 @@ impl<R: Runtime, C: DeserializeOwned> Builder<R, C> {
   /// it's recommended to check the `window.location` to guard your script from running on unexpected origins.
   ///
   /// The script is wrapped into its own context with `(function () { /* your script here */ })();`,
-  /// so global variables must be assigned to `window` instead of implicity declared.
+  /// so global variables must be assigned to `window` instead of implicitly declared.
   ///
   /// Note that calling this function multiple times overrides previous values.
   ///

+ 2 - 3
core/tauri/src/scope/fs.rs

@@ -256,10 +256,9 @@ fn escaped_pattern(p: &str) -> Result<Pattern, glob::PatternError> {
 
 fn escaped_pattern_with(p: &str, append: &str) -> Result<Pattern, glob::PatternError> {
   Pattern::new(&format!(
-    "{}{}{}",
+    "{}{}{append}",
     glob::Pattern::escape(p),
-    MAIN_SEPARATOR,
-    append
+    MAIN_SEPARATOR
   ))
 }
 

+ 1 - 1
core/tauri/src/scope/shell.rs

@@ -301,7 +301,7 @@ impl Scope {
   /// Open a path in the default (or specified) browser.
   ///
   /// The path is validated against the `tauri > allowlist > shell > open` validation regex, which
-  /// defaults to `^https?://`.
+  /// defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`.
   #[cfg(feature = "shell-open-api")]
   pub fn open(&self, path: &str, with: Option<Program>) -> Result<(), ScopeError> {
     // ensure we pass validation if the configuration has one

+ 28 - 2
core/tauri/src/test/mock_runtime.rs

@@ -12,8 +12,8 @@ use tauri_runtime::{
     dpi::{PhysicalPosition, PhysicalSize, Position, Size},
     CursorIcon, DetachedWindow, MenuEvent, PendingWindow, WindowEvent,
   },
-  Dispatch, EventLoopProxy, Icon, Result, RunEvent, Runtime, RuntimeHandle, UserAttentionType,
-  UserEvent,
+  DeviceEventFilter, Dispatch, EventLoopProxy, Icon, Result, RunEvent, Runtime, RuntimeHandle,
+  UserAttentionType, UserEvent,
 };
 #[cfg(all(desktop, feature = "system-tray"))]
 use tauri_runtime::{
@@ -270,6 +270,10 @@ impl WindowBuilder for MockWindowBuilder {
     self
   }
 
+  fn content_protected(self, protected: bool) -> Self {
+    self
+  }
+
   fn icon(self, icon: Icon) -> Result<Self> {
     Ok(self)
   }
@@ -357,6 +361,10 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
     Ok(false)
   }
 
+  fn url(&self) -> Result<url::Url> {
+    todo!()
+  }
+
   fn scale_factor(&self) -> Result<f64> {
     Ok(1.0)
   }
@@ -387,6 +395,10 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
     Ok(false)
   }
 
+  fn is_minimized(&self) -> Result<bool> {
+    Ok(false)
+  }
+
   fn is_maximized(&self) -> Result<bool> {
     Ok(false)
   }
@@ -403,6 +415,10 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
     Ok(true)
   }
 
+  fn title(&self) -> Result<String> {
+    Ok(String::new())
+  }
+
   fn is_menu_visible(&self) -> Result<bool> {
     Ok(true)
   }
@@ -513,6 +529,10 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
     Ok(())
   }
 
+  fn set_content_protected(&self, protected: bool) -> Result<()> {
+    Ok(())
+  }
+
   fn set_size(&self, size: Size) -> Result<()> {
     Ok(())
   }
@@ -605,6 +625,10 @@ impl TrayHandle for MockTrayHandler {
     Ok(())
   }
 
+  fn set_tooltip(&self, tooltip: &str) -> Result<()> {
+    Ok(())
+  }
+
   fn destroy(&self) -> Result<()> {
     Ok(())
   }
@@ -727,6 +751,8 @@ impl<T: UserEvent> Runtime<T> for MockRuntime {
   #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
   fn hide(&self) {}
 
+  fn set_device_event_filter(&mut self, filter: DeviceEventFilter) {}
+
   #[cfg(any(
     target_os = "macos",
     windows,

+ 35 - 26
core/tauri/src/updater/core.rs

@@ -9,7 +9,7 @@ use crate::{
   api::http::{ClientBuilder, HttpRequestBuilder},
   AppHandle, Manager, Runtime,
 };
-use base64::decode;
+use base64::Engine;
 use http::{
   header::{HeaderName, HeaderValue},
   HeaderMap, StatusCode,
@@ -612,15 +612,7 @@ impl<R: Runtime> Update<R> {
         archive_buffer,
         &self.extract_path,
         self.with_elevated_task,
-        self
-          .app
-          .config()
-          .tauri
-          .updater
-          .windows
-          .install_mode
-          .clone()
-          .msiexec_args(),
+        &self.app.config(),
       )?;
       #[cfg(not(target_os = "windows"))]
       copy_files_and_run(archive_buffer, &self.extract_path)?;
@@ -698,17 +690,19 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
 }
 
 // Windows
-
+//
 // ### Expected structure:
 // ├── [AppName]_[version]_x64.msi.zip          # ZIP generated by tauri-bundler
 // │   └──[AppName]_[version]_x64.msi           # Application MSI
+// ├── [AppName]_[version]_x64-setup.exe.zip          # ZIP generated by tauri-bundler
+// │   └──[AppName]_[version]_x64-setup.exe           # NSIS installer
 // └── ...
-
+//
 // ## MSI
 // Update server can provide a MSI for Windows. (Generated with tauri-bundler from *Wix*)
 // To replace current version of the application. In later version we'll offer
 // incremental update to push specific binaries.
-
+//
 // ## EXE
 // Update server can provide a custom EXE (installer) who can run any task.
 #[cfg(target_os = "windows")]
@@ -717,7 +711,7 @@ fn copy_files_and_run<R: Read + Seek>(
   archive_buffer: R,
   _extract_path: &Path,
   with_elevated_task: bool,
-  msiexec_args: &[&str],
+  config: &crate::Config,
 ) -> Result {
   // FIXME: We need to create a memory buffer with the MSI and then run it.
   //        (instead of extracting the MSI to a temp path)
@@ -736,8 +730,6 @@ fn copy_files_and_run<R: Read + Seek>(
   extractor.extract_into(&tmp_dir)?;
 
   let paths = read_dir(&tmp_dir)?;
-  // This consumes the TempDir without deleting directory on the filesystem,
-  // meaning that the directory will no longer be automatically deleted.
 
   for path in paths {
     let found_path = path?.path();
@@ -745,9 +737,15 @@ fn copy_files_and_run<R: Read + Seek>(
     // If it's an `exe` we expect an installer not a runtime.
     if found_path.extension() == Some(OsStr::new("exe")) {
       // Run the EXE
-      Command::new(found_path)
-        .spawn()
-        .expect("installer failed to start");
+      let mut installer = Command::new(found_path);
+      if crate::utils::config::WindowsUpdateInstallMode::Quiet
+        == config.tauri.updater.windows.install_mode
+      {
+        installer.arg("/S");
+      }
+      installer.args(&config.tauri.updater.windows.installer_args);
+
+      installer.spawn().expect("installer failed to start");
 
       exit(0);
     } else if found_path.extension() == Some(OsStr::new("msi")) {
@@ -760,7 +758,7 @@ fn copy_files_and_run<R: Read + Seek>(
           let product_name = bin_name.replace(".exe", "");
 
           // Check if there is a task that enables the updater to skip the UAC prompt
-          let update_task_name = format!("Update {} - Skip UAC", product_name);
+          let update_task_name = format!("Update {product_name} - Skip UAC");
           if let Ok(output) = Command::new("schtasks")
             .arg("/QUERY")
             .arg("/TN")
@@ -801,6 +799,18 @@ fn copy_files_and_run<R: Read + Seek>(
       msi_path_arg.push(&found_path);
       msi_path_arg.push("\"\"\"");
 
+      let mut msiexec_args = config
+        .tauri
+        .updater
+        .windows
+        .install_mode
+        .clone()
+        .msiexec_args()
+        .iter()
+        .map(|p| p.to_string())
+        .collect::<Vec<String>>();
+      msiexec_args.extend(config.tauri.updater.windows.installer_args.clone());
+
       // run the installer and relaunch the application
       let system_root = std::env::var("SYSTEMROOT");
       let powershell_path = system_root.as_ref().map_or_else(
@@ -968,7 +978,7 @@ pub fn extract_path_from_executable(env: &Env, executable_path: &Path) -> PathBu
 
 // Convert base64 to string and prevent failing
 fn base64_to_string(base64_string: &str) -> Result<String> {
-  let decoded_string = &decode(base64_string)?;
+  let decoded_string = &base64::engine::general_purpose::STANDARD.decode(base64_string)?;
   let result = from_utf8(decoded_string)
     .map_err(|_| Error::SignatureUtf8(base64_string.into()))?
     .to_string();
@@ -1565,11 +1575,10 @@ mod test {
     #[cfg(target_os = "windows")]
     let archive_file = "archive.windows.zip";
 
-    let good_archive_url = format!("{}/{}", mockito::server_url(), archive_file);
+    let good_archive_url = format!("{}/{archive_file}", mockito::server_url());
 
     let mut signature_file = File::open(format!(
-      "./test/updater/fixture/archives/{}.sig",
-      archive_file
+      "./test/updater/fixture/archives/{archive_file}.sig"
     ))
     .expect("Unable to open signature");
     let mut signature = String::new();
@@ -1585,10 +1594,10 @@ mod test {
       .expect("Unable to read signature as string");
 
     // add sample file
-    let _m = mockito::mock("GET", format!("/{}", archive_file).as_str())
+    let _m = mockito::mock("GET", format!("/{archive_file}").as_str())
       .with_status(200)
       .with_header("content-type", "application/octet-stream")
-      .with_body_from_file(format!("./test/updater/fixture/archives/{}", archive_file))
+      .with_body_from_file(format!("./test/updater/fixture/archives/{archive_file}"))
       .create();
 
     // sample mock for update file

+ 4 - 4
core/tauri/src/updater/mod.rs

@@ -202,7 +202,7 @@ impl<R: Runtime> UpdateBuilder<R> {
   ///     let handle = app.handle();
   ///     tauri::async_runtime::spawn(async move {
   ///       let kind = if cfg!(debug_assertions) { "debug" } else { "release" };
-  ///       let builder = tauri::updater::builder(handle).target(format!("{}-{}", tauri::updater::target().unwrap(), kind));
+  ///       let builder = tauri::updater::builder(handle).target(format!("{}-{kind}", tauri::updater::target().unwrap()));
   ///       match builder.check().await {
   ///         Ok(update) => {}
   ///         Err(error) => {}
@@ -567,13 +567,13 @@ async fn prompt_for_install<R: Runtime>(
     parent_window,
     format!(r#"A new version of {app_name} is available! "#),
     format!(
-      r#"{} {} is now available -- you have {}.
+      r#"{app_name} {} is now available -- you have {}.
 
 Would you like to install it now?
 
 Release Notes:
-{}"#,
-      app_name, update.version, update.current_version, body,
+{body}"#,
+      update.version, update.current_version
     ),
   );
 

+ 134 - 2
core/tauri/src/window.rs

@@ -7,6 +7,7 @@
 pub(crate) mod menu;
 
 pub use menu::{MenuEvent, MenuHandle};
+use url::Url;
 
 #[cfg(target_os = "macos")]
 use crate::TitleBarStyle;
@@ -28,7 +29,7 @@ use crate::{
   },
   sealed::ManagerBase,
   sealed::RuntimeOrDispatch,
-  utils::config::WindowUrl,
+  utils::config::{WindowConfig, WindowUrl},
   EventLoopMessage, Invoke, InvokeError, InvokeMessage, InvokeResolver, Manager, PageLoadPayload,
   Runtime, Theme, WindowEvent,
 };
@@ -56,6 +57,7 @@ use std::{
 };
 
 pub(crate) type WebResourceRequestHandler = dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync;
+pub(crate) type NavigationHandler = dyn Fn(Url) -> bool + Send;
 
 #[derive(Clone, Serialize)]
 struct WindowCreatedEvent {
@@ -116,6 +118,7 @@ pub struct WindowBuilder<'a, R: Runtime> {
   pub(crate) window_builder: <R::Dispatcher as Dispatch<EventLoopMessage>>::WindowBuilder,
   pub(crate) webview_attributes: WebviewAttributes,
   web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
+  navigation_handler: Option<Box<NavigationHandler>>,
 }
 
 impl<'a, R: Runtime> fmt::Debug for WindowBuilder<'a, R> {
@@ -189,9 +192,58 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
       window_builder: <R::Dispatcher as Dispatch<EventLoopMessage>>::WindowBuilder::new(),
       webview_attributes: WebviewAttributes::new(url),
       web_resource_request_handler: None,
+      navigation_handler: None,
     }
   }
 
+  /// Initializes a webview window builder from a window config 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, see [the Webview2 issue].
+  /// You should use `async` commands when creating windows.
+  ///
+  /// # Examples
+  ///
+  /// - Create a window in a command:
+  ///
+  /// ```
+  /// #[tauri::command]
+  /// async fn reopen_window(app: tauri::AppHandle) {
+  ///   let window = tauri::WindowBuilder::from_config(&app, app.config().tauri.windows.get(0).unwrap().clone())
+  ///     .build()
+  ///     .unwrap();
+  /// }
+  /// ```
+  ///
+  /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583
+  pub fn from_config<M: Manager<R>>(manager: &'a M, config: WindowConfig) -> Self {
+    let runtime = manager.runtime();
+    let app_handle = manager.app_handle();
+    let url = config.url.clone();
+    let file_drop_enabled = config.file_drop_enabled;
+    let mut builder = Self {
+      manager: manager.manager().clone(),
+      runtime,
+      app_handle,
+      label: config.label.clone(),
+      window_builder: <R::Dispatcher as Dispatch<EventLoopMessage>>::WindowBuilder::with_config(
+        config,
+      ),
+      webview_attributes: WebviewAttributes::new(url),
+      web_resource_request_handler: None,
+      navigation_handler: None,
+    };
+
+    if !file_drop_enabled {
+      builder = builder.disable_file_drop_handler();
+    }
+
+    builder
+  }
+
   /// 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.
@@ -237,6 +289,33 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
     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, WindowUrl},
+  ///   http::header::HeaderValue,
+  ///   window::WindowBuilder,
+  /// };
+  /// use std::collections::HashMap;
+  /// tauri::Builder::default()
+  ///   .setup(|app| {
+  ///     WindowBuilder::new(app, "core", WindowUrl::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<F: Fn(Url) -> bool + Send + 'static>(mut self, f: F) -> Self {
+    self.navigation_handler.replace(Box::new(f));
+    self
+  }
+
   /// Creates a new webview window.
   pub fn build(mut self) -> crate::Result<Window<R>> {
     let web_resource_request_handler = self.web_resource_request_handler.take();
@@ -246,12 +325,13 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
       self.label.clone(),
     )?;
     let labels = self.manager.labels().into_iter().collect::<Vec<_>>();
-    let pending = self.manager.prepare_window(
+    let mut pending = self.manager.prepare_window(
       self.app_handle.clone(),
       pending,
       &labels,
       web_resource_request_handler,
     )?;
+    pending.navigation_handler = self.navigation_handler.take();
     let window = match &mut self.runtime {
       RuntimeOrDispatch::Runtime(runtime) => runtime.create_window(pending),
       RuntimeOrDispatch::RuntimeHandle(handle) => handle.create_window(pending),
@@ -259,6 +339,11 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
     }
     .map(|window| self.manager.attach_window(self.app_handle.clone(), window))?;
 
+    self.manager.eval_script_all(format!(
+      "window.__TAURI_METADATA__.__windows = {window_labels_array}.map(function (label) {{ return {{ label: label }} }})",
+      window_labels_array = serde_json::to_string(&self.manager.labels())?,
+    ))?;
+
     self.manager.emit_filter(
       "tauri://window-created",
       None,
@@ -408,6 +493,13 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
     self
   }
 
+  /// Whether the window should always be on top of other windows.
+  #[must_use]
+  pub fn content_protected(mut self, protected: bool) -> Self {
+    self.window_builder = self.window_builder.content_protected(protected);
+    self
+  }
+
   /// Sets the window icon.
   pub fn icon(mut self, icon: Icon) -> crate::Result<Self> {
     self.window_builder = self.window_builder.icon(icon.try_into()?)?;
@@ -559,6 +651,22 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
     self
   }
 
+  /// Set additional arguments for the webview.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **macOS / Linux / Android / iOS**: Unsupported.
+  ///
+  /// ## Warning
+  ///
+  /// 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 {
@@ -924,6 +1032,11 @@ impl<R: Runtime> Window<R> {
     self.window.dispatcher.is_fullscreen().map_err(Into::into)
   }
 
+  /// Gets the window's current minimized state.
+  pub fn is_minimized(&self) -> crate::Result<bool> {
+    self.window.dispatcher.is_minimized().map_err(Into::into)
+  }
+
   /// Gets the window's current maximized state.
   pub fn is_maximized(&self) -> crate::Result<bool> {
     self.window.dispatcher.is_maximized().map_err(Into::into)
@@ -944,6 +1057,11 @@ impl<R: Runtime> Window<R> {
     self.window.dispatcher.is_visible().map_err(Into::into)
   }
 
+  /// Gets the window's current title.
+  pub fn title(&self) -> crate::Result<String> {
+    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.
@@ -1169,6 +1287,15 @@ impl<R: Runtime> Window<R> {
       .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<S: Into<Size>>(&self, size: S) -> crate::Result<()> {
     self
@@ -1310,6 +1437,11 @@ impl<R: Runtime> Window<R> {
 
 /// Webview APIs.
 impl<R: Runtime> Window<R> {
+  /// Returns the current url of the webview.
+  pub fn url(&self) -> crate::Result<Url> {
+    self.window.dispatcher.url().map_err(Into::into)
+  }
+
   /// Handles this window receiving an [`InvokeMessage`].
   pub fn on_message(self, payload: InvokePayload) -> crate::Result<()> {
     let manager = self.manager.clone();

+ 8 - 1
core/tests/app-updater/src/main.rs

@@ -8,6 +8,13 @@
 )]
 
 fn main() {
+  let mut context = tauri::generate_context!();
+  if std::env::var("TARGET").unwrap_or_default() == "nsis" {
+    context.config_mut().tauri.updater.windows.installer_args = vec![format!(
+      "/D={}",
+      std::env::current_exe().unwrap().parent().unwrap().display()
+    )];
+  }
   tauri::Builder::default()
     .setup(|app| {
       let handle = app.handle();
@@ -28,6 +35,6 @@ fn main() {
       });
       Ok(())
     })
-    .run(tauri::generate_context!())
+    .run(context)
     .expect("error while running tauri application");
 }

+ 188 - 108
core/tests/app-updater/tests/update.rs

@@ -54,7 +54,13 @@ fn get_cli_bin_path(cli_dir: &Path, debug: bool) -> Option<PathBuf> {
   }
 }
 
-fn build_app(cli_bin_path: &Path, cwd: &Path, config: &Config, bundle_updater: bool) {
+fn build_app(
+  cli_bin_path: &Path,
+  cwd: &Path,
+  config: &Config,
+  bundle_updater: bool,
+  target: BundleTarget,
+) {
   let mut command = Command::new(cli_bin_path);
   command
     .args(["build", "--debug", "--verbose"])
@@ -62,18 +68,22 @@ fn build_app(cli_bin_path: &Path, cwd: &Path, config: &Config, bundle_updater: b
     .arg(serde_json::to_string(config).unwrap())
     .current_dir(cwd);
 
-  #[cfg(windows)]
-  command.args(["--bundles", "msi"]);
   #[cfg(target_os = "linux")]
-  command.args(["--bundles", "appimage"]);
+  command.args(["--bundles", target.name()]);
   #[cfg(target_os = "macos")]
-  command.args(["--bundles", "app"]);
+  command.args(["--bundles", target.name()]);
 
   if bundle_updater {
+    #[cfg(windows)]
+    command.args(["--bundles", "msi", "nsis"]);
+
     command
       .env("TAURI_PRIVATE_KEY", UPDATER_PRIVATE_KEY)
       .env("TAURI_KEY_PASSWORD", "")
       .args(["--bundles", "updater"]);
+  } else {
+    #[cfg(windows)]
+    command.args(["--bundles", target.name()]);
   }
 
   let status = command
@@ -85,21 +95,62 @@ fn build_app(cli_bin_path: &Path, cwd: &Path, config: &Config, bundle_updater: b
   }
 }
 
+#[derive(Copy, Clone)]
+enum BundleTarget {
+  AppImage,
+
+  App,
+
+  Msi,
+  Nsis,
+}
+
+impl BundleTarget {
+  fn name(self) -> &'static str {
+    match self {
+      Self::AppImage => "appimage",
+      Self::App => "app",
+      Self::Msi => "msi",
+      Self::Nsis => "nsis",
+    }
+  }
+}
+
+impl Default for BundleTarget {
+  fn default() -> Self {
+    #[cfg(any(target_os = "macos", target_os = "ios"))]
+    return Self::App;
+    #[cfg(target_os = "linux")]
+    return Self::App;
+    #[cfg(windows)]
+    return Self::Nsis;
+  }
+}
+
 #[cfg(target_os = "linux")]
-fn bundle_path(root_dir: &Path, version: &str) -> PathBuf {
-  root_dir.join(format!(
-    "target/debug/bundle/appimage/app-updater_{version}_amd64.AppImage"
-  ))
+fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> {
+  vec![(
+    BundleTarget::AppImage,
+    root_dir.join(format!(
+      "target/debug/bundle/appimage/app-updater_{version}_amd64.AppImage"
+    )),
+  )]
 }
 
 #[cfg(target_os = "macos")]
-fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf {
-  root_dir.join("target/debug/bundle/macos/app-updater.app")
+fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> {
+  vec![(
+    BundleTarget::App,
+    root_dir.join(format!("target/debug/bundle/macos/app-updater.app")),
+  )]
 }
 
 #[cfg(target_os = "ios")]
-fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf {
-  root_dir.join(format!("target/debug/bundle/ios/app-updater.ipa"))
+fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> {
+  vec![(
+    BundleTarget::App,
+    root_dir.join(format!("target/debug/bundle/ios/app-updater.ipa")),
+  )]
 }
 
 #[cfg(target_os = "android")]
@@ -108,11 +159,21 @@ fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf {
 }
 
 #[cfg(windows)]
-fn bundle_path(root_dir: &Path, version: &str) -> PathBuf {
-  root_dir.join(format!(
-    "target/debug/bundle/msi/app-updater_{}_x64_en-US.msi",
-    version
-  ))
+fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> {
+  vec![
+    (
+      BundleTarget::Nsis,
+      root_dir.join(format!(
+        "target/debug/bundle/nsis/app-updater_{version}_x64-setup.exe"
+      )),
+    ),
+    (
+      BundleTarget::Msi,
+      root_dir.join(format!(
+        "target/debug/bundle/msi/app-updater_{version}_x64_en-US.msi"
+      )),
+    ),
+  ]
 }
 
 #[test]
@@ -144,99 +205,118 @@ fn update_app() {
   };
 
   // bundle app update
-  build_app(&cli_bin_path, &manifest_dir, &config, true);
-
-  let updater_ext = if cfg!(windows) { "zip" } else { "tar.gz" };
-
-  let out_bundle_path = bundle_path(&root_dir, "1.0.0");
-  let signature_path = out_bundle_path.with_extension(format!(
-    "{}.{}.sig",
-    out_bundle_path.extension().unwrap().to_str().unwrap(),
-    updater_ext
-  ));
-  let signature = std::fs::read_to_string(&signature_path)
-    .unwrap_or_else(|_| panic!("failed to read signature file {}", signature_path.display()));
-  let out_updater_path = out_bundle_path.with_extension(format!(
-    "{}.{}",
-    out_bundle_path.extension().unwrap().to_str().unwrap(),
-    updater_ext
-  ));
-  let updater_path = root_dir.join(format!(
-    "target/debug/{}",
-    out_updater_path.file_name().unwrap().to_str().unwrap()
-  ));
-  std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle");
-
-  std::thread::spawn(move || {
-    // start the updater server
-    let server = tiny_http::Server::http("localhost:3007").expect("failed to start updater server");
-
-    loop {
-      if let Ok(request) = server.recv() {
-        match request.url() {
-          "/" => {
-            let mut platforms = HashMap::new();
-
-            platforms.insert(
-              target.clone(),
-              PlatformUpdate {
-                signature: signature.clone(),
-                url: "http://localhost:3007/download",
-                with_elevated_task: false,
-              },
-            );
-            let body = serde_json::to_vec(&Update {
-              version: "1.0.0",
-              date: time::OffsetDateTime::now_utc()
-                .format(&time::format_description::well_known::Rfc3339)
-                .unwrap(),
-              platforms,
-            })
-            .unwrap();
-            let len = body.len();
-            let response = tiny_http::Response::new(
-              tiny_http::StatusCode(200),
-              Vec::new(),
-              std::io::Cursor::new(body),
-              Some(len),
-              None,
-            );
-            let _ = request.respond(response);
-          }
-          "/download" => {
-            let _ = request.respond(tiny_http::Response::from_file(
-              File::open(&updater_path).unwrap_or_else(|_| {
-                panic!("failed to open updater bundle {}", updater_path.display())
-              }),
-            ));
+  build_app(
+    &cli_bin_path,
+    &manifest_dir,
+    &config,
+    true,
+    Default::default(),
+  );
+
+  let updater_zip_ext = if cfg!(windows) { "zip" } else { "tar.gz" };
+
+  for (bundle_target, out_bundle_path) in bundle_paths(&root_dir, "1.0.0") {
+    let bundle_updater_ext = out_bundle_path
+      .extension()
+      .unwrap()
+      .to_str()
+      .unwrap()
+      .replace("exe", "nsis");
+    let signature_path =
+      out_bundle_path.with_extension(format!("{bundle_updater_ext}.{updater_zip_ext}.sig"));
+    let signature = std::fs::read_to_string(&signature_path)
+      .unwrap_or_else(|_| panic!("failed to read signature file {}", signature_path.display()));
+    let out_updater_path =
+      out_bundle_path.with_extension(format!("{}.{}", bundle_updater_ext, updater_zip_ext));
+    let updater_path = root_dir.join(format!(
+      "target/debug/{}",
+      out_updater_path.file_name().unwrap().to_str().unwrap()
+    ));
+    std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle");
+
+    let target = target.clone();
+    std::thread::spawn(move || {
+      // start the updater server
+      let server =
+        tiny_http::Server::http("localhost:3007").expect("failed to start updater server");
+
+      loop {
+        if let Ok(request) = server.recv() {
+          match request.url() {
+            "/" => {
+              let mut platforms = HashMap::new();
+
+              platforms.insert(
+                target.clone(),
+                PlatformUpdate {
+                  signature: signature.clone(),
+                  url: "http://localhost:3007/download",
+                  with_elevated_task: false,
+                },
+              );
+              let body = serde_json::to_vec(&Update {
+                version: "1.0.0",
+                date: time::OffsetDateTime::now_utc()
+                  .format(&time::format_description::well_known::Rfc3339)
+                  .unwrap(),
+                platforms,
+              })
+              .unwrap();
+              let len = body.len();
+              let response = tiny_http::Response::new(
+                tiny_http::StatusCode(200),
+                Vec::new(),
+                std::io::Cursor::new(body),
+                Some(len),
+                None,
+              );
+              let _ = request.respond(response);
+            }
+            "/download" => {
+              let _ = request.respond(tiny_http::Response::from_file(
+                File::open(&updater_path).unwrap_or_else(|_| {
+                  panic!("failed to open updater bundle {}", updater_path.display())
+                }),
+              ));
+              // close server
+              return;
+            }
+            _ => (),
           }
-          _ => (),
         }
       }
-    }
-  });
-
-  config.package.version = "0.1.0";
-
-  // bundle initial app version
-  build_app(&cli_bin_path, &manifest_dir, &config, false);
-
-  let mut binary_cmd = if cfg!(windows) {
-    Command::new(root_dir.join("target/debug/app-updater.exe"))
-  } else if cfg!(target_os = "macos") {
-    Command::new(bundle_path(&root_dir, "0.1.0").join("Contents/MacOS/app-updater"))
-  } else if std::env::var("CI").map(|v| v == "true").unwrap_or_default() {
-    let mut c = Command::new("xvfb-run");
-    c.arg("--auto-servernum")
-      .arg(bundle_path(&root_dir, "0.1.0"));
-    c
-  } else {
-    Command::new(bundle_path(&root_dir, "0.1.0"))
-  };
+    });
+
+    config.package.version = "0.1.0";
+
+    // bundle initial app version
+    build_app(&cli_bin_path, &manifest_dir, &config, false, bundle_target);
+
+    let mut binary_cmd = if cfg!(windows) {
+      Command::new(root_dir.join("target/debug/app-updater.exe"))
+    } else if cfg!(target_os = "macos") {
+      Command::new(
+        bundle_paths(&root_dir, "0.1.0")
+          .first()
+          .unwrap()
+          .1
+          .join("Contents/MacOS/app-updater"),
+      )
+    } else if std::env::var("CI").map(|v| v == "true").unwrap_or_default() {
+      let mut c = Command::new("xvfb-run");
+      c.arg("--auto-servernum")
+        .arg(&bundle_paths(&root_dir, "0.1.0").first().unwrap().1);
+      c
+    } else {
+      Command::new(&bundle_paths(&root_dir, "0.1.0").first().unwrap().1)
+    };
+
+    binary_cmd.env("TARGET", bundle_target.name());
+
+    let status = binary_cmd.status().expect("failed to run app");
 
-  let status = binary_cmd.status().expect("failed to run app");
-
-  if !status.success() {
-    panic!("failed to run app");
+    if !status.success() {
+      panic!("failed to run app");
+    }
   }
 }

File diff ditekan karena terlalu besar
+ 0 - 0
examples/api/dist/assets/index.js


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini