Merge remote-tracking branch 'origin/dev' into feat/command-tests

This commit is contained in:
Lucas Nogueira 2023-01-04 19:00:48 -03:00
commit 83a9e6c572
No known key found for this signature in database
GPG Key ID: FFEA6C72E73482F1
200 changed files with 7154 additions and 1217 deletions

2
.cargo/config Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

6
.changes/cli-info.md Normal file
View File

@ -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.

View File

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

View File

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

View File

@ -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.

View File

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

View File

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

View File

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

View File

@ -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
.changes/custom-icon.md Normal file
View File

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

View File

@ -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
.changes/fix-2549.md Normal file
View File

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

View File

@ -0,0 +1,5 @@
---
"tauri": "patch"
---
Fix the filesystem scope allowing sub-directories of the directory picked by the dialog when `recursive` option was `false`.

5
.changes/heck-unicode.md Normal file
View File

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

8
.changes/is-minimized.md Normal file
View File

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

8
.changes/nsis.md Normal file
View File

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

View File

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

View File

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

View File

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

8
.changes/title-getter.md Normal file
View File

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

View File

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

5
.changes/tray-tooltip.md Normal file
View File

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

7
.changes/url-getter.md Normal file
View File

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

View File

@ -0,0 +1,5 @@
---
'cli.rs': 'minor'
---
Add support for Cargo's workspace inheritance for the package version.

View File

@ -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.

View File

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

View File

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

View File

@ -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 }}

View File

@ -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'

View File

@ -124,7 +124,7 @@ jobs:
token: ${{ secrets.ORG_TAURI_BOT_PAT }}
repository: tauri-apps/tauri
event-type: publish-clijs
inputs: '{"releaseId": "${{ steps.covector.outputs.cli.js-releaseId }}" }'
client-payload: '{"releaseId": "${{ steps.covector.outputs.cli.js-releaseId }}" }'
- name: Trigger cli.rs publishing workflow
if: |

View File

@ -397,4 +397,4 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.ORG_NPM_TOKEN }}
RELEASE_ID: ${{ github.event.inputs.releaseId }}
RELEASE_ID: ${{ github.event.client_payload.releaseId || github.event.inputs.releaseId }}

View File

@ -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[@]}"

View File

@ -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

View File

@ -13,7 +13,16 @@ If you have found a potential security threat, vulnerability or exploit in Tauri
or one of its upstream dependencies, please DONT create a pull-request, DONT
file an issue on GitHub, DONT mention it on Discord and DONT 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.

View File

@ -18,7 +18,7 @@ pub fn main() -> Result<(), Box<dyn Error>> {
crate_dir.join("../../tooling/cli/schema.json"),
] {
let mut schema_file = BufWriter::new(File::create(file)?);
write!(schema_file, "{}", schema_str)?;
write!(schema_file, "{schema_str}")?;
}
Ok(())

View File

@ -105,6 +105,7 @@
"print": false,
"requestUserAttention": false,
"setAlwaysOnTop": false,
"setContentProtected": false,
"setCursorGrab": false,
"setCursorIcon": false,
"setCursorPosition": false,
@ -145,6 +146,7 @@
"allowDowngrades": true,
"certificateThumbprint": null,
"digestAlgorithm": null,
"nsis": null,
"timestampUrl": null,
"tsp": false,
"webviewFixedRuntimePath": null,
@ -168,7 +170,8 @@
"dialog": true,
"pubkey": "",
"windows": {
"installMode": "passive"
"installMode": "passive",
"installerArgs": []
}
},
"windows": []
@ -281,6 +284,7 @@
"allowDowngrades": true,
"certificateThumbprint": null,
"digestAlgorithm": null,
"nsis": null,
"timestampUrl": null,
"tsp": false,
"webviewFixedRuntimePath": null,
@ -378,6 +382,7 @@
"print": false,
"requestUserAttention": false,
"setAlwaysOnTop": false,
"setContentProtected": false,
"setCursorGrab": false,
"setCursorIcon": false,
"setCursorPosition": false,
@ -425,7 +430,8 @@
"dialog": true,
"pubkey": "",
"windows": {
"installMode": "passive"
"installMode": "passive",
"installerArgs": []
}
},
"allOf": [
@ -641,6 +647,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,
@ -682,6 +693,13 @@
"string",
"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"
]
}
},
"additionalProperties": false
@ -1004,7 +1022,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": [
{
@ -1118,6 +1136,7 @@
"allowDowngrades": true,
"certificateThumbprint": null,
"digestAlgorithm": null,
"nsis": null,
"timestampUrl": null,
"tsp": false,
"webviewFixedRuntimePath": null,
@ -1186,6 +1205,13 @@
"msi"
]
},
{
"description": "The NSIS bundle (.exe).",
"type": "string",
"enum": [
"nsis"
]
},
{
"description": "The macOS application bundle (.app).",
"type": "string",
@ -1370,6 +1396,17 @@
"type": "null"
}
]
},
"nsis": {
"description": "Configuration for the installer generated with NSIS.",
"anyOf": [
{
"$ref": "#/definitions/NsisConfig"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
@ -1618,8 +1655,78 @@
},
"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"
}
]
}
},
"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"
]
}
]
},
"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": {
@ -1661,6 +1768,7 @@
"print": false,
"requestUserAttention": false,
"setAlwaysOnTop": false,
"setContentProtected": false,
"setCursorGrab": false,
"setCursorIcon": false,
"setCursorPosition": false,
@ -2011,6 +2119,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,
@ -2211,7 +2324,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"
},
{
@ -2559,7 +2672,8 @@
"windows": {
"description": "The Windows configuration for the updater.",
"default": {
"installMode": "passive"
"installMode": "passive",
"installerArgs": []
},
"allOf": [
{
@ -2579,6 +2693,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",
@ -2602,7 +2724,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"

View File

@ -21,10 +21,10 @@ anyhow = "1"
quote = { version = "1", optional = true }
tauri-codegen = { version = "1.2.1", path = "../tauri-codegen", optional = true }
tauri-utils = { version = "1.2.1", path = "../tauri-utils", features = [ "build", "resources" ] }
cargo_toml = "0.13"
cargo_toml = "0.14"
serde_json = "1"
heck = "0.4"
json-patch = "0.2"
json-patch = "0.3"
[target."cfg(windows)".dependencies]
winres = "0.1"

View File

@ -86,7 +86,7 @@ impl CodegenContext {
pub fn build(self) -> PathBuf {
match self.try_build() {
Ok(out) => out,
Err(error) => panic!("Error found during Codegen::build: {}", error),
Err(error) => panic!("Error found during Codegen::build: {error}"),
}
}
@ -161,7 +161,7 @@ impl CodegenContext {
)
})?;
writeln!(file, "{}", code).with_context(|| {
writeln!(file, "{code}").with_context(|| {
format!(
"Unable to write tokenstream to out file during tauri-build {}",
out.display()

View File

@ -35,8 +35,8 @@ fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
Ok(())
}
fn copy_binaries<'a>(
binaries: ResourcePaths<'a>,
fn copy_binaries(
binaries: ResourcePaths,
target_triple: &str,
path: &Path,
package_name: Option<&String>,
@ -48,7 +48,7 @@ fn copy_binaries<'a>(
.file_name()
.expect("failed to extract external binary filename")
.to_string_lossy()
.replace(&format!("-{}", target_triple), "");
.replace(&format!("-{target_triple}"), "");
if package_name.map_or(false, |n| n == &file_name) {
return Err(anyhow::anyhow!(
@ -90,7 +90,7 @@ fn has_feature(feature: &str) -> bool {
// `alias` must be a snake case string.
fn cfg_alias(alias: &str, has_feature: bool) {
if has_feature {
println!("cargo:rustc-cfg={}", alias);
println!("cargo:rustc-cfg={alias}");
}
}
@ -110,6 +110,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 {
@ -135,6 +144,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.
@ -179,8 +227,8 @@ impl Attributes {
/// This is typically desirable when running inside a build script; see [`try_build`] for no panics.
pub fn build() {
if let Err(error) = try_build(Attributes::default()) {
let error = format!("{:#}", error);
println!("{}", error);
let error = format!("{error:#}");
println!("{error}");
if error.starts_with("unknown field") {
print!("found an unknown configuration field. This usually means that you are using a CLI version that is newer than `tauri-build` and is incompatible. ");
println!(
@ -336,24 +384,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() {

View File

@ -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>

View File

@ -14,7 +14,7 @@ readme = "README.md"
[dependencies]
sha2 = "0.10"
base64 = "0.13"
base64 = "0.20"
proc-macro2 = "1"
quote = "1"
serde = { version = "1", features = [ "derive" ] }
@ -26,9 +26,9 @@ brotli = { version = "3", optional = true, default-features = false, features =
regex = { version = "1.7.0", 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"
[target."cfg(target_os = \"macos\")".dependencies]
plist = "1"

View File

@ -141,7 +141,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
} else if target.contains("apple-ios") {
Target::Ios
} else {
panic!("unknown codegen target {}", target);
panic!("unknown codegen target {target}");
}
} else if cfg!(target_os = "linux") {
Target::Linux
@ -370,10 +370,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
PatternKind::Isolation { dir } => {
let dir = config_parent.join(dir);
if !dir.exists() {
panic!(
"The isolation application path is set to `{:?}` but it does not exist",
dir
)
panic!("The isolation application path is set to `{dir:?}` but it does not exist")
}
let mut sets_isolation_hook = false;
@ -414,7 +411,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())),

View File

@ -343,14 +343,14 @@ impl EmbeddedAssets {
let mut hex = String::with_capacity(2 * bytes.len());
for b in bytes {
write!(hex, "{:02x}", b).map_err(EmbeddedAssetsError::Hex)?;
write!(hex, "{b:02x}").map_err(EmbeddedAssetsError::Hex)?;
}
hex
};
// use the content hash to determine filename, keep extensions that exist
let out_path = if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
out_dir.join(format!("{}.{}", hash, ext))
out_dir.join(format!("{hash}.{ext}"))
} else {
out_dir.join(hash)
};

View File

@ -13,7 +13,7 @@ exclude = [ "CHANGELOG.md", "/target" ]
readme = "README.md"
[dependencies]
wry = { version = "0.23", default-features = false, features = [ "file-drop", "protocol" ] }
wry = { git = "https://github.com/tauri-apps/wry", default-features = false, features = [ "file-drop", "protocol" ] }
tauri-runtime = { version = "0.12.1", path = "../tauri-runtime" }
tauri-utils = { version = "1.2.1", path = "../tauri-utils" }
uuid = { version = "1", features = [ "v4" ] }

View File

@ -6,7 +6,7 @@
// `alias` must be a snake case string.
fn alias(alias: &str, has_feature: bool) {
if has_feature {
println!("cargo:rustc-cfg={}", alias);
println!("cargo:rustc-cfg={alias}");
}
}

View File

@ -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)]
@ -79,6 +81,7 @@ pub use wry::application::platform::macos::{
};
use std::{
borrow::Cow,
cell::RefCell,
collections::{
hash_map::Entry::{Occupied, Vacant},
@ -284,7 +287,7 @@ impl From<&WryRequest<Vec<u8>>> for HttpRequestWrapper {
}
// response
struct HttpResponseWrapper(WryResponse<Vec<u8>>);
struct HttpResponseWrapper(WryResponse<Cow<'static, [u8]>>);
impl From<HttpResponse> for HttpResponseWrapper {
fn from(response: HttpResponse) -> Self {
let (parts, body) = response.into_parts();
@ -298,7 +301,7 @@ impl From<HttpResponse> for HttpResponseWrapper {
res_builder = res_builder.header(name, val);
}
let res = res_builder.body(body).unwrap();
let res = res_builder.body(Cow::Owned(body)).unwrap();
Self(res)
}
}
@ -364,6 +367,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);
@ -705,6 +720,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);
@ -839,6 +855,11 @@ impl WindowBuilder for WindowBuilderWrapper {
self
}
fn content_protected(mut self, protected: bool) -> Self {
self.inner = self.inner.with_content_protection(protected);
self
}
#[cfg(windows)]
fn parent_window(mut self, parent: HWND) -> Self {
self.inner = self.inner.with_parent_window(parent);
@ -1017,16 +1038,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>>),
@ -1057,6 +1081,7 @@ pub enum WindowMessage {
Close,
SetDecorations(bool),
SetAlwaysOnTop(bool),
SetContentProtected(bool),
SetSize(Size),
SetMinSize(Option<Size>),
SetMaxSize(Option<Size>),
@ -1099,6 +1124,7 @@ pub enum TrayMessage {
UpdateIconAsTemplate(bool),
#[cfg(target_os = "macos")]
UpdateTitle(String),
UpdateTooltip(String),
Create(SystemTray, Sender<Result<()>>),
Destroy(Sender<Result<()>>),
}
@ -1215,6 +1241,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)
}
@ -1239,6 +1269,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)
}
@ -1257,6 +1291,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)
}
@ -1426,6 +1464,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,
@ -2028,6 +2076,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;
@ -2315,6 +2369,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(
@ -2339,10 +2398,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(),
@ -2387,6 +2448,9 @@ fn handle_user_message<T: UserEvent>(
}
WindowMessage::SetDecorations(decorations) => window.set_decorations(decorations),
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);
}
@ -2573,6 +2637,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
}
@ -2788,6 +2857,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());
}
@ -2962,6 +3044,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()
@ -2998,9 +3083,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,

View File

@ -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(

View File

@ -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.19.1"

View File

@ -6,7 +6,7 @@
// `alias` must be a snake case string.
fn alias(alias: &str, has_feature: bool) {
if has_feature {
println!("cargo:rustc-cfg={}", alias);
println!("cargo:rustc-cfg={alias}");
}
}

View File

@ -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 {
@ -461,6 +493,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;
@ -500,6 +545,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>;
@ -522,6 +570,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>;
@ -533,6 +584,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>;
@ -623,6 +676,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<()>;

View File

@ -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<()>;
}

View File

@ -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>;

View File

@ -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},
@ -232,6 +233,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>>,
}
pub fn is_label_valid(label: &str) -> bool {
@ -271,6 +275,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(),
})
}
}
@ -300,6 +305,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(),
})
}
}

View File

@ -55,7 +55,7 @@ pub enum WindowUrl {
impl fmt::Display for WindowUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::External(url) => write!(f, "{}", url),
Self::External(url) => write!(f, "{url}"),
Self::App(path) => write!(f, "{}", path.display()),
}
}
@ -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,10 +125,11 @@ 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),
_ => Err(DeError::custom(format!("unknown bundle target '{}'", s))),
_ => Err(DeError::custom(format!("unknown bundle target '{s}'"))),
}
}
}
@ -223,7 +227,7 @@ impl<'de> Deserialize<'de> for BundleTarget {
match BundleTargetInner::deserialize(deserializer)? {
BundleTargetInner::All(s) if s.to_lowercase() == "all" => Ok(Self::All),
BundleTargetInner::All(t) => Err(DeError::custom(format!("invalid bundle type {}", t))),
BundleTargetInner::All(t) => Err(DeError::custom(format!("invalid bundle type {t}"))),
BundleTargetInner::List(l) => Ok(Self::List(l)),
BundleTargetInner::One(t) => Ok(Self::One(t)),
}
@ -416,6 +420,58 @@ 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.
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.
pub sidebar_image: Option<PathBuf>,
/// The path to an icon file used as the installer icon.
pub installer_icon: Option<PathBuf>,
/// Whether the installation will be for all users or just the current user.
#[serde(default)]
pub install_mode: NSISInstallerMode,
}
/// 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.
///
@ -512,6 +568,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 {
@ -525,6 +583,7 @@ impl Default for WindowsConfig {
webview_fixed_runtime_path: None,
allow_downgrades: default_allow_downgrades(),
wix: None,
nsis: None,
}
}
}
@ -542,7 +601,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`).
@ -859,6 +918,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,
@ -881,6 +943,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>,
}
impl Default for WindowConfig {
@ -908,12 +974,14 @@ impl Default for WindowConfig {
visible: default_visible(),
decorations: default_decorations(),
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,
}
}
}
@ -985,7 +1053,7 @@ impl CspDirectiveSources {
/// Whether the given source is configured on this directive or not.
pub fn contains(&self, source: &str) -> bool {
match self {
Self::Inline(s) => s.contains(&format!("{} ", source)) || s.contains(&format!(" {}", source)),
Self::Inline(s) => s.contains(&format!("{source} ")) || s.contains(&format!(" {source}")),
Self::List(l) => l.contains(&source.into()),
}
}
@ -1051,7 +1119,7 @@ impl From<Csp> for HashMap<String, CspDirectiveSources> {
impl Display for Csp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Policy(s) => write!(f, "{}", s),
Self::Policy(s) => write!(f, "{s}"),
Self::DirectiveMap(m) => {
let len = m.len();
let mut i = 0;
@ -1329,6 +1397,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,
@ -1394,6 +1465,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,
@ -1444,6 +1516,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");
@ -1599,7 +1677,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.
@ -2062,7 +2140,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)]
@ -2287,7 +2374,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,
@ -2346,8 +2433,7 @@ impl<'de> Deserialize<'de> for WindowsUpdateInstallMode {
"quiet" => Ok(Self::Quiet),
"passive" => Ok(Self::Passive),
_ => Err(DeError::custom(format!(
"unknown update install mode '{}'",
s
"unknown update install mode '{s}'"
))),
}
}
@ -2359,6 +2445,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,
@ -2491,7 +2580,7 @@ pub enum AppUrl {
impl std::fmt::Display for AppUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Url(url) => write!(f, "{}", url),
Self::Url(url) => write!(f, "{url}"),
Self::Files(files) => write!(f, "{}", serde_json::to_string(files).unwrap()),
}
}
@ -2630,9 +2719,9 @@ impl<'d> serde::Deserialize<'d> for PackageVersion {
let path = PathBuf::from(value);
if path.exists() {
let json_str = read_to_string(&path)
.map_err(|e| DeError::custom(format!("failed to read version JSON file: {}", e)))?;
.map_err(|e| DeError::custom(format!("failed to read version JSON file: {e}")))?;
let package_json: serde_json::Value = serde_json::from_str(&json_str)
.map_err(|e| DeError::custom(format!("failed to read version JSON file: {}", e)))?;
.map_err(|e| DeError::custom(format!("failed to read version JSON file: {e}")))?;
if let Some(obj) = package_json.as_object() {
let version = obj
.get("version")
@ -3040,12 +3129,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());
literal_struct!(
tokens,
@ -3072,12 +3163,14 @@ mod build {
visible,
decorations,
always_on_top,
content_protected,
skip_taskbar,
theme,
title_bar_style,
hidden_title,
accept_first_mouse,
tabbing_identifier
tabbing_identifier,
additional_browser_args
);
}
}
@ -3333,7 +3426,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);
}
}

View File

@ -37,7 +37,7 @@ fn serialize_node_ref_internal<S: Serializer>(
traversal_scope: TraversalScope,
) -> crate::Result<()> {
match (traversal_scope, node.data()) {
(ref scope, &NodeData::Element(ref element)) => {
(ref scope, NodeData::Element(element)) => {
if *scope == TraversalScope::IncludeNode {
let attrs = element.attributes.borrow();
@ -82,16 +82,16 @@ fn serialize_node_ref_internal<S: Serializer>(
(TraversalScope::ChildrenOnly(_), _) => Ok(()),
(TraversalScope::IncludeNode, &NodeData::Doctype(ref doctype)) => {
(TraversalScope::IncludeNode, NodeData::Doctype(doctype)) => {
serializer.write_doctype(&doctype.name).map_err(Into::into)
}
(TraversalScope::IncludeNode, &NodeData::Text(ref text)) => {
(TraversalScope::IncludeNode, NodeData::Text(text)) => {
serializer.write_text(&text.borrow()).map_err(Into::into)
}
(TraversalScope::IncludeNode, &NodeData::Comment(ref text)) => {
(TraversalScope::IncludeNode, NodeData::Comment(text)) => {
serializer.write_comment(&text.borrow()).map_err(Into::into)
}
(TraversalScope::IncludeNode, &NodeData::ProcessingInstruction(ref contents)) => {
(TraversalScope::IncludeNode, NodeData::ProcessingInstruction(contents)) => {
let contents = contents.borrow();
serializer
.write_processing_instruction(&contents.0, &contents.1)

View File

@ -39,7 +39,7 @@ impl std::fmt::Display for MimeType {
MimeType::Svg => "image/svg+xml",
MimeType::Mp4 => "video/mp4",
};
write!(f, "{}", mime)
write!(f, "{mime}")
}
}

View File

@ -133,10 +133,10 @@ pub fn target_triple() -> crate::Result<String> {
return Err(crate::Error::Environment);
};
format!("{}-{}", os, env)
format!("{os}-{env}")
};
Ok(format!("{}-{}", arch, os))
Ok(format!("{arch}-{os}"))
}
/// Computes the resource directory of the current environment.
@ -157,8 +157,8 @@ pub fn resource_dir(package_info: &PackageInfo, env: &Env) -> crate::Result<Path
let exe_dir = exe.parent().expect("failed to get exe directory");
let curr_dir = exe_dir.display().to_string();
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!("{MAIN_SEPARATOR}target{MAIN_SEPARATOR}debug").as_str())
|| curr_dir.ends_with(format!("{MAIN_SEPARATOR}target{MAIN_SEPARATOR}release").as_str())
|| cfg!(target_os = "windows")
{
// running from the out dir or windows

View File

@ -1,5 +1,10 @@
# Changelog
## \[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
@ -67,6 +72,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.
@ -117,6 +127,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.

View File

@ -10,7 +10,7 @@ license = "Apache-2.0 OR MIT"
name = "tauri"
readme = "README.md"
repository = "https://github.com/tauri-apps/tauri"
version = "1.2.2"
version = "1.2.3"
[package.metadata.docs.rs]
no-default-features = true
@ -87,7 +87,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.10", optional = true }
rfd = { version = "0.10", 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]
@ -240,6 +240,7 @@ window-all = [
"window-close",
"window-set-decorations",
"window-set-always-on-top",
"window-set-content-protected",
"window-set-size",
"window-set-min-size",
"window-set-max-size",
@ -270,6 +271,7 @@ window-hide = [ ]
window-close = [ ]
window-set-decorations = [ ]
window-set-always-on-top = [ ]
window-set-content-protected = [ ]
window-set-size = [ ]
window-set-min-size = [ ]
window-set-max-size = [ ]

View File

@ -31,7 +31,7 @@ fn has_feature(feature: &str) -> bool {
// `alias` must be a snake case string.
fn alias(alias: &str, has_feature: bool) {
if has_feature {
println!("cargo:rustc-cfg={}", alias);
println!("cargo:rustc-cfg={alias}");
}
}
@ -81,6 +81,7 @@ fn main() {
"close",
"set-decorations",
"set-always-on-top",
"set-content-protected",
"set-size",
"set-min-size",
"set-max-size",
@ -140,6 +141,18 @@ fn main() {
CHECKED_FEATURES.get().unwrap().lock().unwrap().join(","),
)
.expect("failed to write checked_features file");
// workaround needed to preven `STATUS_ENTRYPOINT_NOT_FOUND` error
// see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864
let target_os = std::env::var("CARGO_CFG_TARGET_OS");
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
&& Ok("windows") == target_os.as_deref()
&& Ok("msvc") == target_env.as_deref()
{
add_manifest();
}
}
// create aliases for the given module with its apis.
@ -152,14 +165,14 @@ fn main() {
//
// Note that both `module` and `apis` strings must be written in kebab case.
fn alias_module(module: &str, apis: &[&str], api_all: bool) {
let all_feature_name = format!("{}-all", module);
let all_feature_name = format!("{module}-all");
let all = has_feature(&all_feature_name) || api_all;
alias(&all_feature_name.to_snake_case(), all);
let mut any = all;
for api in apis {
let has = has_feature(&format!("{}-{}", module, api)) || all;
let has = has_feature(&format!("{module}-{api}")) || all;
alias(
&format!("{}_{}", AsSnakeCase(module), AsSnakeCase(api)),
has,
@ -169,3 +182,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 suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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'

View File

@ -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)
}
}
}
}

View File

@ -226,7 +226,7 @@ mod test {
fn check_test_dir() {
// create a callback closure that takes in a TempDir type and prints it.
let callback = |td: &tempfile::TempDir| {
println!("{:?}", td);
println!("{td:?}");
};
// execute the with_temp_dir function on the callback

View File

@ -150,7 +150,7 @@ impl<'a, R: std::fmt::Debug + Read + Seek> std::fmt::Debug for Extract<'a, R> {
impl<'a, R: Read + Seek> Extract<'a, R> {
/// Create archive from reader.
pub fn from_cursor(mut reader: R, archive_format: ArchiveFormat) -> Extract<'a, R> {
if reader.seek(io::SeekFrom::Start(0)).is_err() {
if reader.rewind().is_err() {
#[cfg(debug_assertions)]
eprintln!("Could not seek to start of the file");
}

View File

@ -441,8 +441,7 @@ impl<'de> Deserialize<'de> for HeaderMap {
headers.insert(key, value);
} else {
return Err(serde::de::Error::custom(format!(
"invalid header `{}` `{}`",
key, value
"invalid header `{key}` `{value}`"
)));
}
}

View File

@ -242,22 +242,22 @@ mod test {
}
let raw_str = "T".repeat(MIN_JSON_PARSE_LEN);
assert_eq!(serialize_js(&raw_str).unwrap(), format!("\"{}\"", raw_str));
assert_eq!(serialize_js(&raw_str).unwrap(), format!("\"{raw_str}\""));
assert_eq!(
serialize_js(&JsonObj {
value: raw_str.clone()
})
.unwrap(),
format!("JSON.parse('{{\"value\":\"{}\"}}')", raw_str)
format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')")
);
assert_eq!(
serialize_js(&JsonObj {
value: format!("\"{}\"", raw_str)
value: format!("\"{raw_str}\"")
})
.unwrap(),
format!("JSON.parse('{{\"value\":\"\\\\\"{}\\\\\"\"}}')", raw_str)
format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')")
);
let dangerous_json = RawValue::from_string(

View File

@ -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
@ -112,5 +112,5 @@ pub fn open<P: AsRef<str>>(
) -> crate::api::Result<()> {
scope
.open(path.as_ref(), with)
.map_err(|err| crate::api::Error::Shell(format!("failed to open: {}", err)))
.map_err(|err| crate::api::Error::Shell(format!("failed to open: {err}")))
}

View File

@ -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)]
@ -805,6 +805,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
@ -1008,6 +1037,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> {
@ -1036,6 +1068,7 @@ impl<R: Runtime> Builder<R> {
system_tray_event_listeners: Vec::new(),
#[cfg(updater)]
updater_settings: Default::default(),
device_event_filter: Default::default(),
}
}
@ -1257,8 +1290,7 @@ impl<R: Runtime> Builder<R> {
let type_name = std::any::type_name::<T>();
assert!(
self.state.set(state),
"state for type '{}' is already being managed",
type_name
"state for type '{type_name}' is already being managed"
);
self
}
@ -1486,6 +1518,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>> {
@ -1519,6 +1573,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();
}
@ -1531,13 +1588,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();

View File

@ -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)

View File

@ -238,8 +238,7 @@ pub(crate) fn handle<R: Runtime>(
if let Some(unknown_variant_name) = s.next() {
if unknown_variant_name == module {
return resolver.reject(format!(
"The `{}` module is not enabled. You must enable one of its APIs in the allowlist.",
module
"The `{module}` module is not enabled. You must enable one of its APIs in the allowlist."
));
} else if module == "Window" {
return resolver.reject(window::into_allowlist_error(unknown_variant_name).to_string());

View File

@ -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)
}
);
}

View File

@ -138,7 +138,7 @@ impl Cmd {
serde_json::to_string(&p)
.map_err(|e| {
#[cfg(debug_assertions)]
eprintln!("{}", e);
eprintln!("{e}");
e
})
.ok()

View File

@ -135,7 +135,7 @@ impl Cmd {
Ok(cmd) => cmd,
Err(e) => {
#[cfg(debug_assertions)]
eprintln!("{}", e);
eprintln!("{e}");
return Err(crate::Error::ProgramNotAllowed(PathBuf::from(program)).into_anyhow());
}
}
@ -154,7 +154,7 @@ impl Cmd {
if let Some(encoding) = crate::api::process::Encoding::for_label(encoding.as_bytes()) {
command = command.encoding(encoding);
} else {
return Err(anyhow::anyhow!(format!("unknown encoding {}", encoding)));
return Err(anyhow::anyhow!(format!("unknown encoding {encoding}")));
}
}
let (mut rx, child) = command.spawn()?;

View File

@ -63,10 +63,12 @@ pub enum WindowManagerCmd {
InnerSize,
OuterSize,
IsFullscreen,
IsMinimized,
IsMaximized,
IsDecorated,
IsResizable,
IsVisible,
Title,
CurrentMonitor,
PrimaryMonitor,
AvailableMonitors,
@ -101,6 +103,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)]
@ -162,6 +166,9 @@ pub fn into_allowlist_error(variant: &str) -> crate::Error {
"close" => crate::Error::ApiNotAllowlisted("window > close".to_string()),
"setDecorations" => crate::Error::ApiNotAllowlisted("window > setDecorations".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()),
@ -180,9 +187,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()),
}
}
@ -253,10 +261,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()),
@ -295,6 +305,10 @@ impl Cmd {
WindowManagerCmd::SetDecorations(decorations) => window.set_decorations(decorations)?,
#[cfg(window_set_always_on_top)]
WindowManagerCmd::SetAlwaysOnTop(always_on_top) => window.set_always_on_top(always_on_top)?,
#[cfg(window_set_content_protected)]
WindowManagerCmd::SetContentProtected(protected) => {
window.set_content_protected(protected)?
}
#[cfg(window_set_size)]
WindowManagerCmd::SetSize(size) => window.set_size(size)?,
#[cfg(window_set_min_size)]

View File

@ -232,7 +232,7 @@ mod test {
// dummy event handler function
fn event_fn(s: Event) {
println!("{:?}", s);
println!("{s:?}");
}
proptest! {
@ -304,18 +304,15 @@ pub fn unlisten_js(listeners_object_name: String, event_name: String, event_id:
format!(
"
(function () {{
const listeners = (window['{listeners}'] || {{}})['{event_name}']
const listeners = (window['{listeners_object_name}'] || {{}})['{event_name}']
if (listeners) {{
const index = window['{listeners}']['{event_name}'].findIndex(e => e.id === {event_id})
const index = window['{listeners_object_name}']['{event_name}'].findIndex(e => e.id === {event_id})
if (index > -1) {{
window['{listeners}']['{event_name}'].splice(index, 1)
window['{listeners_object_name}']['{event_name}'].splice(index, 1)
}}
}}
}})()
",
listeners = listeners_object_name,
event_name = event_name,
event_id = event_id,
)
}
@ -341,11 +338,7 @@ 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,
@ -353,7 +346,7 @@ pub fn listen_js(
event_id = event_id,
window_label = if let Some(l) = window_label {
crate::runtime::window::assert_label_is_valid(&l);
format!("'{}'", l)
format!("'{l}'")
} else {
"null".to_owned()
},

View File

@ -99,7 +99,7 @@ impl InvokeError {
/// Create an [`InvokeError`] as a string of the [`anyhow::Error`] message.
#[inline(always)]
pub fn from_anyhow(error: anyhow::Error) -> Self {
Self(JsonValue::String(format!("{:#}", error)))
Self(JsonValue::String(format!("{error:#}")))
}
}

View File

@ -134,6 +134,7 @@
//! - **window-close**: Enables the [`close` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#close).
//! - **window-set-decorations**: Enables the [`setDecorations` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setdecorations).
//! - **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).
@ -243,7 +244,7 @@ pub use {
dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Pixel, Position, Size},
CursorIcon, FileDropEvent,
},
RunIteration, UserAttentionType,
DeviceEventFilter, RunIteration, UserAttentionType,
},
self::state::{State, StateManager},
self::utils::{
@ -439,8 +440,7 @@ impl TryFrom<Icon> for runtime::Icon {
})
}
_ => panic!(
"image `{}` extension not supported; please file a Tauri feature request. `png` or `ico` icons are supported with the `icon-png` and `icon-ico` feature flags",
extension
"image `{extension}` extension not supported; please file a Tauri feature request. `png` or `ico` icons are supported with the `icon-png` and `icon-ico` feature flags"
),
}
}
@ -844,8 +844,8 @@ mod tests {
let lib_code = read_to_string(manifest_dir.join("src/lib.rs")).expect("failed to read lib.rs");
for f in get_manifest().features.keys() {
if !(f.starts_with("__") || f == "default" || lib_code.contains(&format!("*{}**", f))) {
panic!("Feature {} is not documented", f);
if !(f.starts_with("__") || f == "default" || lib_code.contains(&format!("*{f}**"))) {
panic!("Feature {f} is not documented");
}
}
}
@ -857,8 +857,7 @@ mod tests {
for checked_feature in checked_features {
if !manifest.features.iter().any(|(f, _)| f == checked_feature) {
panic!(
"Feature {} was checked in the alias build step but it does not exist in core/tauri/Cargo.toml",
checked_feature
"Feature {checked_feature} was checked in the alias build step but it does not exist in core/tauri/Cargo.toml"
);
}
}
@ -894,22 +893,21 @@ mod tests {
let module = module_all_feature.replace("-all", "");
assert!(
checked_features.contains(&module_all_feature.as_str()),
"`{}` is not aliased",
module
"`{module}` is not aliased"
);
let module_prefix = format!("{}-", module);
let module_prefix = format!("{module}-");
// we assume that module features are the ones that start with `<module>-`
// though it's not 100% accurate, we have an allowed list to fix it
let module_features = manifest
.features.keys()
.features
.keys()
.filter(|f| f.starts_with(&module_prefix));
for module_feature in module_features {
assert!(
allowed.contains(&module_feature.as_str())
|| checked_features.contains(&module_feature.as_str()),
"`{}` is not aliased",
module_feature
"`{module_feature}` is not aliased"
);
}
}
@ -937,7 +935,7 @@ mod test_utils {
fn check_spawn_task(task in "[a-z]+") {
// create dummy task function
let dummy_task = async move {
format!("{}-run-dummy-task", task);
format!("{task}-run-dummy-task");
};
// call spawn
crate::async_runtime::spawn(dummy_task);

View File

@ -184,7 +184,7 @@ fn replace_csp_nonce(
if !(nonces.is_empty() && hashes.is_empty()) {
let nonce_sources = nonces
.into_iter()
.map(|n| format!("'nonce-{}'", n))
.map(|n| format!("'nonce-{n}'"))
.collect::<Vec<String>>();
let sources = csp.entry(directive.into()).or_insert_with(Default::default);
let self_source = "'self'".to_string();
@ -487,7 +487,7 @@ impl<R: Runtime> WindowManager<R> {
window_url.scheme(),
window_url.host().unwrap(),
if let Some(port) = window_url.port() {
format!(":{}", port)
format!(":{port}")
} else {
"".into()
}
@ -714,7 +714,7 @@ impl<R: Runtime> WindowManager<R> {
{
let assets = assets.clone();
let schema_ = schema.clone();
let url_base = format!("{}://localhost", schema_);
let url_base = format!("{schema_}://localhost");
let aes_gcm_key = *crypto_keys.aes_gcm().raw();
pending.register_uri_scheme_protocol(schema, move |request| {
@ -813,7 +813,7 @@ impl<R: Runtime> WindowManager<R> {
let asset_response = assets
.get(&path.as_str().into())
.or_else(|| {
eprintln!("Asset `{}` not found; fallback to {}.html", path, path);
eprintln!("Asset `{path}` not found; fallback to {path}.html");
let fallback = format!("{}.html", path.as_str()).into();
let asset = assets.get(&fallback);
asset_path = fallback;
@ -1302,6 +1302,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()
}
@ -1408,8 +1416,7 @@ fn on_window_event<R: Runtime>(
let windows = windows_map.values();
for window in windows {
window.eval(&format!(
r#"(function () {{ const metadata = window.__TAURI_METADATA__; if (metadata != null) {{ metadata.__windows = window.__TAURI_METADATA__.__windows.filter(w => w.label !== "{}"); }} }})()"#,
label
r#"(function () {{ const metadata = window.__TAURI_METADATA__; if (metadata != null) {{ metadata.__windows = window.__TAURI_METADATA__.__windows.filter(w => w.label !== "{label}"); }} }})()"#
))?;
}
}

View File

@ -87,8 +87,8 @@ pub(crate) struct PatternJavascript {
#[allow(dead_code)]
pub(crate) fn format_real_schema(schema: &str) -> String {
if cfg!(windows) {
format!("https://{}.localhost", schema)
format!("https://{schema}.localhost")
} else {
format!("{}://localhost", schema)
format!("{schema}://localhost")
}
}

View File

@ -545,7 +545,7 @@ impl<R: Runtime> PluginStore<R> {
.values()
.filter_map(|p| p.initialization_script())
.fold(String::new(), |acc, script| {
format!("{}\n(function () {{ {} }})();", acc, script)
format!("{acc}\n(function () {{ {script} }})();")
})
}
@ -586,9 +586,7 @@ impl<R: Runtime> PluginStore<R> {
.unwrap_or_else(String::new);
plugin.extend_api(invoke);
} else {
invoke
.resolver
.reject(format!("plugin {} not found", target));
invoke.resolver.reject(format!("plugin {target} not found"));
}
}
}

View File

@ -141,7 +141,7 @@ impl Scope {
/// Extend the allowed patterns with the given directory.
///
/// After this function has been called, the frontend will be able to use the Tauri API to read
/// the directory and all of its files and subdirectories.
/// the directory and all of its files. If `recursive` is `true`, subdirectories will be accessible too.
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
let path = path.as_ref();
{
@ -216,13 +216,22 @@ impl Scope {
if let Ok(path) = path {
let path: PathBuf = path.components().collect();
let options = glob::MatchOptions {
// this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
// see: https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5
require_literal_separator: true,
// dotfiles are not supposed to be exposed by default
#[cfg(unix)]
require_literal_leading_dot: true,
..Default::default()
};
let forbidden = self
.forbidden_patterns
.lock()
.unwrap()
.iter()
.any(|p| p.matches_path(&path));
.any(|p| p.matches_path_with(&path, options));
if forbidden {
false
@ -232,7 +241,7 @@ impl Scope {
.lock()
.unwrap()
.iter()
.any(|p| p.matches_path(&path));
.any(|p| p.matches_path_with(&path, options));
allowed
}
} else {
@ -269,32 +278,97 @@ mod tests {
#[test]
fn path_is_escaped() {
let scope = new_scope();
scope.allow_directory("/home/tauri/**", false).unwrap();
assert!(scope.is_allowed("/home/tauri/**"));
assert!(scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/anyfile"));
#[cfg(unix)]
{
scope.allow_directory("/home/tauri/**", false).unwrap();
assert!(scope.is_allowed("/home/tauri/**"));
assert!(scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri\\**", false).unwrap();
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
scope.allow_file("/home/tauri/**").unwrap();
assert!(scope.is_allowed("/home/tauri/**"));
assert!(!scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/anyfile"));
#[cfg(unix)]
{
scope.allow_file("/home/tauri/**").unwrap();
assert!(scope.is_allowed("/home/tauri/**"));
assert!(!scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_file("C:\\home\\tauri\\**").unwrap();
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
scope.allow_directory("/home/tauri", true).unwrap();
scope.forbid_directory("/home/tauri/**", false).unwrap();
assert!(!scope.is_allowed("/home/tauri/**"));
assert!(!scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/**/inner/file"));
assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile"));
assert!(scope.is_allowed("/home/tauri/anyfile"));
#[cfg(unix)]
{
scope.allow_directory("/home/tauri", true).unwrap();
scope.forbid_directory("/home/tauri/**", false).unwrap();
assert!(!scope.is_allowed("/home/tauri/**"));
assert!(!scope.is_allowed("/home/tauri/**/file"));
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile"));
assert!(scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri", true).unwrap();
scope
.forbid_directory("C:\\home\\tauri\\**", false)
.unwrap();
assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\inner\\folder\\anyfile"));
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
scope.allow_directory("/home/tauri", true).unwrap();
scope.forbid_file("/home/tauri/**").unwrap();
assert!(!scope.is_allowed("/home/tauri/**"));
assert!(scope.is_allowed("/home/tauri/**/file"));
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
assert!(scope.is_allowed("/home/tauri/anyfile"));
#[cfg(unix)]
{
scope.allow_directory("/home/tauri", true).unwrap();
scope.forbid_file("/home/tauri/**").unwrap();
assert!(!scope.is_allowed("/home/tauri/**"));
assert!(scope.is_allowed("/home/tauri/**/file"));
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
assert!(scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri", true).unwrap();
scope.forbid_file("C:\\home\\tauri\\**").unwrap();
assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
let scope = new_scope();
#[cfg(unix)]
{
scope.allow_directory("/home/tauri", false).unwrap();
assert!(scope.is_allowed("/home/tauri/**"));
assert!(!scope.is_allowed("/home/tauri/**/file"));
assert!(!scope.is_allowed("/home/tauri/**/inner/file"));
assert!(scope.is_allowed("/home/tauri/anyfile"));
}
#[cfg(windows)]
{
scope.allow_directory("C:\\home\\tauri", false).unwrap();
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
}
}
}

View File

@ -21,7 +21,7 @@ impl Scope {
.iter()
.map(|url| {
glob::Pattern::new(url.as_str())
.unwrap_or_else(|_| panic!("scoped URL is not a valid glob pattern: `{}`", url))
.unwrap_or_else(|_| panic!("scoped URL is not a valid glob pattern: `{url}`"))
})
.collect(),
}

View File

@ -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

View File

@ -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::{
@ -250,6 +250,10 @@ impl WindowBuilder for MockWindowBuilder {
self
}
fn content_protected(self, protected: bool) -> Self {
self
}
fn icon(self, icon: Icon) -> Result<Self> {
Ok(self)
}
@ -329,6 +333,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)
}
@ -359,6 +367,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)
}
@ -375,6 +387,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)
}
@ -481,6 +497,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(())
}
@ -574,6 +594,10 @@ impl TrayHandle for MockTrayHandler {
Ok(())
}
fn set_tooltip(&self, tooltip: &str) -> Result<()> {
Ok(())
}
fn destroy(&self) -> Result<()> {
Ok(())
}
@ -696,6 +720,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,

View File

@ -96,7 +96,7 @@ impl<'de> Deserialize<'de> for RemoteRelease {
let pub_date = if let Some(date) = release.pub_date {
Some(
OffsetDateTime::parse(&date, &time::format_description::well_known::Rfc3339)
.map_err(|e| DeError::custom(format!("invalid value for `pub_date`: {}", e)))?,
.map_err(|e| DeError::custom(format!("invalid value for `pub_date`: {e}")))?,
)
} else {
None
@ -336,7 +336,7 @@ impl<R: Runtime> UpdateBuilder<R> {
(target.clone(), target)
} else {
let target = get_updater_target().ok_or(Error::UnsupportedOs)?;
(target.to_string(), format!("{}-{}", target, arch))
(target.to_string(), format!("{target}-{arch}"))
};
// Get the extract_path from the provided executable_path
@ -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")) {
@ -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(
@ -895,7 +905,7 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
})?;
let _ = std::process::Command::new("touch")
.arg(&extract_path)
.arg(extract_path)
.status();
Ok(())
@ -1049,14 +1059,13 @@ mod test {
format!(
r#"
{{
"name": "v{}",
"name": "v{version}",
"notes": "This is the latest version! Once updated you shouldn't see this prompt.",
"pub_date": "2020-06-25T14:14:19Z",
"signature": "{}",
"url": "{}"
"signature": "{public_signature}",
"url": "{download_url}"
}}
"#,
version, public_signature, download_url
"#
)
}
@ -1069,15 +1078,14 @@ mod test {
format!(
r#"
{{
"name": "v{}",
"name": "v{version}",
"notes": "This is the latest version! Once updated you shouldn't see this prompt.",
"pub_date": "2020-06-25T14:14:19Z",
"signature": "{}",
"url": "{}",
"with_elevated_task": {}
"signature": "{public_signature}",
"url": "{download_url}",
"with_elevated_task": {with_elevated_task}
}}
"#,
version, public_signature, download_url, with_elevated_task
"#
)
}
@ -1515,7 +1523,7 @@ mod test {
}"#;
fn missing_field_error(field: &str) -> String {
format!("the `{}` field was not set on the updater response", field)
format!("the `{field}` field was not set on the updater response")
}
let test_cases = [
@ -1547,7 +1555,7 @@ mod test {
.target("test-target")
.build());
if let Err(e) = check_update {
println!("ERROR: {}, expected: {}", e, error);
println!("ERROR: {e}, expected: {error}");
assert!(e.to_string().contains(&error));
} else {
panic!("unexpected Ok response");

View File

@ -109,7 +109,7 @@ pub const EVENT_STATUS_UPTODATE: &str = "UPTODATE";
/// Gets the target string used on the updater.
pub fn target() -> Option<String> {
if let (Some(target), Some(arch)) = (core::get_updater_target(), core::get_updater_arch()) {
Some(format!("{}-{}", target, arch))
Some(format!("{target}-{arch}"))
} else {
None
}
@ -565,7 +565,7 @@ async fn prompt_for_install<R: Runtime>(
// something more conventional.
let should_install = ask(
parent_window,
format!(r#"A new version of {} is available! "#, app_name),
format!(r#"A new version of {app_name} is available! "#),
format!(
r#"{} {} is now available -- you have {}.

View File

@ -7,6 +7,7 @@
pub(crate) mod menu;
pub use menu::{MenuEvent, MenuHandle};
use url::Url;
#[cfg(target_os = "macos")]
use crate::TitleBarStyle;
@ -48,6 +49,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 {
@ -108,6 +110,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> {
@ -181,6 +184,7 @@ 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,
}
}
@ -229,6 +233,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();
@ -238,12 +269,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),
@ -251,6 +283,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,
@ -398,6 +435,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()?)?;
@ -526,6 +570,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 {
@ -888,6 +948,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)
@ -908,6 +973,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.
@ -1115,6 +1185,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
@ -1256,6 +1335,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();

View File

@ -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();
@ -15,19 +22,19 @@ fn main() {
match handle.updater().check().await {
Ok(update) => {
if let Err(e) = update.download_and_install().await {
println!("{}", e);
println!("{e}");
std::process::exit(1);
}
std::process::exit(0);
}
Err(e) => {
println!("{}", e);
println!("{e}");
std::process::exit(1);
}
}
});
Ok(())
})
.run(tauri::generate_context!())
.run(context)
.expect("error while running tauri application");
}

View File

@ -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,30 +95,83 @@ 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_{}_amd64.AppImage",
version
))
fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> {
vec![(
BundleTarget::AppImage,
root_dir.join(format!(
"target/debug/bundle/appimage/app-updater_{}_amd64.AppImage",
version
)),
)]
}
#[cfg(target_os = "macos")]
fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf {
root_dir.join(format!("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.app"))
fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> {
vec![(
BundleTarget::App,
root_dir.join(format!("target/debug/bundle/ios/app-updater.app")),
)]
}
#[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_{}_x64-setup.exe",
version
)),
),
(
BundleTarget::Msi,
root_dir.join(format!(
"target/debug/bundle/msi/app-updater_{}_x64_en-US.msi",
version
)),
),
]
}
#[test]
@ -140,99 +203,118 @@ fn update_app() {
};
// bundle app update
build_app(&cli_bin_path, &manifest_dir, &config, true);
build_app(
&cli_bin_path,
&manifest_dir,
&config,
true,
Default::default(),
);
let updater_ext = if cfg!(windows) { "zip" } else { "tar.gz" };
let updater_zip_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");
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!("{}.{}.sig", bundle_updater_ext, updater_zip_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!("{}.{}", 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");
std::thread::spawn(move || {
// start the updater server
let server = tiny_http::Server::http("localhost:3007").expect("failed to start updater server");
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();
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);
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;
}
_ => (),
}
"/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())
}),
));
}
_ => (),
}
}
});
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");
if !status.success() {
panic!("failed to run app");
}
});
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"))
};
let status = binary_cmd.status().expect("failed to run app");
if !status.success() {
panic!("failed to run app");
}
}

View File

@ -24,7 +24,7 @@ fn main() {
env.args.clear();
tauri::api::process::restart(&env)
}
Some(invalid) => panic!("only argument `restart` is allowed, {} is invalid", invalid),
Some(invalid) => panic!("only argument `restart` is allowed, {invalid} is invalid"),
None => {}
};
}

View File

@ -32,7 +32,7 @@ fn symlink_runner(create_symlinks: impl Fn(&Path) -> io::Result<Symlink>) -> Res
if cfg!(windows) {
compiled_binary.set_extension("exe");
}
println!("{:?}", compiled_binary);
println!("{compiled_binary:?}");
// set up all the temporary file paths
let temp = tempfile::TempDir::new()?;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,12 +8,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "aead"
version = "0.4.3"
@ -188,6 +182,12 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -635,16 +635,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "deflate"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4"
dependencies = [
"adler32",
"byteorder",
]
[[package]]
name = "derive_more"
version = "0.99.17"
@ -1348,12 +1338,12 @@ dependencies = [
[[package]]
name = "ico"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a4b3331534254a9b64095ae60d3dc2a8225a7a70229cd5888be127cdc1f6804"
checksum = "031530fe562d8c8d71c0635013d6d155bbfe8ba0aa4b4d2d24ce8af6b71047bd"
dependencies = [
"byteorder",
"png 0.11.0",
"png",
]
[[package]]
@ -1431,15 +1421,6 @@ dependencies = [
"cfb 0.7.3",
]
[[package]]
name = "inflate"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5f9f47468e9a76a6452271efadc88fe865a82be91fe75e6c0c57b87ccea59d4"
dependencies = [
"adler32",
]
[[package]]
name = "instant"
version = "0.1.12"
@ -1541,7 +1522,19 @@ checksum = "f995a3c8f2bc3dd52a18a583e90f9ec109c047fa1603a853e46bcda14d2e279d"
dependencies = [
"serde",
"serde_json",
"treediff",
"treediff 3.0.2",
]
[[package]]
name = "json-patch"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e712e62827c382a77b87f590532febb1f8b2fdbc3eefa1ee37fe7281687075ef"
dependencies = [
"serde",
"serde_json",
"thiserror",
"treediff 4.0.2",
]
[[package]]
@ -1891,17 +1884,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.1"
@ -2290,7 +2272,7 @@ version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225"
dependencies = [
"base64",
"base64 0.13.1",
"indexmap",
"line-wrap",
"serde",
@ -2298,18 +2280,6 @@ dependencies = [
"xml-rs",
]
[[package]]
name = "png"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0b0cabbbd20c2d7f06dbf015e06aad59b6ca3d9ed14848783e98af9aaf19925"
dependencies = [
"bitflags",
"deflate",
"inflate",
"num-iter",
]
[[package]]
name = "png"
version = "0.17.7"
@ -2565,7 +2535,7 @@ version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
dependencies = [
"base64",
"base64 0.13.1",
"bytes",
"encoding_rs",
"futures-core",
@ -2761,18 +2731,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.147"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.147"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [
"proc-macro2",
"quote",
@ -2781,9 +2751,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.87"
version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
dependencies = [
"itoa 1.0.4",
"ryu",
@ -3032,9 +3002,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
version = "1.0.103"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
dependencies = [
"proc-macro2",
"quote",
@ -3069,9 +3039,8 @@ dependencies = [
[[package]]
name = "tao"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2845fd58915455c5faf2c9ac5d8a5ed43bd23ab57f0a67d63612936209eae74"
version = "0.15.8"
source = "git+https://github.com/tauri-apps/tao?branch=dev#b3aa3982d18a1ca2c8f08a135d7256b1aca46369"
dependencies = [
"bitflags",
"cairo-rs",
@ -3103,11 +3072,11 @@ dependencies = [
"objc",
"once_cell",
"parking_lot",
"paste",
"png 0.17.7",
"png",
"raw-window-handle",
"scopeguard",
"serde",
"tao-macros",
"unicode-segmentation",
"uuid 1.2.1",
"windows 0.39.0",
@ -3115,6 +3084,16 @@ dependencies = [
"x11-dl",
]
[[package]]
name = "tao-macros"
version = "0.0.0"
source = "git+https://github.com/tauri-apps/tao?branch=dev#b3aa3982d18a1ca2c8f08a135d7256b1aca46369"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tar"
version = "0.4.38"
@ -3128,11 +3107,11 @@ dependencies = [
[[package]]
name = "tauri"
version = "1.2.0"
version = "1.2.3"
dependencies = [
"anyhow",
"attohttpc",
"base64",
"base64 0.13.1",
"bytes",
"clap",
"cocoa",
@ -3157,7 +3136,7 @@ dependencies = [
"os_info",
"os_pipe",
"percent-encoding",
"png 0.17.7",
"png",
"rand 0.8.5",
"raw-window-handle",
"regex",
@ -3190,12 +3169,12 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "1.2.0"
version = "1.2.1"
dependencies = [
"anyhow",
"cargo_toml",
"heck 0.4.0",
"json-patch",
"json-patch 0.3.0",
"quote",
"semver 1.0.14",
"serde_json",
@ -3206,14 +3185,14 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "1.2.0"
version = "1.2.1"
dependencies = [
"base64",
"base64 0.20.0",
"brotli",
"ico",
"json-patch",
"json-patch 0.3.0",
"plist",
"png 0.17.7",
"png",
"proc-macro2",
"quote",
"regex",
@ -3230,7 +3209,7 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "1.2.0"
version = "1.2.1"
dependencies = [
"heck 0.4.0",
"proc-macro2",
@ -3242,7 +3221,7 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "0.12.0"
version = "0.12.1"
dependencies = [
"gtk",
"http",
@ -3253,6 +3232,7 @@ dependencies = [
"serde_json",
"tauri-utils",
"thiserror",
"url",
"uuid 1.2.1",
"webview2-com",
"windows 0.39.0",
@ -3260,7 +3240,7 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "0.12.0"
version = "0.12.2"
dependencies = [
"cocoa",
"gtk",
@ -3278,7 +3258,7 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "1.2.0"
version = "1.2.1"
dependencies = [
"aes-gcm",
"brotli",
@ -3288,7 +3268,7 @@ dependencies = [
"heck 0.4.0",
"html5ever",
"infer 0.7.0",
"json-patch",
"json-patch 0.2.6",
"kuchiki",
"memchr",
"phf 0.10.1",
@ -3573,6 +3553,15 @@ dependencies = [
"serde_json",
]
[[package]]
name = "treediff"
version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303"
dependencies = [
"serde_json",
]
[[package]]
name = "try-lock"
version = "0.2.3"
@ -4241,11 +4230,10 @@ dependencies = [
[[package]]
name = "wry"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923d297b203eae65b095af16c02978b7932be1968012b4da7138390edf34dea5"
version = "0.23.4"
source = "git+https://github.com/tauri-apps/wry?branch=dev#fca42a0730e75a142f7f354c6ac3f6d6a0f4711f"
dependencies = [
"base64",
"base64 0.13.1",
"block",
"cocoa",
"core-graphics",

Some files were not shown because too many files have changed in this diff Show More