diff --git a/.changes/add-command.md b/.changes/add-command.md new file mode 100644 index 00000000000..7c8638185b4 --- /dev/null +++ b/.changes/add-command.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:feat' +"@tauri-apps/cli": 'patch:feat' +--- + +Added `tauri plugin add` command to add a plugin to the Tauri project. diff --git a/.changes/add-mobile-to-plugin.md b/.changes/add-mobile-to-plugin.md new file mode 100644 index 00000000000..e3c15a50df5 --- /dev/null +++ b/.changes/add-mobile-to-plugin.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'minor:feat' +"@tauri-apps/cli": 'minor:feat' +--- + +Added `plugin android add` and `plugin ios add` commands to add mobile plugin functionality to existing projects. diff --git a/.changes/add-visible-on-all-workspaces.md b/.changes/add-visible-on-all-workspaces.md new file mode 100644 index 00000000000..66cfaeccfc2 --- /dev/null +++ b/.changes/add-visible-on-all-workspaces.md @@ -0,0 +1,7 @@ +--- +"tauri": 'minor:feat' +"tauri-runtime": 'minor' +"tauri-utils": 'minor:feat' +--- + +Added `visible_on_all_workspaces` configuration option to `WindowBuilder`, `Window`, and `WindowConfig`. diff --git a/.changes/android-apis-runtime.md b/.changes/android-apis-runtime.md new file mode 100644 index 00000000000..fe14fb1433a --- /dev/null +++ b/.changes/android-apis-runtime.md @@ -0,0 +1,6 @@ +--- +"tauri-runtime": 'minor:feat' +"tauri-runtime-wry": 'minor:feat' +--- + +Add `find_class`, `run_on_android_context` on `RuntimeHandle`. diff --git a/.changes/android-buildsrc-gitignore.md b/.changes/android-buildsrc-gitignore.md new file mode 100644 index 00000000000..9f0ee5f4c62 --- /dev/null +++ b/.changes/android-buildsrc-gitignore.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Do not gitignore the Android project's `buildSrc` folder by default since we removed absolute paths from it. diff --git a/.changes/android-enhance-method-parse.md b/.changes/android-enhance-method-parse.md new file mode 100644 index 00000000000..165f6868d5f --- /dev/null +++ b/.changes/android-enhance-method-parse.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Enhance parsing of annotated Android plugin methods to support private functions. diff --git a/.changes/android-load-config.md b/.changes/android-load-config.md new file mode 100644 index 00000000000..02132202963 --- /dev/null +++ b/.changes/android-load-config.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Added static function `loadConfig` on the Android `PluginManager` class. diff --git a/.changes/android-on-new-intent.md b/.changes/android-on-new-intent.md new file mode 100644 index 00000000000..4164d23f798 --- /dev/null +++ b/.changes/android-on-new-intent.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Listen to `onNewIntent` and forward it to registered plugins. diff --git a/.changes/android-plugin-command-exception.md b/.changes/android-plugin-command-exception.md new file mode 100644 index 00000000000..40ccb8135aa --- /dev/null +++ b/.changes/android-plugin-command-exception.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Adjust Android plugin exception error. diff --git a/.changes/api-ipc-refactor.md b/.changes/api-ipc-refactor.md new file mode 100644 index 00000000000..9b02a48cd4e --- /dev/null +++ b/.changes/api-ipc-refactor.md @@ -0,0 +1,5 @@ +--- +"@tauri-apps/api": patch:feat +--- + +Change the IPC call to align with the new format for the custom protocol based API. diff --git a/.changes/app-builder-send.md b/.changes/app-builder-send.md new file mode 100644 index 00000000000..479dbad7d5a --- /dev/null +++ b/.changes/app-builder-send.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:enhance +--- + +Ensure Builder is Send by requiring the menu closure to be Send. diff --git a/.changes/build-android-env-vars.md b/.changes/build-android-env-vars.md new file mode 100644 index 00000000000..a5402774ae5 --- /dev/null +++ b/.changes/build-android-env-vars.md @@ -0,0 +1,5 @@ +--- +"tauri-build": 'patch:enhance' +--- + +Set environment variables used by `tauri::mobile_entry_point`. diff --git a/.changes/bump-1.3.md b/.changes/bump-1.3.md new file mode 100644 index 00000000000..47dabadd5b5 --- /dev/null +++ b/.changes/bump-1.3.md @@ -0,0 +1,10 @@ +--- +"tauri-bundler": 'patch:enhance' +"tauri-codegen": 'patch:enhance' +"tauri-macros": 'patch:enhance' +"tauri-utils": 'patch:enhance' +"tauri-runtime": 'patch:enhance' +"tauri-runtime-wry": 'patch:enhance' +--- + +Pull changes from Tauri 1.3 release. diff --git a/.changes/bundler-remove-dialog-option.md b/.changes/bundler-remove-dialog-option.md new file mode 100644 index 00000000000..9ee8f8e02ba --- /dev/null +++ b/.changes/bundler-remove-dialog-option.md @@ -0,0 +1,5 @@ +--- +"tauri-bundler": 'patch:enhance' +--- + +Removed the `UpdaterSettings::dialog` field. diff --git a/.changes/bundler-zip-deflate.md b/.changes/bundler-zip-deflate.md new file mode 100644 index 00000000000..a6cf01db840 --- /dev/null +++ b/.changes/bundler-zip-deflate.md @@ -0,0 +1,5 @@ +--- +'tauri-bundler': 'patch:bug' +--- + +Enable `zip`'s `deflate` feature flag to fix issues when downloading nsis and wix tools. diff --git a/.changes/channel-api.md b/.changes/channel-api.md new file mode 100644 index 00000000000..716eca43ad3 --- /dev/null +++ b/.changes/channel-api.md @@ -0,0 +1,6 @@ +--- +"api": 'patch:enhance' +"tauri": 'patch:enhance' +--- + +Add channel API for sending data across the IPC. diff --git a/.changes/channel-rust.md b/.changes/channel-rust.md new file mode 100644 index 00000000000..d951507b266 --- /dev/null +++ b/.changes/channel-rust.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:enhance +--- + +Added `Channel::new` allowing communication from a mobile plugin with Rust. diff --git a/.changes/cli-android-build.md b/.changes/cli-android-build.md new file mode 100644 index 00000000000..708a482db8d --- /dev/null +++ b/.changes/cli-android-build.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'minor:feat' +"@tauri-apps/cli": 'minor:feat' +--- + +Added `android build` command. diff --git a/.changes/cli-android-dev-release.md b/.changes/cli-android-dev-release.md new file mode 100644 index 00000000000..638c0130503 --- /dev/null +++ b/.changes/cli-android-dev-release.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'patch:enhance' +'@tauri-apps/cli': 'patch:enhance' +--- + +Add `--release` flag for `tauri android dev` however you will need to sign your Android app, see https://next--tauri.netlify.app/next/guides/distribution/sign-android diff --git a/.changes/cli-android-specified-targets-only.md b/.changes/cli-android-specified-targets-only.md new file mode 100644 index 00000000000..3eace2f7e9d --- /dev/null +++ b/.changes/cli-android-specified-targets-only.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'patch:enhance' +'@tauri-apps/cli': 'patch:enhance' +--- + +Build only specified rust targets for `tauri android build` instead of all. diff --git a/.changes/cli-android-split-per-abit-target.md b/.changes/cli-android-split-per-abit-target.md new file mode 100644 index 00000000000..00330291d66 --- /dev/null +++ b/.changes/cli-android-split-per-abit-target.md @@ -0,0 +1,5 @@ +--- +'tauri-cli': 'patch:bug' +--- + +Fix `--split-per-abi` not building any targets unless specified by `--target` flag. diff --git a/.changes/cli-apple-dev-team.md b/.changes/cli-apple-dev-team.md new file mode 100644 index 00000000000..b7d63ab27f9 --- /dev/null +++ b/.changes/cli-apple-dev-team.md @@ -0,0 +1,5 @@ +--- +'tauri-cli': 'patch:bug' +--- + +Fix `tauri info` failing when there is no available iOS code signing certificate. diff --git a/.changes/cli-built-in-dev-server-mobile.md b/.changes/cli-built-in-dev-server-mobile.md new file mode 100644 index 00000000000..5ad54dfa0a7 --- /dev/null +++ b/.changes/cli-built-in-dev-server-mobile.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'patch:enhance' +'@tauri-apps/cli': 'patch:enhance' +--- + +Use local ip address for built-in dev server on mobile. diff --git a/.changes/cli-expose-plugin-config.md b/.changes/cli-expose-plugin-config.md new file mode 100644 index 00000000000..2cb4b683cfe --- /dev/null +++ b/.changes/cli-expose-plugin-config.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:feat +"@tauri-apps/cli": patch:feat +--- + +Expose an environment variable `TAURI_${PLUGIN_NAME}_PLUGIN_CONFIG` for each defined plugin configuration object. diff --git a/.changes/cli-ios-build.md b/.changes/cli-ios-build.md new file mode 100644 index 00000000000..bb0a39b951d --- /dev/null +++ b/.changes/cli-ios-build.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Added `ios build` command. diff --git a/.changes/cli-ios-metadata-env-var.md b/.changes/cli-ios-metadata-env-var.md new file mode 100644 index 00000000000..9399155b05f --- /dev/null +++ b/.changes/cli-ios-metadata-env-var.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:feat +"@tauri-apps/cli": patch:feat +--- + +Expose the `TAURI_IOS_PROJECT_PATH` and `TAURI_IOS_APP_NAME` environment variables when using `ios` commands. diff --git a/.changes/cli-key-properties.md b/.changes/cli-key-properties.md new file mode 100644 index 00000000000..1ccb6623e53 --- /dev/null +++ b/.changes/cli-key-properties.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Add `key.properties` file to android's `.gitignore`. diff --git a/.changes/cli-libname-dashes.md b/.changes/cli-libname-dashes.md new file mode 100644 index 00000000000..c341ca92c1f --- /dev/null +++ b/.changes/cli-libname-dashes.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'patch:enhance' +'@tauri-apps/cli': 'patch:enhance' +--- + +On mobile, fix regression introduced in `tauri-cli` version `2.0.0-alpha.3` where library not found error was thrown. diff --git a/.changes/cli-library-compilation.md b/.changes/cli-library-compilation.md new file mode 100644 index 00000000000..c26ac0db148 --- /dev/null +++ b/.changes/cli-library-compilation.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Don't build library files when building desktop targets. diff --git a/.changes/cli-mobile-auto-ip.md b/.changes/cli-mobile-auto-ip.md new file mode 100644 index 00000000000..6a21831f7a4 --- /dev/null +++ b/.changes/cli-mobile-auto-ip.md @@ -0,0 +1,5 @@ +--- +'tauri-cli': 'patch:enhance' +--- + +Auto select an external IP for mobile development and fallback to prompting the user. Use `--force-ip-prompt` to force prompting. diff --git a/.changes/cli-mobile-cwd-config.md b/.changes/cli-mobile-cwd-config.md new file mode 100644 index 00000000000..dc6ab98e57d --- /dev/null +++ b/.changes/cli-mobile-cwd-config.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'patch:bug' +'@tauri-apps/cli': 'patch:bug' +--- + +Set current directory to tauri directory before reading config file. diff --git a/.changes/cli-mobile-dev.md b/.changes/cli-mobile-dev.md new file mode 100644 index 00000000000..395680aad11 --- /dev/null +++ b/.changes/cli-mobile-dev.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'minor:feat' +"@tauri-apps/cli": 'minor:feat' +--- + +Added `android dev` and `ios dev` commands. diff --git a/.changes/cli-mobile-plugin.md b/.changes/cli-mobile-plugin.md new file mode 100644 index 00000000000..31e8538dccc --- /dev/null +++ b/.changes/cli-mobile-plugin.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'minor:feat' +"@tauri-apps/cli": 'minor:feat' +--- + +Add commands to add native Android and iOS functionality to plugins. diff --git a/.changes/cli-nodejs-detection.md b/.changes/cli-nodejs-detection.md new file mode 100644 index 00000000000..1bd8ddc60f9 --- /dev/null +++ b/.changes/cli-nodejs-detection.md @@ -0,0 +1,5 @@ +--- +'tauri-cli': 'patch:enhance' +--- + +In mobile commands, correctly detect when nodejs binary has the version in its name, for example `node-18` diff --git a/.changes/cli-npx-mobile.md b/.changes/cli-npx-mobile.md new file mode 100644 index 00000000000..973f4fb88fe --- /dev/null +++ b/.changes/cli-npx-mobile.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'patch:bug' +'@tauri-apps/cli': 'patch:bug' +--- + +Fix `tauri (android|ios) (dev|build)` failing when using `npx tauri` diff --git a/.changes/cli-pnpm.md b/.changes/cli-pnpm.md new file mode 100644 index 00000000000..c22fa2baad4 --- /dev/null +++ b/.changes/cli-pnpm.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'patch:enhance' +'@tauri-apps/cli': 'patch:enhance' +--- + +Fix android project build crashing when using `pnpm` caused by extra `--`. diff --git a/.changes/cli-refactor-ipc-mobile.md b/.changes/cli-refactor-ipc-mobile.md new file mode 100644 index 00000000000..2494371aa8e --- /dev/null +++ b/.changes/cli-refactor-ipc-mobile.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Use temp file instead of environment variable to pass CLI IPC websocket address to the IDE. diff --git a/.changes/cli-skip-targets-install.md b/.changes/cli-skip-targets-install.md new file mode 100644 index 00000000000..e5825cb0c7c --- /dev/null +++ b/.changes/cli-skip-targets-install.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'patch:enhance' +'@tauri-apps/cli': 'patch:enhance' +--- + +Skip Rust target installation if they are already installed. diff --git a/.changes/cli-wry-0-28.md b/.changes/cli-wry-0-28.md new file mode 100644 index 00000000000..b6a0210f352 --- /dev/null +++ b/.changes/cli-wry-0-28.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'patch:enhance' +'@tauri-apps/cli': 'patch:enhance' +--- + +Update mobile template to `wry@0.28` diff --git a/.changes/codegen-mobile-devurl.md b/.changes/codegen-mobile-devurl.md new file mode 100644 index 00000000000..ba385474425 --- /dev/null +++ b/.changes/codegen-mobile-devurl.md @@ -0,0 +1,5 @@ +--- +"tauri-codegen": 'patch:enhance' +--- + +Change `devPath` URL to use the local IP address on iOS and Android. diff --git a/.changes/config-incognito.md b/.changes/config-incognito.md new file mode 100644 index 00000000000..1ac657deb1a --- /dev/null +++ b/.changes/config-incognito.md @@ -0,0 +1,5 @@ +--- +'tauri': 'patch:feat' +--- + +Add `incognito` option to the window configuration object. diff --git a/.changes/config-tray-icon-tooltip.md b/.changes/config-tray-icon-tooltip.md new file mode 100644 index 00000000000..b02a90d6eed --- /dev/null +++ b/.changes/config-tray-icon-tooltip.md @@ -0,0 +1,5 @@ +--- +'tauri-utils': 'minor:feat' +--- + +Add option to specify a tooltip text for the tray icon in the config. diff --git a/.changes/config-tray-icon.md b/.changes/config-tray-icon.md new file mode 100644 index 00000000000..86e36cbefd7 --- /dev/null +++ b/.changes/config-tray-icon.md @@ -0,0 +1,5 @@ +--- +'tauri-utils': 'major:breaking' +--- + +`systemTray` config option has been renamed to `trayIcon`. diff --git a/.changes/config.json b/.changes/config.json index 78a03db4c17..7fb05912edf 100644 --- a/.changes/config.json +++ b/.changes/config.json @@ -8,17 +8,17 @@ "pref": "Performance Improvements", "changes": "What's Changed", "sec": "Security fixes", - "deps": "Dependencies" + "deps": "Dependencies", + "breaking": "Breaking Changes" }, "defaultChangeTag": "changes", "pkgManagers": { "rust": { - "errorOnVersionRange": "^2.0.0-0", "version": true, "getPublishedVersion": "node ../../.scripts/covector/package-latest-version.js cargo ${ pkgFile.pkg.package.name } ${ pkgFile.pkg.package.version }", "prepublish": [ "sudo apt-get update", - "sudo apt-get install -y webkit2gtk-4.0 libayatana-appindicator3-dev", + "sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev", "cargo install cargo-audit --features=fix", { "command": "cargo generate-lockfile", @@ -78,7 +78,6 @@ ] }, "javascript": { - "errorOnVersionRange": "^2.0.0-0", "version": true, "getPublishedVersion": "node ../../.scripts/covector/package-latest-version.js npm ${ pkgFile.pkg.name } ${ pkgFile.pkg.version }", "prepublish": [ @@ -115,7 +114,7 @@ "pipe": true }, { - "command": "yarn publish --access public --loglevel silly", + "command": "yarn publish --access public --loglevel silly --tag next", "dryRunCommand": "npm publish --dry-run --access public", "pipe": true }, @@ -237,7 +236,8 @@ "tauri-macros", "tauri-utils", "tauri-runtime", - "tauri-runtime-wry" + "tauri-runtime-wry", + "tauri-build" ], "postversion": "node ../../.scripts/covector/sync-cli-metadata.js ${ pkg.pkg } ${ release.type }" }, diff --git a/.changes/core-android-proxy-method.md b/.changes/core-android-proxy-method.md new file mode 100644 index 00000000000..8d81a2dfd87 --- /dev/null +++ b/.changes/core-android-proxy-method.md @@ -0,0 +1,5 @@ +--- +'tauri': 'patch:enhance' +--- + +Use correct HTTP method when making requests to the proxied server on mobile. diff --git a/.changes/core-app-montior.md b/.changes/core-app-montior.md new file mode 100644 index 00000000000..acd09e301be --- /dev/null +++ b/.changes/core-app-montior.md @@ -0,0 +1,5 @@ +--- +'tauri': 'minor:feat' +--- + +Add `App::primary_monitor`, `App::available_monitors`, `AppHandle::primary_monitor`, and `AppHandle::available_monitors` diff --git a/.changes/core-channel-clone.md b/.changes/core-channel-clone.md new file mode 100644 index 00000000000..d4446fc6079 --- /dev/null +++ b/.changes/core-channel-clone.md @@ -0,0 +1,5 @@ +--- +'tauri': 'patch:enhance' +--- + +Implement `Clone` for `Channel` diff --git a/.changes/core-incognito.md b/.changes/core-incognito.md new file mode 100644 index 00000000000..ebfdeb5314f --- /dev/null +++ b/.changes/core-incognito.md @@ -0,0 +1,5 @@ +--- +'tauri': 'patch:feat' +--- + +Add `WindowBuilder::incognito` diff --git a/.changes/core-navigate-method.md b/.changes/core-navigate-method.md new file mode 100644 index 00000000000..4767c1a1bf3 --- /dev/null +++ b/.changes/core-navigate-method.md @@ -0,0 +1,5 @@ +--- +"tauri": 'minor:feat' +--- + +Added `Window::navigate`. diff --git a/.changes/core-remove-file-dir-semver-apis.md b/.changes/core-remove-file-dir-semver-apis.md new file mode 100644 index 00000000000..a94a03816a0 --- /dev/null +++ b/.changes/core-remove-file-dir-semver-apis.md @@ -0,0 +1,6 @@ +--- +'tauri': 'patch:breaking' +--- + +- Removed `tauri::api::file` and `tauri::api::dir` modules, use `std::fs` instead. +- Removed `tauri::api::version` module, use `semver` crate instead. diff --git a/.changes/core-wry-0-28.md b/.changes/core-wry-0-28.md new file mode 100644 index 00000000000..00542fa265b --- /dev/null +++ b/.changes/core-wry-0-28.md @@ -0,0 +1,5 @@ +--- +'tauri': 'patch:enhance' +--- + +On Android, update proguard rules. diff --git a/.changes/dark-light-mica-effect.md b/.changes/dark-light-mica-effect.md new file mode 100644 index 00000000000..d2ce4762b6f --- /dev/null +++ b/.changes/dark-light-mica-effect.md @@ -0,0 +1,5 @@ +--- +'tauri-utils': 'patch:feat' +--- + +Add `WindowEffect::MicaDark` and `WindowEffect::MicaLight` diff --git a/.changes/default-tls-features.md b/.changes/default-tls-features.md new file mode 100644 index 00000000000..5ad707dfe97 --- /dev/null +++ b/.changes/default-tls-features.md @@ -0,0 +1,5 @@ +--- +"tauri": major:feat +--- + +Added the `default-tls` and `reqwest-default-tls` Cargo features for enabling TLS suppport to connect over HTTPS. diff --git a/.changes/default-window-icon.md b/.changes/default-window-icon.md new file mode 100644 index 00000000000..b1d13172ecd --- /dev/null +++ b/.changes/default-window-icon.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Add `default_window_icon` getter on `App` and `AppHandle`. diff --git a/.changes/dev-proxy-response-cache.md b/.changes/dev-proxy-response-cache.md new file mode 100644 index 00000000000..53f6623162d --- /dev/null +++ b/.changes/dev-proxy-response-cache.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Implement response cache on the dev server proxy, used when the server responds with status 304. diff --git a/.changes/dev-proxy.md b/.changes/dev-proxy.md new file mode 100644 index 00000000000..86d86fea489 --- /dev/null +++ b/.changes/dev-proxy.md @@ -0,0 +1,5 @@ +--- +"tauri": major:feat +--- + +**Breaking change:** Use the custom protocol as a proxy to the development server on all platforms except Linux. diff --git a/.changes/dnd-position.md b/.changes/dnd-position.md new file mode 100644 index 00000000000..cc35c0c4ec0 --- /dev/null +++ b/.changes/dnd-position.md @@ -0,0 +1,7 @@ +--- +'tauri': 'minor:feat' +'tauri-runtime': 'minor' +'tauri-runtime-wry': 'minor' +--- + +Changed `FileDropEvent` to include drop and hover position. diff --git a/.changes/downgrade-min-sdk-version.md b/.changes/downgrade-min-sdk-version.md new file mode 100644 index 00000000000..98b033b1fe7 --- /dev/null +++ b/.changes/downgrade-min-sdk-version.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"tauri": 'patch:enhance' +--- + +Change minimum Android SDK version to 21 for the plugin library. diff --git a/.changes/dynamic-wry-plugin.md b/.changes/dynamic-wry-plugin.md new file mode 100644 index 00000000000..cdcf95ed4b8 --- /dev/null +++ b/.changes/dynamic-wry-plugin.md @@ -0,0 +1,6 @@ +--- +"tauri-runtime-wry": 'patch:enhance' +"tauri": 'patch:enhance' +--- + +Allow a wry plugin to be registered at runtime. diff --git a/.changes/enable-minify.md b/.changes/enable-minify.md new file mode 100644 index 00000000000..0535a886aa5 --- /dev/null +++ b/.changes/enable-minify.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Change the Android template to enable minification on release and pull ProGuard rules from proguard-tauri.pro. diff --git a/.changes/enable-path-commands.md b/.changes/enable-path-commands.md new file mode 100644 index 00000000000..522441c61e4 --- /dev/null +++ b/.changes/enable-path-commands.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Fixes path commands not being added. diff --git a/.changes/enhance-jsobject-return-types.md b/.changes/enhance-jsobject-return-types.md new file mode 100644 index 00000000000..d70bdbdb7f7 --- /dev/null +++ b/.changes/enhance-jsobject-return-types.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Enhance Android's `JSObject` return types. diff --git a/.changes/error-on-identifier-change.md b/.changes/error-on-identifier-change.md new file mode 100644 index 00000000000..9229a370b4b --- /dev/null +++ b/.changes/error-on-identifier-change.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Print an error if the Android project was generated with an older bundle identifier or package name. diff --git a/.changes/event-api-window-label.md b/.changes/event-api-window-label.md new file mode 100644 index 00000000000..d84aa7b3e93 --- /dev/null +++ b/.changes/event-api-window-label.md @@ -0,0 +1,5 @@ +--- +"@tauri-apps/api": 'patch:enhance' +--- + +Expose the window target option on event APIs. diff --git a/.changes/file-associations-config.md b/.changes/file-associations-config.md new file mode 100644 index 00000000000..076f21b94e9 --- /dev/null +++ b/.changes/file-associations-config.md @@ -0,0 +1,5 @@ +--- +"tauri-utils": minor:feat +--- + +Add a configuration object for file associations under `BundleConfig`. diff --git a/.changes/file-associations.md b/.changes/file-associations.md new file mode 100644 index 00000000000..6156f0398bf --- /dev/null +++ b/.changes/file-associations.md @@ -0,0 +1,5 @@ +--- +"tauri": minor:feat +--- + +Added support to file associations. diff --git a/.changes/fix-build-script-mobile-runner-npm.md b/.changes/fix-build-script-mobile-runner-npm.md new file mode 100644 index 00000000000..db7700583e1 --- /dev/null +++ b/.changes/fix-build-script-mobile-runner-npm.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Fixes the generated mobile build script when using an NPM runner. diff --git a/.changes/fix-dev-server-proxy-path.md b/.changes/fix-dev-server-proxy-path.md new file mode 100644 index 00000000000..eb8fd9f26e4 --- /dev/null +++ b/.changes/fix-dev-server-proxy-path.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Properly proxy dev server requests with query strings and fragments. diff --git a/.changes/fix-empty-identifier.md b/.changes/fix-empty-identifier.md new file mode 100644 index 00000000000..e929844ef52 --- /dev/null +++ b/.changes/fix-empty-identifier.md @@ -0,0 +1,7 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +"tauri-macros": 'patch:enhance' +--- + +Resolve Android package name from single word bundle identifiers. diff --git a/.changes/fix-ios-cli-panic.md b/.changes/fix-ios-cli-panic.md new file mode 100644 index 00000000000..451e7a30029 --- /dev/null +++ b/.changes/fix-ios-cli-panic.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:bug +"@tauri-apps/cli": patch:bug +--- + +Fixes panic when exiting the `ios dev` command with Ctrl + C. diff --git a/.changes/fix-ios-plugin-throws-command.md b/.changes/fix-ios-plugin-throws-command.md new file mode 100644 index 00000000000..01d302107d8 --- /dev/null +++ b/.changes/fix-ios-plugin-throws-command.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Use actual iOS plugin instance to run command with `throws`. diff --git a/.changes/fix-ios-run-xcode14.md b/.changes/fix-ios-run-xcode14.md new file mode 100644 index 00000000000..0774a5cffd3 --- /dev/null +++ b/.changes/fix-ios-run-xcode14.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Fixes running on device using Xcode 14. diff --git a/.changes/fix-ios-template.md b/.changes/fix-ios-template.md new file mode 100644 index 00000000000..f8f1df92dc9 --- /dev/null +++ b/.changes/fix-ios-template.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Fixes the iOS project script to build the Rust library. diff --git a/.changes/fix-mobile-env-vars.md b/.changes/fix-mobile-env-vars.md new file mode 100644 index 00000000000..1433afd99dd --- /dev/null +++ b/.changes/fix-mobile-env-vars.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Fixes `TAURI_*` environment variables for hook scripts on mobile commands. diff --git a/.changes/fix-nodejs-android-cmds.md b/.changes/fix-nodejs-android-cmds.md new file mode 100644 index 00000000000..41c62ca0d79 --- /dev/null +++ b/.changes/fix-nodejs-android-cmds.md @@ -0,0 +1,5 @@ +--- +"@tauri-apps/cli": 'patch:enhance' +--- + +Update tauri-mobile to fix running ADB scripts. diff --git a/.changes/fix-orientation-crash.md b/.changes/fix-orientation-crash.md new file mode 100644 index 00000000000..ad181f56b1d --- /dev/null +++ b/.changes/fix-orientation-crash.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Update Android project template with fix to crash on orientation change. diff --git a/.changes/fix-plugin-ios-bool.md b/.changes/fix-plugin-ios-bool.md new file mode 100644 index 00000000000..37a13079bf2 --- /dev/null +++ b/.changes/fix-plugin-ios-bool.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Fixes boolean plugin parameters freezing the application. diff --git a/.changes/fix-plugin-removal.md b/.changes/fix-plugin-removal.md new file mode 100644 index 00000000000..6cf606b7e88 --- /dev/null +++ b/.changes/fix-plugin-removal.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Clear Android plugin JSON file before building Rust library to ensure removed plugins are propagated to the Android project. diff --git a/.changes/fix-plugin-template-cargotoml.md b/.changes/fix-plugin-template-cargotoml.md new file mode 100644 index 00000000000..e3ccd5b7ace --- /dev/null +++ b/.changes/fix-plugin-template-cargotoml.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Readd the Cargo.toml file to the plugin template. diff --git a/.changes/fix-proguard-injection.md b/.changes/fix-proguard-injection.md new file mode 100644 index 00000000000..eedcabbc0c0 --- /dev/null +++ b/.changes/fix-proguard-injection.md @@ -0,0 +1,5 @@ +--- +"tauri-build": 'patch:bug' +--- + +Fixes injection of the proguard rules on the Android project. diff --git a/.changes/fix-proguard-rules.md b/.changes/fix-proguard-rules.md new file mode 100644 index 00000000000..31e67486b72 --- /dev/null +++ b/.changes/fix-proguard-rules.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Fixes ProGuard rules. diff --git a/.changes/fix-shell-build.md b/.changes/fix-shell-build.md new file mode 100644 index 00000000000..ce7c3dc676f --- /dev/null +++ b/.changes/fix-shell-build.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Fix compilation issues without the shell API features. diff --git a/.changes/fix-tauri-binary-windows.md b/.changes/fix-tauri-binary-windows.md new file mode 100644 index 00000000000..8e932906a42 --- /dev/null +++ b/.changes/fix-tauri-binary-windows.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Fixes the Android build gradle plugin implementation on Windows. diff --git a/.changes/fix-tray-icon-validation.md b/.changes/fix-tray-icon-validation.md new file mode 100644 index 00000000000..487fb8a0392 --- /dev/null +++ b/.changes/fix-tray-icon-validation.md @@ -0,0 +1,5 @@ +--- +"tauri-build": patch:bug +--- + +Skip validation of the `tray-icon` feature flag. diff --git a/.changes/fix-xcodescript-lib-path.md b/.changes/fix-xcodescript-lib-path.md new file mode 100644 index 00000000000..99b7bfab55e --- /dev/null +++ b/.changes/fix-xcodescript-lib-path.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Fixes iOS build script using the wrong path for the app library file. diff --git a/.changes/force-colored-logs.md b/.changes/force-colored-logs.md new file mode 100644 index 00000000000..27d48e4b0cb --- /dev/null +++ b/.changes/force-colored-logs.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Force colored logs on mobile commands. diff --git a/.changes/generate-tauri-activity.md b/.changes/generate-tauri-activity.md new file mode 100644 index 00000000000..3be10eafc01 --- /dev/null +++ b/.changes/generate-tauri-activity.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Generate `TauriActivity` Kotlin class on the build script. diff --git a/.changes/gradle-8.md b/.changes/gradle-8.md new file mode 100644 index 00000000000..845db01dbe8 --- /dev/null +++ b/.changes/gradle-8.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'patch:enhance' +'@tauri-apps/cli': 'patch:enhance' +--- + +Update android template to gradle 8.0 diff --git a/.changes/gtk16.md b/.changes/gtk16.md new file mode 100644 index 00000000000..4ca4359b7b0 --- /dev/null +++ b/.changes/gtk16.md @@ -0,0 +1,8 @@ +--- +"tauri-runtime": 'minor:feat' +"tauri-runtime-wry": 'minor:feat' +"tauri": 'minor:feat' +--- + +Update gtk to 0.16. + diff --git a/.changes/improve-local-ip-detection.md b/.changes/improve-local-ip-detection.md new file mode 100644 index 00000000000..5668e53ab19 --- /dev/null +++ b/.changes/improve-local-ip-detection.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Improve local IP address detection with user selection. diff --git a/.changes/improve-mobile-plugin-error-handling.md b/.changes/improve-mobile-plugin-error-handling.md new file mode 100644 index 00000000000..a819ad07ef0 --- /dev/null +++ b/.changes/improve-mobile-plugin-error-handling.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Improve the `run_mobile_plugin` function error handling. diff --git a/.changes/inject-config.md b/.changes/inject-config.md new file mode 100644 index 00000000000..735e86f2d71 --- /dev/null +++ b/.changes/inject-config.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Inject Tauri configuration in the Android assets. diff --git a/.changes/inject-proguard.md b/.changes/inject-proguard.md new file mode 100644 index 00000000000..f9eae0c5dd9 --- /dev/null +++ b/.changes/inject-proguard.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Inject `proguard-tauri.pro` file in the Android project. diff --git a/.changes/invoke-handler-attributes.md b/.changes/invoke-handler-attributes.md new file mode 100644 index 00000000000..35cedd0529f --- /dev/null +++ b/.changes/invoke-handler-attributes.md @@ -0,0 +1,6 @@ +--- +"tauri-macros": 'patch:enhance' +"tauri": 'patch:enhance' +--- + +Added support to attibutes for each command path in the `generate_handler` macro. diff --git a/.changes/invoke-return-bool.md b/.changes/invoke-return-bool.md new file mode 100644 index 00000000000..5e475c5aa17 --- /dev/null +++ b/.changes/invoke-return-bool.md @@ -0,0 +1,7 @@ +--- +"tauri-macros": major:feat +"tauri-codegen": major:feat +"tauri": major:feat +--- + +Return `bool` in the invoke handler. diff --git a/.changes/ios-deployment-target.md b/.changes/ios-deployment-target.md new file mode 100644 index 00000000000..e66aec4e7ab --- /dev/null +++ b/.changes/ios-deployment-target.md @@ -0,0 +1,5 @@ +--- +"tauri-build": 'patch:enhance' +--- + +Read the `IPHONEOS_DEPLOYMENT_TARGET` environment variable to set the Swift iOS target version, defaults to 13. diff --git a/.changes/ios-entitlements.md b/.changes/ios-entitlements.md new file mode 100644 index 00000000000..5bf13ff5709 --- /dev/null +++ b/.changes/ios-entitlements.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:feat +"@tauri-apps/cli": patch:feat +--- + +Generate empty entitlements file for the iOS project. diff --git a/.changes/ios-icon-color.md b/.changes/ios-icon-color.md new file mode 100644 index 00000000000..7cfb8802e91 --- /dev/null +++ b/.changes/ios-icon-color.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Added `--ios-color` option to the `tauri icon` command. diff --git a/.changes/ios-keep-alive.md b/.changes/ios-keep-alive.md new file mode 100644 index 00000000000..0d2829aa3a8 --- /dev/null +++ b/.changes/ios-keep-alive.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Keep the process alive even when the iOS application is closed. diff --git a/.changes/ios-logs.md b/.changes/ios-logs.md new file mode 100644 index 00000000000..cfaa27570c1 --- /dev/null +++ b/.changes/ios-logs.md @@ -0,0 +1,7 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +"tauri": 'patch:enhance' +--- + +Show all application logs on iOS. diff --git a/.changes/ios-product-name.md b/.changes/ios-product-name.md new file mode 100644 index 00000000000..b2968073671 --- /dev/null +++ b/.changes/ios-product-name.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:feat +"@tauri-apps/cli": patch:feat +--- + +Set the iOS project PRODUCT_NAME value to the string under `tauri.conf.json > package > productName` if it is set. diff --git a/.changes/ipc-custom-protocol.md b/.changes/ipc-custom-protocol.md new file mode 100644 index 00000000000..f3b99fb691b --- /dev/null +++ b/.changes/ipc-custom-protocol.md @@ -0,0 +1,6 @@ +--- +"tauri": patch:enhance +"tauri-utils": patch:enhance +--- + +Use custom protocols on the IPC implementation to enhance performance. diff --git a/.changes/ipc-refactor.md b/.changes/ipc-refactor.md new file mode 100644 index 00000000000..6fd4527b56a --- /dev/null +++ b/.changes/ipc-refactor.md @@ -0,0 +1,6 @@ +--- +"tauri": patch:breaking +"tauri-macros": patch:breaking +--- + +Moved `tauri::api::ipc` to `tauri::ipc` and refactored all types. diff --git a/.changes/ipc-scope-remove-enable-tauri-api.md b/.changes/ipc-scope-remove-enable-tauri-api.md new file mode 100644 index 00000000000..3e0a77095bb --- /dev/null +++ b/.changes/ipc-scope-remove-enable-tauri-api.md @@ -0,0 +1,6 @@ +--- +"tauri": 'patch:enhance' +"tauri-utils": 'patch:enhance' +--- + +Remove `enable_tauri_api` from the IPC scope. diff --git a/.changes/kill-dev-process-on-err.md b/.changes/kill-dev-process-on-err.md new file mode 100644 index 00000000000..c59f7140a4a --- /dev/null +++ b/.changes/kill-dev-process-on-err.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:bug +"@tauri-apps/cli": patch:bug +--- + +Exit `beforeDevCommand` process if the android or iOS `dev` command fails. diff --git a/.changes/kuchikiki.md b/.changes/kuchikiki.md new file mode 100644 index 00000000000..b5705790177 --- /dev/null +++ b/.changes/kuchikiki.md @@ -0,0 +1,5 @@ +--- +"tauri-utils": patch:sec +--- + +Changed HTML implementation from unmaintained `kuchiki` to `kuchikiki`. diff --git a/.changes/lib-name-xcode.md b/.changes/lib-name-xcode.md new file mode 100644 index 00000000000..5af4eb4b36c --- /dev/null +++ b/.changes/lib-name-xcode.md @@ -0,0 +1,5 @@ +--- +'tauri': 'patch:enhance' +--- + +Use correct lib name in xcode project. diff --git a/.changes/linux-ipc-body-feature.md b/.changes/linux-ipc-body-feature.md new file mode 100644 index 00000000000..a80dbc0fdd3 --- /dev/null +++ b/.changes/linux-ipc-body-feature.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:breaking +--- + +Removed the `linux-protocol-headers` feature (now always enabled) and added `linux-ipc-protocol`. diff --git a/.changes/linux-protocol-body-feature.md b/.changes/linux-protocol-body-feature.md new file mode 100644 index 00000000000..9542fbdd8a3 --- /dev/null +++ b/.changes/linux-protocol-body-feature.md @@ -0,0 +1,5 @@ +--- +"tauri-runtime-wry": patch:breaking +--- + +Removed the `linux-headers` feature (now always enabled) and added `linux-protocol-body`. diff --git a/.changes/local-dev-path-mobile.md b/.changes/local-dev-path-mobile.md new file mode 100644 index 00000000000..5a1381323b6 --- /dev/null +++ b/.changes/local-dev-path-mobile.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Fixes HMR on mobile when devPath is configured to load a filesystem path. diff --git a/.changes/log-file-fix-for-linux-and-windows.md b/.changes/log-file-fix-for-linux-and-windows.md new file mode 100644 index 00000000000..4949b3d2dce --- /dev/null +++ b/.changes/log-file-fix-for-linux-and-windows.md @@ -0,0 +1,5 @@ +--- +'tauri': 'minor:bug' +--- + +Fix default log path for linux and windows diff --git a/.changes/logcat-all-tags.md b/.changes/logcat-all-tags.md new file mode 100644 index 00000000000..8d40bcfe786 --- /dev/null +++ b/.changes/logcat-all-tags.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Print log output for all tags on Android development. diff --git a/.changes/migrate-cmd.md b/.changes/migrate-cmd.md new file mode 100644 index 00000000000..4fae458f3b6 --- /dev/null +++ b/.changes/migrate-cmd.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:feat' +"@tauri-apps/cli": 'patch:feat' +--- + +Added `migrate` command. diff --git a/.changes/migrate-csp.md b/.changes/migrate-csp.md new file mode 100644 index 00000000000..551c7c8473d --- /dev/null +++ b/.changes/migrate-csp.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:enhance +"@tauri-apps/cli": patch:enhance +--- + +Update migrate command to update the configuration CSP to include `ipc:` on the `connect-src` directive, needed by the new IPC using custom protocols. diff --git a/.changes/migrate-plugins.md b/.changes/migrate-plugins.md new file mode 100644 index 00000000000..e4c63d3e1a9 --- /dev/null +++ b/.changes/migrate-plugins.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": patch:feat +"@tauri-apps/cli": patch:feat +--- + +The `migrate` command now automatically reads all JavaScript files and updates `@tauri-apps/api` import paths and install the missing plugins. diff --git a/.changes/min-sdk-version.md b/.changes/min-sdk-version.md new file mode 100644 index 00000000000..633247f3448 --- /dev/null +++ b/.changes/min-sdk-version.md @@ -0,0 +1,5 @@ +--- +"tauri-utils": 'patch:enhance' +--- + +Added `android` configuration object under `tauri > bundle`. diff --git a/.changes/mobile-config.md b/.changes/mobile-config.md new file mode 100644 index 00000000000..85026f10ea5 --- /dev/null +++ b/.changes/mobile-config.md @@ -0,0 +1,5 @@ +--- +"tauri-utils": 'minor:feat' +--- + +Parse `android` and `ios` Tauri configuration files. diff --git a/.changes/mobile-dev-watcher-ignore-gen.md b/.changes/mobile-dev-watcher-ignore-gen.md new file mode 100644 index 00000000000..96ca550d81a --- /dev/null +++ b/.changes/mobile-dev-watcher-ignore-gen.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Ignore the `gen` folder on the dev watcher. diff --git a/.changes/mobile-entry-point-macro.md b/.changes/mobile-entry-point-macro.md new file mode 100644 index 00000000000..5f428bdc4ca --- /dev/null +++ b/.changes/mobile-entry-point-macro.md @@ -0,0 +1,5 @@ +--- +"tauri-macros": 'minor:feat' +--- + +Added the `mobile_entry_point` macro. diff --git a/.changes/mobile-env-vars-rename.md b/.changes/mobile-env-vars-rename.md new file mode 100644 index 00000000000..7067ce7bad2 --- /dev/null +++ b/.changes/mobile-env-vars-rename.md @@ -0,0 +1,6 @@ +--- +"tauri-build": 'patch:enhance' +"tauri-macros": 'patch:enhance' +--- + +Refactor mobile environment variables. diff --git a/.changes/mobile-init.md b/.changes/mobile-init.md new file mode 100644 index 00000000000..7b47ed16cd6 --- /dev/null +++ b/.changes/mobile-init.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'minor:feat' +"@tauri-apps/cli": 'minor:feat' +--- + +Added `android init` and `ios init` commands. diff --git a/.changes/mobile-lib-name.md b/.changes/mobile-lib-name.md new file mode 100644 index 00000000000..496d4e7f821 --- /dev/null +++ b/.changes/mobile-lib-name.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Add support to custom and kebab case library names for mobile apps. diff --git a/.changes/mobile-open.md b/.changes/mobile-open.md new file mode 100644 index 00000000000..007eafe53bc --- /dev/null +++ b/.changes/mobile-open.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'minor:feat' +"@tauri-apps/cli": 'minor:feat' +--- + +Added `android open` and `ios open` commands. diff --git a/.changes/mobile-plugin-config.md b/.changes/mobile-plugin-config.md new file mode 100644 index 00000000000..6eb4f772b58 --- /dev/null +++ b/.changes/mobile-plugin-config.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Expose plugin configuration on the Android and iOS plugin classes. diff --git a/.changes/mobile-plugins.md b/.changes/mobile-plugins.md new file mode 100644 index 00000000000..03a673a0ed7 --- /dev/null +++ b/.changes/mobile-plugins.md @@ -0,0 +1,5 @@ +--- +"tauri": 'minor:feat' +--- + +Run Android and iOS native plugins on the invoke handler if a Rust plugin command is not found. diff --git a/.changes/mobile-webview-access.md b/.changes/mobile-webview-access.md new file mode 100644 index 00000000000..be725d5e7cc --- /dev/null +++ b/.changes/mobile-webview-access.md @@ -0,0 +1,6 @@ +--- +"tauri-runtime-wry": 'minor:feat' +"tauri": 'minor:feat' +--- + +Support `with_webview` for Android platform alowing execution of JNI code in context. diff --git a/.changes/mobile.md b/.changes/mobile.md new file mode 100644 index 00000000000..e494dfd8507 --- /dev/null +++ b/.changes/mobile.md @@ -0,0 +1,13 @@ +--- +"api": major:feat +"tauri-utils": major:feat +"tauri-bundler": major:feat +"tauri-codegen": major:feat +"tauri-macros": major:feat +"tauri-build": major:feat +"tauri": major:feat +"tauri-cli": major:feat +"@tauri-apps/cli": major:feat +--- + +First mobile alpha release! diff --git a/.changes/move-app.md b/.changes/move-app.md new file mode 100644 index 00000000000..746c84ab435 --- /dev/null +++ b/.changes/move-app.md @@ -0,0 +1,6 @@ +--- +"api": 'patch:enhance' +"tauri": 'patch:enhance' +--- + +Moved the `app` feature to its own plugin in the plugins-workspace repository. diff --git a/.changes/move-cli.md b/.changes/move-cli.md new file mode 100644 index 00000000000..0cd3ac386b2 --- /dev/null +++ b/.changes/move-cli.md @@ -0,0 +1,7 @@ +--- +"api": 'patch:enhance' +"tauri": 'patch:enhance' +"tauri-utils": 'patch:enhance' +--- + +Moved the `cli` feature to its own plugin in the plugins-workspace repository. diff --git a/.changes/move-dialog-plugin.md b/.changes/move-dialog-plugin.md new file mode 100644 index 00000000000..16a7e40404f --- /dev/null +++ b/.changes/move-dialog-plugin.md @@ -0,0 +1,6 @@ +--- +"tauri": 'patch:enhance' +"api": 'patch:enhance' +--- + +Moved the dialog APIs to its own plugin in the plugins-workspace repository. diff --git a/.changes/move-event.md b/.changes/move-event.md new file mode 100644 index 00000000000..40509a43b51 --- /dev/null +++ b/.changes/move-event.md @@ -0,0 +1,8 @@ +--- +"api": 'patch:enhance' +"tauri": 'patch:enhance' +"tauri-runtime": 'patch:enhance' +"tauri-runtime-wry": 'patch:enhance' +--- + +Moved the `event` JS APIs to a plugin. diff --git a/.changes/move-fs.md b/.changes/move-fs.md new file mode 100644 index 00000000000..b468af4e17c --- /dev/null +++ b/.changes/move-fs.md @@ -0,0 +1,6 @@ +--- +"api": 'patch:enhance' +"tauri": 'patch:enhance' +--- + +Moved the file system APIs to its own plugin in the plugins-workspace repository. diff --git a/.changes/move-http-api.md b/.changes/move-http-api.md new file mode 100644 index 00000000000..98dde1732b5 --- /dev/null +++ b/.changes/move-http-api.md @@ -0,0 +1,6 @@ +--- +"api": 'patch:enhance' +"tauri": 'patch:enhance' +--- + +Moved the `http` feature to its own plugin in the plugins-workspace repository. diff --git a/.changes/move-os.md b/.changes/move-os.md new file mode 100644 index 00000000000..b1dfd4ae8b2 --- /dev/null +++ b/.changes/move-os.md @@ -0,0 +1,6 @@ +--- +"api": 'patch:enhance' +"tauri": 'patch:enhance' +--- + +Moved the `os` feature to its own plugin in the plugins-workspace repository. diff --git a/.changes/move-process.md b/.changes/move-process.md new file mode 100644 index 00000000000..637d13e07b2 --- /dev/null +++ b/.changes/move-process.md @@ -0,0 +1,6 @@ +--- +"api": 'patch:enhance' +"tauri": 'patch:enhance' +--- + +Moved the `process` feature to its own plugin in the plugins-workspace repository. diff --git a/.changes/move-protocol-asset.md b/.changes/move-protocol-asset.md new file mode 100644 index 00000000000..3dd27fbf096 --- /dev/null +++ b/.changes/move-protocol-asset.md @@ -0,0 +1,6 @@ +--- +"tauri": 'patch:enhance' +"tauri-utils": 'patch:enhance' +--- + +Moved the `protocol` scope configuration to the `asset_protocol` field in `SecurityConfig`. diff --git a/.changes/move-shell.md b/.changes/move-shell.md new file mode 100644 index 00000000000..0bd1264df45 --- /dev/null +++ b/.changes/move-shell.md @@ -0,0 +1,8 @@ +--- +"api": 'patch:enhance' +"tauri": 'patch:enhance' +"tauri-codegen": 'patch:enhance' +"tauri-macros": 'patch:enhance' +--- + +Moved the `shell` functionality to its own plugin in the plugins-workspace repository. diff --git a/.changes/move-updater-config.md b/.changes/move-updater-config.md new file mode 100644 index 00000000000..279834d9f8b --- /dev/null +++ b/.changes/move-updater-config.md @@ -0,0 +1,8 @@ +--- +"tauri": 'patch:enhance' +"tauri-utils": 'patch:enhance' +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Moved the updater configuration to the `BundleConfig`. diff --git a/.changes/move-updater.md b/.changes/move-updater.md new file mode 100644 index 00000000000..9a9b1df0299 --- /dev/null +++ b/.changes/move-updater.md @@ -0,0 +1,7 @@ +--- +"api": 'patch:enhance' +"tauri": 'patch:enhance' +"tauri-utils": 'patch:enhance' +--- + +Moved the `updater` feature to its own plugin in the plugins-workspace repository. diff --git a/.changes/msrv-1.64.md b/.changes/msrv-1.64.md new file mode 100644 index 00000000000..2a6186c6bf7 --- /dev/null +++ b/.changes/msrv-1.64.md @@ -0,0 +1,13 @@ +--- +"tauri-cli": 'minor:feat' +"tauri-bundler": 'minor:feat' +"tauri": 'minor:feat' +"tauri-build": 'minor:feat' +"tauri-codegen": 'minor:feat' +"tauri-macros": 'minor:feat' +"tauri-utils": 'minor:feat' +"tauri-runtime": 'minor:feat' +"tauri-runtime-wry": 'minor:feat' +--- + +Bump the MSRV to 1.64. diff --git a/.changes/msrv-1.65.md b/.changes/msrv-1.65.md new file mode 100644 index 00000000000..911372072c9 --- /dev/null +++ b/.changes/msrv-1.65.md @@ -0,0 +1,13 @@ +--- +"tauri-cli": 'minor:feat' +"tauri-bundler": 'minor:feat' +"tauri": 'minor:feat' +"tauri-build": 'minor:feat' +"tauri-codegen": 'minor:feat' +"tauri-macros": 'minor:feat' +"tauri-utils": 'minor:feat' +"tauri-runtime": 'minor:feat' +"tauri-runtime-wry": 'minor:feat' +--- + +Bump the MSRV to 1.65. diff --git a/.changes/napi-rs.md b/.changes/napi-rs.md new file mode 100644 index 00000000000..165a06339e3 --- /dev/null +++ b/.changes/napi-rs.md @@ -0,0 +1,5 @@ +--- +'@tauri-apps/cli': 'patch:enhance' +--- + +Update `napi-rs` dependencies to latest to fix CLI hanging up forever. diff --git a/.changes/npm-pass-args.md b/.changes/npm-pass-args.md new file mode 100644 index 00000000000..6496d32f5fa --- /dev/null +++ b/.changes/npm-pass-args.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Correctly pass arguments from `npm run` to `tauri`. diff --git a/.changes/nsis-bulgarian.md b/.changes/nsis-bulgarian.md new file mode 100644 index 00000000000..8fdc2e3a8ca --- /dev/null +++ b/.changes/nsis-bulgarian.md @@ -0,0 +1,5 @@ +--- +'tauri-bundler': 'patch:enhance' +--- + +Added Bulgarian language support to the NSIS bundler. diff --git a/.changes/nsis-set-compressor.md b/.changes/nsis-set-compressor.md new file mode 100644 index 00000000000..34256f5d5a1 --- /dev/null +++ b/.changes/nsis-set-compressor.md @@ -0,0 +1,5 @@ +--- +'tauri-bundler': 'patch:enhance' +--- + +Add `compression` configuration option under `tauri > bundle > windows > nsis`. diff --git a/.changes/on-navigation-plugin.md b/.changes/on-navigation-plugin.md new file mode 100644 index 00000000000..32b53447286 --- /dev/null +++ b/.changes/on-navigation-plugin.md @@ -0,0 +1,6 @@ +--- +"tauri": 'minor:feat' +--- + +Added `PluginBuilder::on_navigation`. +Added `Plugin::on_navigation`. diff --git a/.changes/on-new-intent.md b/.changes/on-new-intent.md new file mode 100644 index 00000000000..c7c89f80fc7 --- /dev/null +++ b/.changes/on-new-intent.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Added the `onNewIntent` Plugin hook on Android. diff --git a/.changes/only-proxy-on-mobile.md b/.changes/only-proxy-on-mobile.md new file mode 100644 index 00000000000..2998169183c --- /dev/null +++ b/.changes/only-proxy-on-mobile.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Only proxy the dev server on mobile to simplify desktop usage. diff --git a/.changes/open-ts-overload.md b/.changes/open-ts-overload.md new file mode 100644 index 00000000000..6ea100f344e --- /dev/null +++ b/.changes/open-ts-overload.md @@ -0,0 +1,5 @@ +--- +"api": 'patch:enhance' +--- + +Overload the dialog `open` function to have better TS result types. diff --git a/.changes/package-info-crate-name.md b/.changes/package-info-crate-name.md new file mode 100644 index 00000000000..c4d3aa93098 --- /dev/null +++ b/.changes/package-info-crate-name.md @@ -0,0 +1,6 @@ +--- +"tauri-utils": 'patch:enhance' +"tauri-codegen": 'patch:enhance' +--- + +Added `crate_name` field on `PackageInfo`. diff --git a/.changes/path-sep-delimter.md b/.changes/path-sep-delimter.md new file mode 100644 index 00000000000..6882135eb2b --- /dev/null +++ b/.changes/path-sep-delimter.md @@ -0,0 +1,5 @@ +--- +'@tauri-apps/api': 'minor:enhance' +--- + +Changed `sep` and `delimiter` from `path` module into functions to fix import in frameworks like `next.js` diff --git a/.changes/plugin-android-project-refactor.md b/.changes/plugin-android-project-refactor.md new file mode 100644 index 00000000000..d79965d7380 --- /dev/null +++ b/.changes/plugin-android-project-refactor.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"tauri-build": 'patch:enhance' +--- + +Use absolute path to each Android plugin project instead of copying the files to enhance developer experience. diff --git a/.changes/plugin-api-handle.md b/.changes/plugin-api-handle.md new file mode 100644 index 00000000000..8a02043df0d --- /dev/null +++ b/.changes/plugin-api-handle.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Add `app` method for the `PluginApi` struct. diff --git a/.changes/plugin-config-getter.md b/.changes/plugin-config-getter.md new file mode 100644 index 00000000000..b8e831f4a66 --- /dev/null +++ b/.changes/plugin-config-getter.md @@ -0,0 +1,5 @@ +--- +"tauri-build": patch:feat +--- + +Added the `config::plugin_config` function to read the plugin configuration set from the CLI. diff --git a/.changes/plugin-handle-clone.md b/.changes/plugin-handle-clone.md new file mode 100644 index 00000000000..7863ad76228 --- /dev/null +++ b/.changes/plugin-handle-clone.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Implement `Clone` for `plugin::PluginHandle`. diff --git a/.changes/plugin-init-fns.md b/.changes/plugin-init-fns.md new file mode 100644 index 00000000000..ad85adf940e --- /dev/null +++ b/.changes/plugin-init-fns.md @@ -0,0 +1,5 @@ +--- +"tauri": 'minor:feat' +--- + +Added `initialize_android_plugin` and `initialize_ios_plugin` APIs on `AppHandle`. diff --git a/.changes/plugin-init-refactor.md b/.changes/plugin-init-refactor.md new file mode 100644 index 00000000000..b2cbbafd73d --- /dev/null +++ b/.changes/plugin-init-refactor.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'minor:feat' +"@tauri-apps/cli": 'minor:feat' +--- + +Changed the `--api` flag on `plugin init` to `--no-api`. diff --git a/.changes/plugin-setup-refactor.md b/.changes/plugin-setup-refactor.md new file mode 100644 index 00000000000..6e8afa6fba5 --- /dev/null +++ b/.changes/plugin-setup-refactor.md @@ -0,0 +1,5 @@ +--- +"tauri": major:feat +--- + +Changed the plugin setup hook to take a second argument of type `PluginApi`. diff --git a/.changes/plugin-template-examples-manifest.md b/.changes/plugin-template-examples-manifest.md new file mode 100644 index 00000000000..fd082fe3f0d --- /dev/null +++ b/.changes/plugin-template-examples-manifest.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Add Cargo manifest files for the plugin example templates. diff --git a/.changes/pnpm-android.md b/.changes/pnpm-android.md new file mode 100644 index 00000000000..d91a2b4efca --- /dev/null +++ b/.changes/pnpm-android.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Fix `tauri android build/dev` crashing when used with standalone `pnpm` executable on Windows. diff --git a/.changes/pre.json b/.changes/pre.json new file mode 100644 index 00000000000..a3bd9b4bcea --- /dev/null +++ b/.changes/pre.json @@ -0,0 +1,225 @@ +{ + "tag": "alpha", + "changes": [ + ".changes/add-command.md", + ".changes/add-mobile-to-plugin.md", + ".changes/add-visible-on-all-workspaces.md", + ".changes/android-apis-runtime.md", + ".changes/android-buildsrc-gitignore.md", + ".changes/android-enhance-method-parse.md", + ".changes/android-load-config.md", + ".changes/android-on-new-intent.md", + ".changes/android-plugin-command-exception.md", + ".changes/api-ipc-refactor.md", + ".changes/build-android-env-vars.md", + ".changes/bump-1.3.md", + ".changes/bundler-remove-dialog-option.md", + ".changes/bundler-zip-deflate.md", + ".changes/channel-api.md", + ".changes/channel-rust.md", + ".changes/cli-android-build.md", + ".changes/cli-android-dev-release.md", + ".changes/cli-android-specified-targets-only.md", + ".changes/cli-android-split-per-abit-target.md", + ".changes/cli-apple-dev-team.md", + ".changes/cli-built-in-dev-server-mobile.md", + ".changes/cli-expose-plugin-config.md", + ".changes/cli-ios-build.md", + ".changes/cli-ios-metadata-env-var.md", + ".changes/cli-key-properties.md", + ".changes/cli-libname-dashes.md", + ".changes/cli-library-compilation.md", + ".changes/cli-mobile-auto-ip.md", + ".changes/cli-mobile-cwd-config.md", + ".changes/cli-mobile-dev.md", + ".changes/cli-mobile-plugin.md", + ".changes/cli-nodejs-detection.md", + ".changes/cli-npx-mobile.md", + ".changes/cli-pnpm.md", + ".changes/cli-refactor-ipc-mobile.md", + ".changes/cli-skip-targets-install.md", + ".changes/cli-wry-0-28.md", + ".changes/codegen-mobile-devurl.md", + ".changes/config-incognito.md", + ".changes/config-tray-icon-tooltip.md", + ".changes/config-tray-icon.md", + ".changes/core-android-proxy-method.md", + ".changes/core-app-montior.md", + ".changes/core-channel-clone.md", + ".changes/core-incognito.md", + ".changes/core-navigate-method.md", + ".changes/core-remove-file-dir-semver-apis.md", + ".changes/core-wry-0-28.md", + ".changes/dark-light-mica-effect.md", + ".changes/default-tls-features.md", + ".changes/default-window-icon.md", + ".changes/dev-proxy-response-cache.md", + ".changes/dev-proxy.md", + ".changes/downgrade-min-sdk-version.md", + ".changes/dynamic-wry-plugin.md", + ".changes/enable-minify.md", + ".changes/enable-path-commands.md", + ".changes/enhance-jsobject-return-types.md", + ".changes/error-on-identifier-change.md", + ".changes/event-api-window-label.md", + ".changes/file-associations-config.md", + ".changes/file-associations.md", + ".changes/fix-build-script-mobile-runner-npm.md", + ".changes/fix-dev-server-proxy-path.md", + ".changes/fix-empty-identifier.md", + ".changes/fix-ios-cli-panic.md", + ".changes/fix-ios-plugin-throws-command.md", + ".changes/fix-ios-run-xcode14.md", + ".changes/fix-ios-template.md", + ".changes/fix-mobile-env-vars.md", + ".changes/fix-nodejs-android-cmds.md", + ".changes/fix-orientation-crash.md", + ".changes/fix-plugin-ios-bool.md", + ".changes/fix-plugin-removal.md", + ".changes/fix-plugin-template-cargotoml.md", + ".changes/fix-proguard-injection.md", + ".changes/fix-proguard-rules.md", + ".changes/fix-shell-build.md", + ".changes/fix-tauri-binary-windows.md", + ".changes/fix-tray-icon-validation.md", + ".changes/fix-xcodescript-lib-path.md", + ".changes/force-colored-logs.md", + ".changes/generate-tauri-activity.md", + ".changes/gradle-8.md", + ".changes/gtk16.md", + ".changes/improve-local-ip-detection.md", + ".changes/improve-mobile-plugin-error-handling.md", + ".changes/inject-config.md", + ".changes/inject-proguard.md", + ".changes/invoke-handler-attributes.md", + ".changes/invoke-return-bool.md", + ".changes/ios-deployment-target.md", + ".changes/ios-entitlements.md", + ".changes/ios-icon-color.md", + ".changes/ios-keep-alive.md", + ".changes/ios-logs.md", + ".changes/ios-product-name.md", + ".changes/ipc-custom-protocol.md", + ".changes/ipc-refactor.md", + ".changes/ipc-scope-remove-enable-tauri-api.md", + ".changes/kill-dev-process-on-err.md", + ".changes/kuchikiki.md", + ".changes/lib-name-xcode.md", + ".changes/linux-ipc-body-feature.md", + ".changes/linux-protocol-body-feature.md", + ".changes/local-dev-path-mobile.md", + ".changes/log-file-fix-for-linux-and-windows.md", + ".changes/logcat-all-tags.md", + ".changes/migrate-cmd.md", + ".changes/migrate-csp.md", + ".changes/migrate-plugins.md", + ".changes/min-sdk-version.md", + ".changes/mobile-config.md", + ".changes/mobile-dev-watcher-ignore-gen.md", + ".changes/mobile-entry-point-macro.md", + ".changes/mobile-env-vars-rename.md", + ".changes/mobile-init.md", + ".changes/mobile-lib-name.md", + ".changes/mobile-open.md", + ".changes/mobile-plugin-config.md", + ".changes/mobile-plugins.md", + ".changes/mobile-webview-access.md", + ".changes/mobile.md", + ".changes/move-app.md", + ".changes/move-cli.md", + ".changes/move-dialog-plugin.md", + ".changes/move-event.md", + ".changes/move-fs.md", + ".changes/move-http-api.md", + ".changes/move-os.md", + ".changes/move-process.md", + ".changes/move-protocol-asset.md", + ".changes/move-shell.md", + ".changes/move-updater-config.md", + ".changes/move-updater.md", + ".changes/msrv-1.64.md", + ".changes/msrv-1.65.md", + ".changes/napi-rs.md", + ".changes/npm-pass-args.md", + ".changes/on-navigation-plugin.md", + ".changes/on-new-intent.md", + ".changes/only-proxy-on-mobile.md", + ".changes/open-ts-overload.md", + ".changes/package-info-crate-name.md", + ".changes/path-sep-delimter.md", + ".changes/plugin-android-project-refactor.md", + ".changes/plugin-api-handle.md", + ".changes/plugin-config-getter.md", + ".changes/plugin-handle-clone.md", + ".changes/plugin-init-fns.md", + ".changes/plugin-init-refactor.md", + ".changes/plugin-setup-refactor.md", + ".changes/plugin-template-examples-manifest.md", + ".changes/pnpm-android.md", + ".changes/process-mod-refactor.md", + ".changes/raw-encoding.md", + ".changes/refactor-macros.md", + ".changes/refactor-setup.md", + ".changes/refactor-tauri-android-dependency.md", + ".changes/remove-allowlist.md", + ".changes/remove-attohttpc.md", + ".changes/remove-clipboard.md", + ".changes/remove-fs-apis.md", + ".changes/remove-global-shortcut.md", + ".changes/remove-macros-command-module.md", + ".changes/remove-mobile-log.md", + ".changes/remove-sdk-dir.md", + ".changes/remove-shell-constructor.md", + ".changes/remove-tray-icon-mobile.md", + ".changes/remove-updater-dialog.md", + ".changes/remove-updater-event.md", + ".changes/remove-window.md", + ".changes/rewrite-android-manifest.md", + ".changes/rfd101.md", + ".changes/run-event-opened.md", + ".changes/run-mobile-plugin.md", + ".changes/runtime-create-window-handler.md", + ".changes/runtime-defaultvbox.md", + ".changes/runtime-menu-system-tray.md", + ".changes/runtime-monitor.md", + ".changes/runtime-navigate-method.md", + ".changes/runtime-navigation-handler-url-arg.md", + ".changes/runtime-new-args.md", + ".changes/runtime-opened-event.md", + ".changes/safepathbuf-refactor.md", + ".changes/shadow-api.md", + ".changes/shadow-config.md", + ".changes/shadow.md", + ".changes/shadows-default-on.md", + ".changes/simplify-ios-plugin-init-fn.md", + ".changes/skip-target-install-arg.md", + ".changes/state-0.6.md", + ".changes/system-tray-feat.md", + ".changes/target-dir-detection.md", + ".changes/tauri-app-handle-ref.md", + ".changes/tauri-build-mobile.md", + ".changes/tauri-cleanup-before-exit.md", + ".changes/tauri-defaultvbox.md", + ".changes/tauri-libxdo-feat.md", + ".changes/tauri-menu-tray-refactor.md", + ".changes/tauri-mobile-entry-point.md", + ".changes/tauri-nsview.md", + ".changes/tauri-run_on_main_thread.md", + ".changes/tauri-runtime-wry-wry-0-28.md", + ".changes/tauri-tray-icon-feat-flag.md", + ".changes/tempdir-api.md", + ".changes/tempdir-core.md", + ".changes/tls-features-automatically-enabled.md", + ".changes/tls-features-refactor.md", + ".changes/ubuntu-20.04-cli.js.md", + ".changes/ubuntu-20.04-cli.rs.md", + ".changes/update-entitlements-api.md", + ".changes/window-effects-api.md", + ".changes/window-effects-config.md", + ".changes/window-effects.md", + ".changes/window-on-navigation-arg.md", + ".changes/with-webview.md", + ".changes/wry-navigate-method.md", + ".changes/wry26.md" + ] +} diff --git a/.changes/process-mod-refactor.md b/.changes/process-mod-refactor.md new file mode 100644 index 00000000000..47517cff796 --- /dev/null +++ b/.changes/process-mod-refactor.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Moved the `tauri::api::process` module to `tauri::process`. diff --git a/.changes/raw-encoding.md b/.changes/raw-encoding.md new file mode 100644 index 00000000000..8c43cfa78f6 --- /dev/null +++ b/.changes/raw-encoding.md @@ -0,0 +1,6 @@ +--- +"api": 'minor:feat' +"tauri": 'minor:feat' +--- + +Added `raw` encoding option to read stdout and stderr raw bytes. diff --git a/.changes/readme.md b/.changes/readme.md index 73ee22d9790..318eea024e7 100644 --- a/.changes/readme.md +++ b/.changes/readme.md @@ -10,8 +10,8 @@ Use the following format: ```md --- -'package-a': patch -'package-b': patch +'package-a': 'patch:enhance' +'package-b': 'patch:enhance' --- Change summary goes here @@ -33,7 +33,7 @@ Additionally you could specify a tag for the change file to group it with other ```md --- -'package-a': patch:bug +'package-a': 'patch:enhance' --- Change summary goes here diff --git a/.changes/refactor-macros.md b/.changes/refactor-macros.md new file mode 100644 index 00000000000..49b3fb4ed36 --- /dev/null +++ b/.changes/refactor-macros.md @@ -0,0 +1,6 @@ +--- +"tauri-macros": 'patch:enhance' +"tauri": 'patch:enhance' +--- + +Refactored the implementation of the `mobile_entry_point` macro. diff --git a/.changes/refactor-setup.md b/.changes/refactor-setup.md new file mode 100644 index 00000000000..58439224246 --- /dev/null +++ b/.changes/refactor-setup.md @@ -0,0 +1,5 @@ +--- +"tauri": major:feat +--- + +**Breaking change:** The window creation and setup hook are now called when the event loop is ready. diff --git a/.changes/refactor-tauri-android-dependency.md b/.changes/refactor-tauri-android-dependency.md new file mode 100644 index 00000000000..a0bd094ab68 --- /dev/null +++ b/.changes/refactor-tauri-android-dependency.md @@ -0,0 +1,6 @@ +--- +"tauri": 'patch:enhance' +"tauri-build": 'patch:enhance' +--- + +Changed how the `tauri-android` dependency is injected. This requires the `gen/android` project to be recreated. diff --git a/.changes/remove-allowlist.md b/.changes/remove-allowlist.md new file mode 100644 index 00000000000..55cf32fb82e --- /dev/null +++ b/.changes/remove-allowlist.md @@ -0,0 +1,8 @@ +--- +"tauri": 'patch:enhance' +"tauri-utils": 'patch:enhance' +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Removed the allowlist configuration. diff --git a/.changes/remove-attohttpc.md b/.changes/remove-attohttpc.md new file mode 100644 index 00000000000..239701a7fb2 --- /dev/null +++ b/.changes/remove-attohttpc.md @@ -0,0 +1,5 @@ +--- +"tauri": major:feat +--- + +Removed the attohttpc client. The `reqwest-*` Cargo features were also removed. diff --git a/.changes/remove-clipboard.md b/.changes/remove-clipboard.md new file mode 100644 index 00000000000..4469be99acf --- /dev/null +++ b/.changes/remove-clipboard.md @@ -0,0 +1,8 @@ +--- +"tauri": 'patch:enhance' +"tauri-runtime": 'patch:enhance' +"tauri-runtime-wry": 'patch:enhance' +"api": 'patch:enhance' +--- + +Moved the `clipboard` feature to its own plugin in the plugins-workspace repository. diff --git a/.changes/remove-fs-apis.md b/.changes/remove-fs-apis.md new file mode 100644 index 00000000000..4c684e94113 --- /dev/null +++ b/.changes/remove-fs-apis.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Removed extract and move APIs from `tauri::api::file`. diff --git a/.changes/remove-global-shortcut.md b/.changes/remove-global-shortcut.md new file mode 100644 index 00000000000..67017fd1e69 --- /dev/null +++ b/.changes/remove-global-shortcut.md @@ -0,0 +1,8 @@ +--- +"api": 'patch:enhance' +"tauri": 'patch:enhance' +"tauri-runtime": 'patch:enhance' +"tauri-runtime-wry": 'patch:enhance' +--- + +Moved the `global-shortcut` feature to its own plugin in the plugins-workspace repository. diff --git a/.changes/remove-macros-command-module.md b/.changes/remove-macros-command-module.md new file mode 100644 index 00000000000..fdf72026796 --- /dev/null +++ b/.changes/remove-macros-command-module.md @@ -0,0 +1,5 @@ +--- +"tauri-macros": 'patch:enhance' +--- + +Removed the module command macros. diff --git a/.changes/remove-mobile-log.md b/.changes/remove-mobile-log.md new file mode 100644 index 00000000000..3f02355e816 --- /dev/null +++ b/.changes/remove-mobile-log.md @@ -0,0 +1,7 @@ +--- +"tauri": 'patch:enhance' +"tauri-macros": 'patch:enhance' +"tauri-build": 'patch:enhance' +--- + +Removed mobile logging initialization, which will be handled by `tauri-plugin-log`. diff --git a/.changes/remove-sdk-dir.md b/.changes/remove-sdk-dir.md new file mode 100644 index 00000000000..c4150a9257c --- /dev/null +++ b/.changes/remove-sdk-dir.md @@ -0,0 +1,5 @@ +--- +"tauri-build": 'patch:enhance' +--- + +Remove `WindowsAttributes::sdk_dir`. diff --git a/.changes/remove-shell-constructor.md b/.changes/remove-shell-constructor.md new file mode 100644 index 00000000000..c8c4b0bbdc1 --- /dev/null +++ b/.changes/remove-shell-constructor.md @@ -0,0 +1,5 @@ +--- +"api": 'minor:feat' +--- + +Removed shell's `Command` constructor and added the `Command.create` static function instead. diff --git a/.changes/remove-tray-icon-mobile.md b/.changes/remove-tray-icon-mobile.md new file mode 100644 index 00000000000..2ab793e0e0c --- /dev/null +++ b/.changes/remove-tray-icon-mobile.md @@ -0,0 +1,6 @@ +--- +"tauri-codegen": 'patch:enhance' +"tauri": 'patch:enhance' +--- + +Refactor the `Context` conditional fields and only parse the tray icon on desktop. diff --git a/.changes/remove-updater-dialog.md b/.changes/remove-updater-dialog.md new file mode 100644 index 00000000000..bf12438c45a --- /dev/null +++ b/.changes/remove-updater-dialog.md @@ -0,0 +1,6 @@ +--- +"tauri": 'patch:enhance' +"tauri-utils": 'patch:enhance' +--- + +Remove the updater's dialog option. diff --git a/.changes/remove-updater-event.md b/.changes/remove-updater-event.md new file mode 100644 index 00000000000..fcaa80e4e49 --- /dev/null +++ b/.changes/remove-updater-event.md @@ -0,0 +1,6 @@ +--- +"tauri": 'patch:enhance' +--- + +Removed `UpdaterEvent`. See `tauri-plugin-updater` for new usage. + diff --git a/.changes/remove-window.md b/.changes/remove-window.md new file mode 100644 index 00000000000..15662c88eae --- /dev/null +++ b/.changes/remove-window.md @@ -0,0 +1,6 @@ +--- +"tauri": 'patch:enhance' +"api": 'patch:enhance' +--- + +Moved the `window` JS APIs to its own plugin in the plugins-workspace repository. diff --git a/.changes/rewrite-android-manifest.md b/.changes/rewrite-android-manifest.md new file mode 100644 index 00000000000..f1244a8cbf4 --- /dev/null +++ b/.changes/rewrite-android-manifest.md @@ -0,0 +1,5 @@ +--- +"tauri-build": patch:feat +--- + +Added the `mobile::update_android_manifest` function. diff --git a/.changes/rfd101.md b/.changes/rfd101.md new file mode 100644 index 00000000000..78feeb53d28 --- /dev/null +++ b/.changes/rfd101.md @@ -0,0 +1,6 @@ +--- +"tauri": 'minor:feat' +--- + +Update rfd to 0.11. + diff --git a/.changes/run-event-opened.md b/.changes/run-event-opened.md new file mode 100644 index 00000000000..99c845802c4 --- /dev/null +++ b/.changes/run-event-opened.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:feat +--- + +Expose `RunEvent::Opened` on macOS and iOS for deep link support. diff --git a/.changes/run-mobile-plugin.md b/.changes/run-mobile-plugin.md new file mode 100644 index 00000000000..1d18a0b48b0 --- /dev/null +++ b/.changes/run-mobile-plugin.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Added `App::run_mobile_plugin` and `AppHandle::run_mobile_plugin`. diff --git a/.changes/runtime-create-window-handler.md b/.changes/runtime-create-window-handler.md new file mode 100644 index 00000000000..ab624f8e450 --- /dev/null +++ b/.changes/runtime-create-window-handler.md @@ -0,0 +1,6 @@ +--- +'tauri-runtime': 'minor:breaking' +'tauri-runtime-wry': 'minor:breaking' +--- + +`Dispatch::create_window`, `Runtime::create_window` and `RuntimeHandle::create_window` has been changed to accept a 3rd parameter which is a closure that takes `RawWindow` and to be executed right after the window is created and before the webview is added to the window. diff --git a/.changes/runtime-defaultvbox.md b/.changes/runtime-defaultvbox.md new file mode 100644 index 00000000000..51c38df2535 --- /dev/null +++ b/.changes/runtime-defaultvbox.md @@ -0,0 +1,5 @@ +--- +'tauri-runtime-wry': 'minor:feat' +--- + +Add `Dispatch::default_vbox` diff --git a/.changes/runtime-menu-system-tray.md b/.changes/runtime-menu-system-tray.md new file mode 100644 index 00000000000..49102235610 --- /dev/null +++ b/.changes/runtime-menu-system-tray.md @@ -0,0 +1,6 @@ +--- +'tauri-runtime': 'major:breaking' +'tauri-runtime-wry': 'major:breaking' +--- + +System tray and menu related APIs and structs have all been removed and are now implemented in tauri outside of the runtime-space. diff --git a/.changes/runtime-monitor.md b/.changes/runtime-monitor.md new file mode 100644 index 00000000000..91f1f764e0f --- /dev/null +++ b/.changes/runtime-monitor.md @@ -0,0 +1,6 @@ +--- +"tauri-runtime": "minor:feat" +"tauri-runtime-wry": "minor:feat" +--- + +Added `primary_monitor` and `available_monitors` to `Runtime` and `RuntimeHandle`. diff --git a/.changes/runtime-navigate-method.md b/.changes/runtime-navigate-method.md new file mode 100644 index 00000000000..0f907aa7dd4 --- /dev/null +++ b/.changes/runtime-navigate-method.md @@ -0,0 +1,5 @@ +--- +"tauri-runtime": 'minor:feat' +--- + +Added `navigate` function to `Dispatch` trait. diff --git a/.changes/runtime-navigation-handler-url-arg.md b/.changes/runtime-navigation-handler-url-arg.md new file mode 100644 index 00000000000..b6f48f6893a --- /dev/null +++ b/.changes/runtime-navigation-handler-url-arg.md @@ -0,0 +1,5 @@ +--- +"tauri-runtime": patch:breaking +--- + +The `PendingWindow#navigation_handler` closure now receives a `&Url` argument instead of `Url`. diff --git a/.changes/runtime-new-args.md b/.changes/runtime-new-args.md new file mode 100644 index 00000000000..e5fc889aff3 --- /dev/null +++ b/.changes/runtime-new-args.md @@ -0,0 +1,6 @@ +--- +'tauri-runtime': 'minor:breaking' +'tauri-runtime-wry': 'minor:breaking' +--- + +`Runtime::new` and `Runtime::new_any_thread` now accept a `RuntimeInitArgs`. diff --git a/.changes/runtime-opened-event.md b/.changes/runtime-opened-event.md new file mode 100644 index 00000000000..81a1ed9eef0 --- /dev/null +++ b/.changes/runtime-opened-event.md @@ -0,0 +1,6 @@ +--- +"tauri-runtime": minor:feat +"tauri-runtime-wry": minor:feat +--- + +Added the `Opened` variant to `RunEvent`. diff --git a/.changes/safepathbuf-refactor.md b/.changes/safepathbuf-refactor.md new file mode 100644 index 00000000000..c915f36345a --- /dev/null +++ b/.changes/safepathbuf-refactor.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Expose `SafePathBuf` type in `tauri::path`. diff --git a/.changes/shadow-api.md b/.changes/shadow-api.md new file mode 100644 index 00000000000..01b7797aad5 --- /dev/null +++ b/.changes/shadow-api.md @@ -0,0 +1,5 @@ +--- +'@tauri-apps/api': 'minor:feat' +--- + +Added the `shadow` option when creating a window and `setShadow` function. diff --git a/.changes/shadow-config.md b/.changes/shadow-config.md new file mode 100644 index 00000000000..6d01ed00d71 --- /dev/null +++ b/.changes/shadow-config.md @@ -0,0 +1,5 @@ +--- +'tauri-utils': 'minor:feat' +--- + +Added the `shadow` option to the window configuration and `set_shadow` option to the `window` allow list. diff --git a/.changes/shadow.md b/.changes/shadow.md new file mode 100644 index 00000000000..1eb40dda020 --- /dev/null +++ b/.changes/shadow.md @@ -0,0 +1,7 @@ +--- +'tauri': 'minor:feat' +'tauri-runtime-wry': 'minor:feat' +'tauri-runtime': 'minor:feat' +--- + +Added the `shadow` option when creating a window and `Window::set_shadow`. diff --git a/.changes/shadows-default-on.md b/.changes/shadows-default-on.md new file mode 100644 index 00000000000..e46ed93a8ad --- /dev/null +++ b/.changes/shadows-default-on.md @@ -0,0 +1,5 @@ +--- +'tauri': 'patch:enhance' +--- + +Enable shadows by default. diff --git a/.changes/simplify-ios-plugin-init-fn.md b/.changes/simplify-ios-plugin-init-fn.md new file mode 100644 index 00000000000..099d9e11878 --- /dev/null +++ b/.changes/simplify-ios-plugin-init-fn.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Change iOS plugin init function signature to `func init_plugin() -> Plugin`. diff --git a/.changes/skip-target-install-arg.md b/.changes/skip-target-install-arg.md new file mode 100644 index 00000000000..98e96702cf6 --- /dev/null +++ b/.changes/skip-target-install-arg.md @@ -0,0 +1,6 @@ +--- +'tauri-cli': 'patch:enhance' +'@tauri-apps/cli': 'patch:enhance' +--- + +Add `--skip-targets-install` flag for `tauri android init` and `tauri ios init` to skip installing needed rust targets vie rustup. diff --git a/.changes/state-0.6.md b/.changes/state-0.6.md new file mode 100644 index 00000000000..7efc9c0f0c8 --- /dev/null +++ b/.changes/state-0.6.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:deps +--- + +Update `state` to v0.6. diff --git a/.changes/submenu-and-menu-builders-item-and-id.md b/.changes/submenu-and-menu-builders-item-and-id.md new file mode 100644 index 00000000000..37889a40e5f --- /dev/null +++ b/.changes/submenu-and-menu-builders-item-and-id.md @@ -0,0 +1,5 @@ +--- +'tauri': 'patch:breaking' +--- + +Changed `MenuBuilder\SubmenuBuilder::text`, `MenuBuilder\SubmenuBuilder::check`, `MenuBuilder\SubmenuBuilder::icon` and `MenuBuilder\SubmenuBuilder::native_icon` to take an `id` as the first argument. diff --git a/.changes/system-tray-feat.md b/.changes/system-tray-feat.md new file mode 100644 index 00000000000..2516e4e432d --- /dev/null +++ b/.changes/system-tray-feat.md @@ -0,0 +1,6 @@ +--- +'tauri-runtime': 'major:breaking' +'tauri-runtime-wry': 'major:breaking' +--- + +Removed `system-tray` feature flag diff --git a/.changes/target-dir-detection.md b/.changes/target-dir-detection.md new file mode 100644 index 00000000000..c77b1adb7d3 --- /dev/null +++ b/.changes/target-dir-detection.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Fix target directory detection when compiling for Android. diff --git a/.changes/tauri-app-handle-ref.md b/.changes/tauri-app-handle-ref.md new file mode 100644 index 00000000000..7afc2171ea6 --- /dev/null +++ b/.changes/tauri-app-handle-ref.md @@ -0,0 +1,5 @@ +--- +'tauri': 'major:breaking' +--- + +Changed `App::handle` and `Manager::app_handle` to return a reference to an `AppHandle` instead of an owned value. diff --git a/.changes/tauri-build-mobile.md b/.changes/tauri-build-mobile.md new file mode 100644 index 00000000000..4e55d09cfd3 --- /dev/null +++ b/.changes/tauri-build-mobile.md @@ -0,0 +1,5 @@ +--- +"tauri-build": 'minor:feat' +--- + +Add `mobile::PluginBuilder` for running build tasks related to Tauri plugins. diff --git a/.changes/tauri-cleanup-before-exit.md b/.changes/tauri-cleanup-before-exit.md new file mode 100644 index 00000000000..ff99c9215f4 --- /dev/null +++ b/.changes/tauri-cleanup-before-exit.md @@ -0,0 +1,5 @@ +--- +'tauri': 'minor:feat' +--- + +Add `App::cleanup_before_exit` and `AppHandle::cleanup_before_exit` to manually call the cleanup logic. **You should always exit the tauri app immediately after this function returns and not use any tauri-related APIs.** diff --git a/.changes/tauri-defaultvbox.md b/.changes/tauri-defaultvbox.md new file mode 100644 index 00000000000..967fb66f440 --- /dev/null +++ b/.changes/tauri-defaultvbox.md @@ -0,0 +1,5 @@ +--- +'tauri': 'minor:feat' +--- + +On Linux, add `Window::default_vbox` to get a reference to the `gtk::Box` that contains the menu bar and the webview. diff --git a/.changes/tauri-libxdo-feat.md b/.changes/tauri-libxdo-feat.md new file mode 100644 index 00000000000..648376946e6 --- /dev/null +++ b/.changes/tauri-libxdo-feat.md @@ -0,0 +1,5 @@ +--- +'tauri': 'minor:feat' +--- + +Add `linux-libxdo` feature flag (disabled by default) to enable linking to `libxdo` which is used to make `Cut`, `Copy`, `Paste` and `SelectAll` native menu items work on Linux. diff --git a/.changes/tauri-menu-tray-refactor.md b/.changes/tauri-menu-tray-refactor.md new file mode 100644 index 00000000000..7c21970ce12 --- /dev/null +++ b/.changes/tauri-menu-tray-refactor.md @@ -0,0 +1,17 @@ +--- +'tauri': 'major:breaking' +--- + +The tray icon and menu have received a huge refactor with a lot of breaking changes in order to add new functionalities and improve the DX around using them and here is an overview of the changes: + +- All menu and tray types are now exported from `tauri::menu` and `tauri::tray` modules with new names so make sure to check the new types. +- Removed `tauri::Builder::system_tray`, instead you should use `tauri::tray::TrayIconBuilder` inside `tauri::Builder::setup` hook to create your tray icons. +- Changed `tauri::Builder::menu` to be a function to accomodate for new menu changes, you can passe `tauri::menu::Menu::default` to it to create a default menu. +- Renamed `tauri::Context` methods `system_tray_icon`, `tauri::Context::system_tray_icon_mut` and `tauri::Context::set_system_tray_icon` to `tauri::Context::tray_icon`, `tauri::Context::tray_icon_mut` and `tauri::Context::set_tray_icon` to be consistent with new type names. +- Added `RunEvent::MenuEvent` and `RunEvent::TrayIconEvent`. +- Added `App/AppHandle::set_menu`, `App/AppHandle::remove_menu`, `App/AppHandle::show_menu`, `App/AppHandle::hide_menu` and `App/AppHandle::menu` to access, remove, hide or show the app-wide menu that is used as the global menu on macOS and on all windows that don't have a specific menu set for it on Windows and Linux. +- Added `Window::set_menu`, `Window::remove_menu`, `Window::show_menu`, `Window::hide_menu`, `Window::is_menu_visible` and `Window::menu` to access, remove, hide or show the menu on this window. +- Added `Window::popup_menu` and `Window::popup_menu_at` to show a context menu on the window at the cursor position or at a specific position. You can also popup a context menu using `popup` and `popup_at` methods from `ContextMenu` trait which is implemented for `Menu` and `Submenu` types. +- Added `App/AppHandle::tray`, `App/AppHandle::tray_by_id`, `App/AppHandle::remove_tray` and `App/AppHandle::remove_tray_by_id` to access or remove a registered tray. +- Added `WindowBuilder/App/AppHandle::on_menu_event` to register a new menu event handler. +- Added `App/AppHandle::on_tray_icon_event` to register a new tray event handler. diff --git a/.changes/tauri-mobile-entry-point.md b/.changes/tauri-mobile-entry-point.md new file mode 100644 index 00000000000..35e06db7188 --- /dev/null +++ b/.changes/tauri-mobile-entry-point.md @@ -0,0 +1,5 @@ +--- +"tauri": 'minor:feat' +--- + +Export types required by the `mobile_entry_point` macro. diff --git a/.changes/tauri-nsview.md b/.changes/tauri-nsview.md new file mode 100644 index 00000000000..e50bfb0b2eb --- /dev/null +++ b/.changes/tauri-nsview.md @@ -0,0 +1,5 @@ +--- +'tauri': 'minor:feat' +--- + +On macOS, add `Window::ns_view` to get a pointer to the NSWindow content view. diff --git a/.changes/tauri-run_on_main_thread.md b/.changes/tauri-run_on_main_thread.md new file mode 100644 index 00000000000..a9bbc7a296c --- /dev/null +++ b/.changes/tauri-run_on_main_thread.md @@ -0,0 +1,5 @@ +--- +'tauri': 'minor:feat' +--- + +Expose `run_on_main_thread` method on `App` that is similar to `AppHandle::run_on_main_thread`. diff --git a/.changes/tauri-runtime-wry-wry-0-28.md b/.changes/tauri-runtime-wry-wry-0-28.md new file mode 100644 index 00000000000..6225b352b35 --- /dev/null +++ b/.changes/tauri-runtime-wry-wry-0-28.md @@ -0,0 +1,5 @@ +--- +'tauri-runtime-wry': 'patch:enhance' +--- + +Update `wry` to `0.28` diff --git a/.changes/tauri-tray-icon-feat-flag.md b/.changes/tauri-tray-icon-feat-flag.md new file mode 100644 index 00000000000..7bbad7a6594 --- /dev/null +++ b/.changes/tauri-tray-icon-feat-flag.md @@ -0,0 +1,5 @@ +--- +'tauri': 'major:breaking' +--- + +Renamed `system-tray` feature flag to `tray-icon`. diff --git a/.changes/tempdir-api.md b/.changes/tempdir-api.md new file mode 100644 index 00000000000..6a16a157a26 --- /dev/null +++ b/.changes/tempdir-api.md @@ -0,0 +1,5 @@ +--- +'@tauri-apps/api': 'patch:enhance' +--- + +Add `tempDir` function to `path` module diff --git a/.changes/tempdir-core.md b/.changes/tempdir-core.md new file mode 100644 index 00000000000..e16b80f128f --- /dev/null +++ b/.changes/tempdir-core.md @@ -0,0 +1,5 @@ +--- +'tauri': 'patch:enhance' +--- + +Add `temp_dir` method to `PathResolver` diff --git a/.changes/tls-features-automatically-enabled.md b/.changes/tls-features-automatically-enabled.md new file mode 100644 index 00000000000..e8629a83903 --- /dev/null +++ b/.changes/tls-features-automatically-enabled.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": 'patch:enhance' +"@tauri-apps/cli": 'patch:enhance' +--- + +Automatically enable the `rustls-tls` tauri feature on mobile and `native-tls` on desktop if `rustls-tls` is not enabled. diff --git a/.changes/tls-features-refactor.md b/.changes/tls-features-refactor.md new file mode 100644 index 00000000000..8dd54f54891 --- /dev/null +++ b/.changes/tls-features-refactor.md @@ -0,0 +1,5 @@ +--- +"tauri": 'patch:enhance' +--- + +Renamed the `default-tls` feature to `native-tls` and added `rustls-tls` feature. diff --git a/.changes/ubuntu-20.04-cli.js.md b/.changes/ubuntu-20.04-cli.js.md new file mode 100644 index 00000000000..640558ce4c4 --- /dev/null +++ b/.changes/ubuntu-20.04-cli.js.md @@ -0,0 +1,5 @@ +--- +"@tauri-apps/cli": 'patch:enhance' +--- + +Use Ubuntu 20.04 to compile the CLI, increasing the minimum libc version required. diff --git a/.changes/ubuntu-20.04-cli.rs.md b/.changes/ubuntu-20.04-cli.rs.md new file mode 100644 index 00000000000..58f0adc7666 --- /dev/null +++ b/.changes/ubuntu-20.04-cli.rs.md @@ -0,0 +1,5 @@ +--- +"tauri-cli": 'patch:enhance' +--- + +- Use Ubuntu 20.04 to compile the CLI for cargo-binstall, increasing the minimum libc required. diff --git a/.changes/update-entitlements-api.md b/.changes/update-entitlements-api.md new file mode 100644 index 00000000000..ccf53a63edf --- /dev/null +++ b/.changes/update-entitlements-api.md @@ -0,0 +1,5 @@ +--- +"tauri-build": patch:feat +--- + +Added the `mobile::update_entitlements` function for iOS. diff --git a/.changes/window-effects-api.md b/.changes/window-effects-api.md new file mode 100644 index 00000000000..44e25bb01da --- /dev/null +++ b/.changes/window-effects-api.md @@ -0,0 +1,5 @@ +--- +'@tauri-apps/api': 'minor:feat' +--- + +Added the `windowEffects` option when creating a window and `setWindowEffects` method to change it at runtime. diff --git a/.changes/window-effects-config.md b/.changes/window-effects-config.md new file mode 100644 index 00000000000..94177132420 --- /dev/null +++ b/.changes/window-effects-config.md @@ -0,0 +1,5 @@ +--- +'tauri-utils': 'minor:feat' +--- + +Added the `window_effects` option to the window configuration. diff --git a/.changes/window-effects.md b/.changes/window-effects.md new file mode 100644 index 00000000000..f5f7678071c --- /dev/null +++ b/.changes/window-effects.md @@ -0,0 +1,7 @@ +--- +'tauri': 'minor:feat' +'tauri-runtime-wry': 'minor:feat' +'tauri-runtime': 'minor:feat' +--- + +Added the `window_effects` option when creating a window and `Window::set_effects` to change it at runtime. diff --git a/.changes/window-on-navigation-arg.md b/.changes/window-on-navigation-arg.md new file mode 100644 index 00000000000..79ebb0e7eca --- /dev/null +++ b/.changes/window-on-navigation-arg.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:breaking +--- + +The `Window#on_navigation` closure now receives a `&Url` argument instead of `Url`. diff --git a/.changes/with-webview.md b/.changes/with-webview.md new file mode 100644 index 00000000000..4b1e8a575b5 --- /dev/null +++ b/.changes/with-webview.md @@ -0,0 +1,7 @@ +--- +"tauri": 'minor:feat' +"tauri-runtime": 'minor:feat' +"tauri-runtime-wry": 'minor:feat' +--- + +Implemented `with_webview` on Android and iOS. diff --git a/.changes/wry-navigate-method.md b/.changes/wry-navigate-method.md new file mode 100644 index 00000000000..ca7aa7bb250 --- /dev/null +++ b/.changes/wry-navigate-method.md @@ -0,0 +1,5 @@ +--- +"tauri-runtime-wry": 'minor:feat' +--- + +Implement navigate method diff --git a/.changes/wry26.md b/.changes/wry26.md new file mode 100644 index 00000000000..dee422188ea --- /dev/null +++ b/.changes/wry26.md @@ -0,0 +1,6 @@ +--- +"tauri-runtime-wry": 'minor:feat' +--- + +Update wry to 0.26. + diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 0435c8cf7b0..63d7bcd3ae6 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -6,7 +6,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT} # Derived from Tauri contribution and setup guides: # See: https://github.com/tauri-apps/tauri/blob/dev/.github/CONTRIBUTING.md#development-guide # See: https://tauri.app/v1/guides/getting-started/prerequisites/#setting-up-linux -ARG TAURI_BUILD_DEPS="build-essential curl libappindicator3-dev libgtk-3-dev librsvg2-dev libssl-dev libwebkit2gtk-4.0-dev wget" +ARG TAURI_BUILD_DEPS="build-essential curl libappindicator3-dev libgtk-3-dev librsvg2-dev libssl-dev libwebkit2gtk-4.1-dev wget" RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get install -y --no-install-recommends $TAURI_BUILD_DEPS diff --git a/.docker/cross/aarch64.Dockerfile b/.docker/cross/aarch64.Dockerfile index 6dfb5bdaa71..498d055b75f 100644 --- a/.docker/cross/aarch64.Dockerfile +++ b/.docker/cross/aarch64.Dockerfile @@ -41,4 +41,4 @@ ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \ RUN dpkg --add-architecture arm64 RUN apt-get update -RUN apt-get install --assume-yes --no-install-recommends libssl-dev:arm64 libdbus-1-dev:arm64 libsoup2.4-dev:arm64 libssl-dev:arm64 libgtk-3-dev:arm64 webkit2gtk-4.0-dev:arm64 libappindicator3-1:arm64 librsvg2-dev:arm64 patchelf:arm64 +RUN apt-get install --assume-yes --no-install-recommends libssl-dev:arm64 libdbus-1-dev:arm64 libsoup2.4-dev:arm64 libssl-dev:arm64 libgtk-3-dev:arm64 webkit2gtk-4.1-dev:arm64 libappindicator3-1:arm64 librsvg2-dev:arm64 patchelf:arm64 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5ddc5cd3034..cdf2d5c619c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -71,7 +71,7 @@ The code for the bundler is located in `[Tauri repo root]/tooling/bundler`, and ### Developing Tauri Core and Related Components (Rust API, Macros, Codegen, and Utils) -The code for Tauri Core is located in `[Tauri repo root]/core/tauri`, and the Rust API, Macros, and Utils are in `[Tauri repo root]/core/tauri-(api/macros/utils)`. The easiest way to test your changes is to use the `[Tauri repo root]/examples/helloworld` app. It automatically rebuilds and uses your local copy of the Tauri core packages. Just run `yarn tauri build` or `yarn tauri dev` in the helloworld app directory after making changes to test them out. To use your local changes in another project, edit its `src-tauri/Cargo.toml` file so that the `tauri` key looks like `tauri = { path = "PATH", features = [ "api-all", "cli" ] }`, where `PATH` is the relative path to `[Tauri repo root]/core/tauri`. Then, your local copy of the Tauri core packages will be rebuilt and used whenever you build that project. +The code for Tauri Core is located in `[Tauri repo root]/core/tauri`, and the Rust API, Macros, and Utils are in `[Tauri repo root]/core/tauri-(api/macros/utils)`. The easiest way to test your changes is to use the `[Tauri repo root]/examples/helloworld` app. It automatically rebuilds and uses your local copy of the Tauri core packages. Just run `yarn tauri build` or `yarn tauri dev` in the helloworld app directory after making changes to test them out. To use your local changes in another project, edit its `src-tauri/Cargo.toml` file so that the `tauri` key looks like `tauri = { path = "PATH" }`, where `PATH` is the relative path to `[Tauri repo root]/core/tauri`. Then, your local copy of the Tauri core packages will be rebuilt and used whenever you build that project. #### Building the documentation locally diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index ebe094d6057..f84f7b5afb7 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -52,7 +52,7 @@ jobs: run: | python -m pip install --upgrade pip sudo apt-get update - sudo apt-get install -y webkit2gtk-4.0 libayatana-appindicator3-dev xvfb + sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev xvfb wget https://github.com/sharkdp/hyperfine/releases/download/v1.11.0/hyperfine_1.11.0_amd64.deb sudo dpkg -i hyperfine_1.11.0_amd64.deb pip install memory_profiler diff --git a/.github/workflows/covector-version-or-publish.yml b/.github/workflows/covector-version-or-publish.yml index 4be13c60069..bb7155e9b1c 100644 --- a/.github/workflows/covector-version-or-publish.yml +++ b/.github/workflows/covector-version-or-publish.yml @@ -7,7 +7,7 @@ name: covector version or publish on: push: branches: - - 1.x + - dev jobs: run-integration-tests: @@ -30,7 +30,7 @@ jobs: if: matrix.platform == 'ubuntu-latest' run: | sudo apt-get update - sudo apt-get install -y webkit2gtk-4.0 libayatana-appindicator3-dev libfuse2 + sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev libfuse2 - uses: Swatinem/rust-cache@v2 with: @@ -99,8 +99,8 @@ jobs: uses: tauri-apps/create-pull-request@v3 with: token: ${{ secrets.GITHUB_TOKEN }} - branch: release/version-updates-v1 - title: Apply Version Updates From Current Changes (v1) + branch: release/version-updates + title: Apply Version Updates From Current Changes commit-message: 'apply version updates' labels: 'version updates' body: ${{ steps.covector.outputs.change }} @@ -121,7 +121,7 @@ jobs: steps.covector.outputs.successfulPublish == 'true' && contains(steps.covector.outputs.packagesPublished, '@tauri-apps/cli') run: | - echo '${{ steps.covector.outputs }}' > output.json + echo '${{ toJSON(steps.covector.outputs) }}' > output.json id=$(jq '.["-tauri-apps-cli-releaseId"]' < output.json) rm output.json echo "cliReleaseId=$id" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index fd02565f0e0..f9b066b5dc8 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -115,7 +115,7 @@ jobs: - name: install dependencies run: | sudo apt-get update - sudo apt-get install -y webkit2gtk-4.0 libayatana-appindicator3-dev + sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev - name: Test run: | diff --git a/.github/workflows/lint-core.yml b/.github/workflows/lint-core.yml index d3004dee68c..fb1251bb2ac 100644 --- a/.github/workflows/lint-core.yml +++ b/.github/workflows/lint-core.yml @@ -50,11 +50,10 @@ jobs: clippy: - { args: '', key: 'empty' } - { - args: '--features compression,wry,linux-protocol-headers,isolation,custom-protocol,api-all,cli,updater,system-tray,windows7-compat,http-multipart,test', + args: '--features compression,wry,linux-ipc-protocol,isolation,custom-protocol,tray-icon,test', key: 'all' } - { args: '--features custom-protocol', key: 'custom-protocol' } - - { args: '--features api-all', key: 'api-all' } steps: - uses: actions/checkout@v2 @@ -62,7 +61,7 @@ jobs: - name: install dependencies run: | sudo apt-get update - sudo apt-get install -y webkit2gtk-4.0 libayatana-appindicator3-dev + sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev - uses: actions-rs/toolchain@v1 with: diff --git a/.github/workflows/publish-cli-js.yml b/.github/workflows/publish-cli-js.yml index 4a02dcbd403..d5e690706dd 100644 --- a/.github/workflows/publish-cli-js.yml +++ b/.github/workflows/publish-cli-js.yml @@ -43,7 +43,7 @@ jobs: - host: windows-latest architecture: x64 target: aarch64-pc-windows-msvc - build: yarn build:release --target aarch64-pc-windows-msvc --features native-tls,native-tls-vendored --cargo-flags="--no-default-features" + build: yarn build:release --target aarch64-pc-windows-msvc --features native-tls-vendored --cargo-flags="--no-default-features" - host: ubuntu-20.04 target: x86_64-unknown-linux-gnu docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian @@ -61,7 +61,7 @@ jobs: - host: macos-latest target: aarch64-apple-darwin build: | - yarn build:release --target=aarch64-apple-darwin + yarn build:release --features native-tls-vendored --target=aarch64-apple-darwin strip -x *.node - host: ubuntu-20.04 target: aarch64-unknown-linux-gnu @@ -261,7 +261,7 @@ jobs: - name: install system dependencies run: | sudo apt-get update - sudo apt-get install -y webkit2gtk-4.0 libayatana-appindicator3-dev + sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev - name: Test bindings run: yarn test test-linux-x64-musl-binding: @@ -355,7 +355,7 @@ jobs: set -e export PATH=/usr/local/cargo/bin/:/usr/local/fnm:$PATH apt-get update - DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get install --no-install-recommends -y unzip webkit2gtk-4.0 libayatana-appindicator3-dev + DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get install --no-install-recommends -y unzip libayatana-appindicator3-dev bash curl https://sh.rustup.rs -sSf | bash -s -- -y curl -fsSL https://fnm.vercel.app/install | bash -s -- --install-dir "/usr/local/fnm" --skip-shell @@ -397,8 +397,8 @@ jobs: - name: Publish run: | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc - npm publish + npm publish --tag next env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.ORG_NPM_TOKEN }} - RELEASE_ID: ${{ github.event.client_payload.releaseId || github.event.inputs.releaseId }} + RELEASE_ID: ${{ github.event.client_payload.releaseId || inputs.releaseId }} diff --git a/.github/workflows/publish-cli-rs.yml b/.github/workflows/publish-cli-rs.yml index d7d6df21640..57b9a376403 100644 --- a/.github/workflows/publish-cli-rs.yml +++ b/.github/workflows/publish-cli-rs.yml @@ -37,11 +37,10 @@ jobs: - os: windows-latest rust_target: aarch64-pc-windows-msvc ext: '.exe' - args: '--no-default-features --features native-tls,native-tls-vendored' + args: '--no-default-features --features native-tls-vendored' steps: - - name: Checkout - uses: actions/checkout@v3 + - uses: actions/checkout@v3 - name: 'Setup Rust' uses: dtolnay/rust-toolchain@stable @@ -62,13 +61,13 @@ jobs: uses: actions-rs/cargo@v1 with: command: build - args: --manifest-path ./tooling/cli/Cargo.toml --release ${{ matrix.config.args }} + args: --manifest-path ./tooling/cli/Cargo.toml --profile release-size-optimized ${{ matrix.config.args }} - name: Upload CLI uses: actions/upload-artifact@v3 with: name: cargo-tauri-${{ matrix.config.rust_target }}${{ matrix.config.ext }} - path: tooling/cli/target/release/cargo-tauri${{ matrix.config.ext }} + path: tooling/cli/target/release-size-optimized/cargo-tauri${{ matrix.config.ext }} if-no-files-found: error upload: @@ -88,7 +87,7 @@ jobs: run: ./.scripts/ci/pack-cli.sh - name: Get CLI version - run: echo "CLI_VERSION=$(cat tooling/cli/metadata.json | jq '."@tauri-apps/cli".version' -r)" >> $GITHUB_ENV + run: echo "CLI_VERSION=$(cat tooling/cli/metadata-v2.json | jq '."cli.js".version' -r)" >> $GITHUB_ENV - name: Publish release uses: softprops/action-gh-release@50195ba7f6f93d1ac97ba8332a178e008ad176aa diff --git a/.github/workflows/test-android.yml b/.github/workflows/test-android.yml new file mode 100644 index 00000000000..ea54d2617b4 --- /dev/null +++ b/.github/workflows/test-android.yml @@ -0,0 +1,100 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: test android + +on: + pull_request: + paths: + - '.github/workflows/test-android.yml' + - 'tooling/cli/templates/mobile/android/**' + - 'tooling/cli/src/mobile/**' + - '!tooling/cli/src/mobile/ios.rs' + - '!tooling/cli/src/mobile/ios/**' + - 'core/tauri-build/src/mobile.rs' + - 'core/tauri/mobile/android/**' + - 'core/tauri/mobile/android-codegen/**' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ${{ matrix.platform }} + + strategy: + fail-fast: false + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v3 + + - name: install Rust stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + + - name: install Linux dependencies + if: matrix.platform == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev webkit2gtk-4.1 + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: yarn + cache-dependency-path: | + tooling/api/yarn.lock + examples/api/yarn.lock + + - uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 17 + cache: gradle + + - name: Setup NDK + uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r25b + local-cache: true + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: | + tooling/cli + examples/api/src-tauri + + - name: build CLI + uses: actions-rs/cargo@v1 + with: + command: build + args: --manifest-path ./tooling/cli/Cargo.toml + + - name: build Tauri API + working-directory: ./tooling/api + run: yarn && yarn build + + - name: install API example dependencies + working-directory: ./examples/api + run: yarn + + - name: init Android Studio project + working-directory: ./examples/api + run: ../../tooling/cli/target/debug/cargo-tauri android init + env: + NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + + - name: build APK + working-directory: ./examples/api + run: ../../tooling/cli/target/debug/cargo-tauri android build + env: + NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} diff --git a/.github/workflows/test-cli-js.yml b/.github/workflows/test-cli-js.yml index 3a744a218fa..6668a23e4af 100644 --- a/.github/workflows/test-cli-js.yml +++ b/.github/workflows/test-cli-js.yml @@ -51,7 +51,7 @@ jobs: if: matrix.platform == 'ubuntu-latest' run: | sudo apt-get update - sudo apt-get install -y webkit2gtk-4.0 libayatana-appindicator3-dev + sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev - uses: Swatinem/rust-cache@v2 with: diff --git a/.github/workflows/test-cli-rs.yml b/.github/workflows/test-cli-rs.yml index 26e98e82058..e906f74dd44 100644 --- a/.github/workflows/test-cli-rs.yml +++ b/.github/workflows/test-cli-rs.yml @@ -24,24 +24,40 @@ concurrency: jobs: test: - runs-on: ${{ matrix.platform }} + runs-on: ${{ matrix.platform.os }} strategy: fail-fast: false matrix: - platform: [ubuntu-latest, macos-latest, windows-latest] + platform: + - { + target: x86_64-pc-windows-msvc, + os: windows-latest + } + - { + target: aarch64-pc-windows-msvc, + os: windows-latest, + args: --no-default-features --features native-tls-vendored + } + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-latest + } + - { + target: x86_64-apple-darwin, + os: macos-latest + } steps: - uses: actions/checkout@v2 - - name: install stable - uses: actions-rs/toolchain@v1 + - name: 'Setup Rust' + uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - override: true + targets: ${{ matrix.platform.target }} - name: install Linux dependencies - if: matrix.platform == 'ubuntu-latest' + if: matrix.platform.os == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y libgtk-3-dev @@ -54,4 +70,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: build - args: --manifest-path ./tooling/cli/Cargo.toml + args: --manifest-path ./tooling/cli/Cargo.toml ${{ matrix.platform.args }} diff --git a/.github/workflows/test-core.yml b/.github/workflows/test-core.yml index 65765256729..3fe76186e77 100644 --- a/.github/workflows/test-core.yml +++ b/.github/workflows/test-core.yml @@ -34,17 +34,37 @@ jobs: - { target: x86_64-pc-windows-msvc, os: windows-latest, - toolchain: '1.61.0' + toolchain: '1.65.0', + cross: false, + command: 'test' } - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, - toolchain: '1.60.0' + toolchain: '1.65.0', + cross: false, + command: 'test' } - { target: x86_64-apple-darwin, os: macos-latest, - toolchain: '1.60.0' + toolchain: '1.65.0', + cross: false, + command: 'test' + } + - { + target: aarch64-apple-ios, + os: macos-latest, + toolchain: '1.65.0', + cross: false, + command: 'build' + } + - { + target: aarch64-linux-android, + os: ubuntu-latest, + toolchain: '1.65.0', + cross: true, + command: 'build' } features: - { @@ -52,11 +72,7 @@ jobs: key: no-default } - { - args: --features api-all, - key: api-all - } - - { - args: --features compression,wry,linux-protocol-headers,isolation,custom-protocol,api-all,cli,updater,system-tray,windows7-compat,http-multipart,test, + args: --features compression,wry,linux-ipc-protocol,isolation,custom-protocol,tray-icon,test, key: all } @@ -75,7 +91,7 @@ jobs: if: contains(matrix.platform.target, 'unknown-linux') run: | sudo apt-get update - sudo apt-get install -y webkit2gtk-4.0 libayatana-appindicator3-dev + sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev - uses: Swatinem/rust-cache@v2 with: @@ -85,15 +101,11 @@ jobs: - name: Downgrade crates with MSRV conflict # The --precise flag can only be used once per invocation. run: | - cargo update -p toml:0.7.4 --precise 0.7.3 - cargo update -p toml_edit --precise 0.19.8 - cargo update -p toml_datetime --precise 0.6.1 - cargo update -p serde_spanned --precise 0.6.1 - cargo update -p winnow --precise 0.4.1 - cargo update -p time --precise 0.3.15 - cargo update -p ignore --precise 0.4.18 - cargo update -p raw-window-handle --precise 0.5.0 - cargo update -p cargo_toml:0.15.3 --precise 0.15.2 + cargo update -p time --precise 0.3.23 - name: test - run: cargo test --target ${{ matrix.platform.target }} ${{ matrix.features.args }} + uses: actions-rs/cargo@v1 + with: + use-cross: ${{ matrix.platform.cross }} + command: ${{ matrix.platform.command }} + args: --target ${{ matrix.platform.target }} ${{ matrix.features.args }} diff --git a/.github/workflows/test-updater-artifacts.yml b/.github/workflows/test-updater-artifacts.yml deleted file mode 100644 index dbe6d00ab7c..00000000000 --- a/.github/workflows/test-updater-artifacts.yml +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2019-2023 Tauri Programme within The Commons Conservancy -# SPDX-License-Identifier: Apache-2.0 -# SPDX-License-Identifier: MIT - -name: test updater artifacts -on: - schedule: - - cron: '0 0 * * *' - pull_request: - paths: - - '.github/workflows/test-updater-artifacts.yml' - - 'examples/updater/**' - workflow_dispatch: - -env: - RUST_BACKTRACE: 1 - CARGO_PROFILE_DEV_DEBUG: 0 # This would add unnecessary bloat to the target folder, decreasing cache efficiency. - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - test: - runs-on: ${{ matrix.platform }} - - strategy: - fail-fast: false - matrix: - platform: [ubuntu-latest, macos-latest, windows-latest] - - steps: - - uses: actions/checkout@v2 - - name: install stable - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - - name: install Linux dependencies - if: matrix.platform == 'ubuntu-latest' - run: | - sudo apt-get update - sudo apt-get install -y webkit2gtk-4.0 libayatana-appindicator3-dev - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: | - core -> ../target - tooling/cli - - - name: build and install `tauri-cli` - run: cargo install --path tooling/cli --force - - name: Check whether code signing should be enabled - id: enablecodesigning - env: - ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }} - run: | - echo "Enable code signing: ${{ env.ENABLE_CODE_SIGNING != '' }}" - echo "::set-output name=enabled::${{ env.ENABLE_CODE_SIGNING != '' }}" - - # run only on tauri-apps/tauri repo (require secrets) - - name: build sample artifacts + code signing (updater) - if: steps.enablecodesigning.outputs.enabled == 'true' - working-directory: ./examples/updater - run: | - yarn install - cargo tauri build --verbose - env: - # Notarization (disabled) - # FIXME: enable only on `dev` push maybe? as it take some times... - # - # APPLE_ID: ${{ secrets.APPLE_ID }} - # APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - - # Apple code signing testing - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - # Updater signature is exposed here to make sure it works in PR's - TAURI_PRIVATE_KEY: dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5YTBGV3JiTy9lRDZVd3NkL0RoQ1htZmExNDd3RmJaNmRMT1ZGVjczWTBKZ0FBQkFBQUFBQUFBQUFBQUlBQUFBQWdMekUzVkE4K0tWQ1hjeGt1Vkx2QnRUR3pzQjVuV0ZpM2czWXNkRm9hVUxrVnB6TUN3K1NheHJMREhQbUVWVFZRK3NIL1VsMDBHNW5ET1EzQno0UStSb21nRW4vZlpTaXIwZFh5ZmRlL1lSN0dKcHdyOUVPclVvdzFhVkxDVnZrbHM2T1o4Tk1NWEU9Cg== - TAURI_KEY_PASSWORD: - # run on PRs and forks - - name: build sample artifacts (updater) - if: steps.enablecodesigning.outputs.enabled != 'true' - working-directory: ./examples/updater - run: | - yarn install - cargo tauri build --verbose - env: - TAURI_PRIVATE_KEY: dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5YTBGV3JiTy9lRDZVd3NkL0RoQ1htZmExNDd3RmJaNmRMT1ZGVjczWTBKZ0FBQkFBQUFBQUFBQUFBQUlBQUFBQWdMekUzVkE4K0tWQ1hjeGt1Vkx2QnRUR3pzQjVuV0ZpM2czWXNkRm9hVUxrVnB6TUN3K1NheHJMREhQbUVWVFZRK3NIL1VsMDBHNW5ET1EzQno0UStSb21nRW4vZlpTaXIwZFh5ZmRlL1lSN0dKcHdyOUVPclVvdzFhVkxDVnZrbHM2T1o4Tk1NWEU9Cg== - TAURI_KEY_PASSWORD: - # upload assets - - uses: actions/upload-artifact@v2 - if: matrix.platform == 'ubuntu-latest' - with: - name: linux-updater-artifacts - path: ./examples/updater/src-tauri/target/release/bundle/appimage/updater-example_*.AppImage.* - - uses: actions/upload-artifact@v2 - if: matrix.platform == 'windows-latest' - with: - name: windows-updater-artifacts - path: ./examples/updater/src-tauri/target/release/bundle/msi/* - - uses: actions/upload-artifact@v2 - if: matrix.platform == 'macos-latest' - with: - name: macos-updater-artifacts - path: ./examples/updater/src-tauri/target/release/bundle/macos/updater-example.app.tar.* diff --git a/.github/workflows/udeps.yml b/.github/workflows/udeps.yml index e8ce33255f9..f3e4d75dee7 100644 --- a/.github/workflows/udeps.yml +++ b/.github/workflows/udeps.yml @@ -157,7 +157,7 @@ jobs: - name: Install required packages run: | sudo apt-get update - sudo apt-get install -y webkit2gtk-4.0 libayatana-appindicator3-dev + sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev - uses: actions-rs/cargo@v1 with: diff --git a/.scripts/ci/check-license-header.js b/.scripts/ci/check-license-header.js index ae3dfcb1edf..d0d6dc68f98 100644 --- a/.scripts/ci/check-license-header.js +++ b/.scripts/ci/check-license-header.js @@ -14,7 +14,7 @@ SPDX-License-Identifier: MIT` const bundlerLicense = '// Copyright 2016-2019 Cargo-Bundle developers ' -const extensions = ['.rs', '.js', '.ts', '.yml'] +const extensions = ['.rs', '.js', '.ts', '.yml', '.swift', '.kt'] const ignore = [ 'target', 'templates', @@ -26,7 +26,7 @@ const ignore = [ ] async function checkFile(file) { - if (extensions.some((e) => file.endsWith(e))) { + if (extensions.some((e) => file.endsWith(e)) && !ignore.some((i) => file.includes(`/${i}/`))) { const fileStream = fs.createReadStream(file) const rl = readline.createInterface({ input: fileStream, @@ -40,6 +40,7 @@ async function checkFile(file) { if ( line.length === 0 || line.startsWith('#!') || + line.startsWith('// swift-tools-version:') || line === bundlerLicense ) { continue diff --git a/.scripts/covector/sync-cli-metadata.js b/.scripts/covector/sync-cli-metadata.js index 79d540e89e0..26b7e41a684 100644 --- a/.scripts/covector/sync-cli-metadata.js +++ b/.scripts/covector/sync-cli-metadata.js @@ -12,17 +12,19 @@ rust binaries. */ const { readFileSync, writeFileSync } = require('fs') +const { resolve } = require('path') const packageNickname = process.argv[2] const filePath = packageNickname === '@tauri-apps/cli' - ? `../../../tooling/cli/metadata.json` - : `../../tooling/cli/metadata.json` + ? `../../../tooling/cli/metadata-v2.json` + : `../../tooling/cli/metadata-v2.json` const bump = process.argv[3] let index = null switch (bump) { case 'major': + case 'premajor': index = 0 break case 'minor': @@ -31,6 +33,9 @@ switch (bump) { case 'patch': index = 2 break + case 'prerelease': + index = 3 + break default: throw new Error('unexpected bump ' + bump) } @@ -44,6 +49,12 @@ const inc = (version) => { v[i] = 0 } } + if (bump === 'premajor') { + const pre = JSON.parse( + readFileSync(resolve(filePath, '../../../.changes/pre.json'), 'utf-8') + ) + return `${v.join('.')}-${pre.tag}.0` + } return v.join('.') } @@ -61,5 +72,5 @@ if (packageNickname === '@tauri-apps/cli') { } writeFileSync(filePath, JSON.stringify(metadata, null, 2) + '\n') -console.log(`wrote ${version} for ${packageNickname} into metadata.json`) +console.log(`wrote ${version} for ${packageNickname} into metadata-v2.json`) console.dir(metadata) diff --git a/.scripts/update-lockfiles.sh b/.scripts/update-lockfiles.sh index 9d71ba83034..961d0fb4c5a 100755 --- a/.scripts/update-lockfiles.sh +++ b/.scripts/update-lockfiles.sh @@ -4,7 +4,7 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: MIT -declare -a examples=("api" "sidecar" "updater" "resources" "tauri-dynamic-lib" "workspace") +declare -a examples=("api" "sidecar" "resources" "tauri-dynamic-lib" "workspace") declare -a tooling=("bench" "cli" "webdriver") for example in "${examples[@]}" diff --git a/Cargo.toml b/Cargo.toml index af5d14713f1..63ee69bc15d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,19 +12,26 @@ members = [ # integration tests "core/tests/restart", - "core/tests/app-updater", ] exclude = [ # examples that can be compiled with the tauri CLI "examples/api/src-tauri", - "examples/updater/src-tauri", "examples/resources/src-tauri", - "examples/sidecar/src-tauri", "examples/web/core", + "examples/file-associations/src-tauri", "examples/workspace", ] +[workspace.package] +authors = [ "Tauri Programme within The Commons Conservancy" ] +homepage = "https://tauri.app/" +repository = "https://github.com/tauri-apps/tauri" +categories = [ "gui", "web-programming" ] +license = "Apache-2.0 OR MIT" +edition = "2021" +rust-version = "1.65" + # default to small, optimized workspace release binaries [profile.release] panic = "abort" diff --git a/README.md b/README.md index 1320cd4adcc..18903b7b5f0 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ For **developing** Tauri apps refer to the [Getting Started guide on tauri.app]( For **running** Tauri apps we support the below configurations (these are automatically added as dependencies for .deb and are bundled for AppImage so that your users don't need to manually install them): - Debian (Ubuntu 18.04 and above or equivalent) with the following packages installed: - - `libwebkit2gtk-4.0-37`, `libgtk-3-0`, `libayatana-appindicator3-1`1 + - `libwebkit2gtk-4.1-0`, `libgtk-3-0`, `libayatana-appindicator3-1`1 - Arch with the following packages installed: - `webkit2gtk`, `gtk3`, `libayatana-appindicator`1 - Fedora (latest 2 versions) with the following packages installed: diff --git a/core/tauri-build/CHANGELOG.md b/core/tauri-build/CHANGELOG.md index ccf6cebbc42..373a0f57a26 100644 --- a/core/tauri-build/CHANGELOG.md +++ b/core/tauri-build/CHANGELOG.md @@ -1,5 +1,69 @@ # Changelog +## \[2.0.0-alpha.8] + +### Bug Fixes + +- [`560b34dd`](https://www.github.com/tauri-apps/tauri/commit/560b34dd2a194ad62db09b3e9e41a2cfff4e5710)([#7610](https://www.github.com/tauri-apps/tauri/pull/7610)) Skip validation of the `tray-icon` feature flag. + +## \[2.0.0-alpha.7] + +### New Features + +- [`522de0e7`](https://www.github.com/tauri-apps/tauri/commit/522de0e78891d0bdf6387a5118985fc41a11baeb)([#7447](https://www.github.com/tauri-apps/tauri/pull/7447)) Added the `config::plugin_config` function to read the plugin configuration set from the CLI. +- [`1e1d839e`](https://www.github.com/tauri-apps/tauri/commit/1e1d839e7e3d9496f71b6bc1336ced01f2965541)([#7450](https://www.github.com/tauri-apps/tauri/pull/7450)) Added the `mobile::update_android_manifest` function. +- [`aba04fa8`](https://www.github.com/tauri-apps/tauri/commit/aba04fa823d70ff8df9bd22f8e6a25184689c3cb)([#7448](https://www.github.com/tauri-apps/tauri/pull/7448)) Added the `mobile::update_entitlements` function for iOS. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.7` +- Upgraded to `tauri-codegen@2.0.0-alpha.7` + +## \[2.0.0-alpha.6] + +### Bug Fixes + +- [`3256a372`](https://www.github.com/tauri-apps/tauri/commit/3256a37263f60eafdf5a8321458b868bff26c1b8)([#7016](https://www.github.com/tauri-apps/tauri/pull/7016)) Fixes injection of the proguard rules on the Android project. + +## \[2.0.0-alpha.5] + +- [`3188f376`](https://www.github.com/tauri-apps/tauri/commit/3188f3764978c6d1452ee31d5a91469691e95094)([#6883](https://www.github.com/tauri-apps/tauri/pull/6883)) Bump the MSRV to 1.65. +- [`2969d1cb`](https://www.github.com/tauri-apps/tauri/commit/2969d1cbba39301f9cc611d9f7d7051d80eef846)([#6773](https://www.github.com/tauri-apps/tauri/pull/6773)) Use absolute path to each Android plugin project instead of copying the files to enhance developer experience. +- [`cdad6e08`](https://www.github.com/tauri-apps/tauri/commit/cdad6e083728ea61bd6fc734ef93f6306056ea2e)([#6774](https://www.github.com/tauri-apps/tauri/pull/6774)) Changed how the `tauri-android` dependency is injected. This requires the `gen/android` project to be recreated. +- [`5a768d5c`](https://www.github.com/tauri-apps/tauri/commit/5a768d5ce69d6c9011c41f38a43481087c8d4921)([#6886](https://www.github.com/tauri-apps/tauri/pull/6886)) Remove `WindowsAttributes::sdk_dir`. + +## \[2.0.0-alpha.4] + +- Added `android` configuration object under `tauri > bundle`. + - Bumped due to a bump in tauri-utils. + - [db4c9dc6](https://www.github.com/tauri-apps/tauri/commit/db4c9dc655e07ee2184fe04571f500f7910890cd) feat(core): add option to configure Android's minimum SDK version ([#6651](https://www.github.com/tauri-apps/tauri/pull/6651)) on 2023-04-07 + +## \[2.0.0-alpha.3] + +- Read the `IPHONEOS_DEPLOYMENT_TARGET` environment variable to set the Swift iOS target version, defaults to 13. + - [4c3b9ecf](https://www.github.com/tauri-apps/tauri/commit/4c3b9ecfdcd1a4489b1e466727f11045ef34d67a) fix(build): iOS deployment target env var is IPHONEOS_DEPLOYMENT_TARGET ([#6602](https://www.github.com/tauri-apps/tauri/pull/6602)) on 2023-03-31 + +## \[2.0.0-alpha.2] + +- Add `mobile::PluginBuilder` for running build tasks related to Tauri plugins. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 + +## \[2.0.0-alpha.1] + +- Refactor mobile environment variables. + - [dee9460f](https://www.github.com/tauri-apps/tauri/commit/dee9460f9c9bc92e9c638e7691e616849ac2085b) feat: keep CLI alive when iOS app exits, show logs, closes [#5855](https://www.github.com/tauri-apps/tauri/pull/5855) ([#5902](https://www.github.com/tauri-apps/tauri/pull/5902)) on 2022-12-27 +- Bump the MSRV to 1.64. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Removed mobile logging initialization, which will be handled by `tauri-plugin-log`. + - [](https://www.github.com/tauri-apps/tauri/commit/undefined) on undefined + +## \[2.0.0-alpha.0] + +- Set environment variables used by `tauri::mobile_entry_point`. + - [98904863](https://www.github.com/tauri-apps/tauri/commit/9890486321c9c79ccfb7c547fafee85b5c3ffa71) feat(core): add `mobile_entry_point` macro ([#4983](https://www.github.com/tauri-apps/tauri/pull/4983)) on 2022-08-21 +- First mobile alpha release! + - [fa3a1098](https://www.github.com/tauri-apps/tauri/commit/fa3a10988a03aed1b66fb17d893b1a9adb90f7cd) feat(ci): prepare 2.0.0-alpha.0 ([#5786](https://www.github.com/tauri-apps/tauri/pull/5786)) on 2022-12-08 + ## \[1.4.0] ### Enhancements diff --git a/core/tauri-build/Cargo.toml b/core/tauri-build/Cargo.toml index 4f92f8a467a..5f19102210d 100644 --- a/core/tauri-build/Cargo.toml +++ b/core/tauri-build/Cargo.toml @@ -1,16 +1,16 @@ [package] name = "tauri-build" -version = "1.4.0" -authors = [ "Tauri Programme within The Commons Conservancy" ] -categories = [ "gui", "web-programming" ] -license = "Apache-2.0 OR MIT" -homepage = "https://tauri.app" -repository = "https://github.com/tauri-apps/tauri/tree/dev/core/tauri-build" +version = "2.0.0-alpha.8" description = "build time code to pair with https://crates.io/crates/tauri" -edition = "2021" -rust-version = "1.60" exclude = [ "CHANGELOG.md", "/target" ] readme = "README.md" +authors = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +categories = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } [package.metadata.docs.rs] all-features = true @@ -19,16 +19,21 @@ rustdoc-args = [ "--cfg", "doc_cfg" ] [dependencies] anyhow = "1" quote = { version = "1", optional = true } -tauri-codegen = { version = "1.4.0", path = "../tauri-codegen", optional = true } -tauri-utils = { version = "1.4.0", path = "../tauri-utils", features = [ "build", "resources" ] } +tauri-codegen = { version = "2.0.0-alpha.7", path = "../tauri-codegen", optional = true } +tauri-utils = { version = "2.0.0-alpha.7", path = "../tauri-utils", features = [ "build", "resources" ] } cargo_toml = "0.15" serde = "1" serde_json = "1" heck = "0.4" json-patch = "1.0" +walkdir = "2" tauri-winres = "0.1" semver = "1" +[target."cfg(target_os = \"macos\")".dependencies] +swift-rs = { version = "1.0.6", features = [ "build" ] } +plist = "1" + [features] codegen = [ "tauri-codegen", "quote" ] isolation = [ "tauri-codegen/isolation", "tauri-utils/isolation" ] diff --git a/core/tauri-build/src/allowlist.rs b/core/tauri-build/src/allowlist.rs index 34654f689c3..8494a5abde4 100644 --- a/core/tauri-build/src/allowlist.rs +++ b/core/tauri-build/src/allowlist.rs @@ -43,11 +43,17 @@ pub fn check(config: &Config, manifest: &mut Manifest) -> Result<()> { name: "tauri".into(), alias: None, kind: DependencyKind::Normal, - all_cli_managed_features: Some(TauriConfig::all_features()), + all_cli_managed_features: Some( + TauriConfig::all_features() + .into_iter() + .filter(|f| f != &"tray-icon") + .collect(), + ), expected_features: config .tauri .features() .into_iter() + .filter(|f| f != &"tray-icon") .map(|f| f.to_string()) .collect::>(), }, diff --git a/core/tauri-build/src/codegen/context.rs b/core/tauri-build/src/codegen/context.rs index e17fe3b8f8d..f702ce21205 100644 --- a/core/tauri-build/src/codegen/context.rs +++ b/core/tauri-build/src/codegen/context.rs @@ -120,7 +120,7 @@ impl CodegenContext { config_parent.join(icon).display() ); } - if let Some(tray_icon) = config.tauri.system_tray.as_ref().map(|t| &t.icon_path) { + if let Some(tray_icon) = config.tauri.tray_icon.as_ref().map(|t| &t.icon_path) { println!( "cargo:rerun-if-changed={}", config_parent.join(tray_icon).display() diff --git a/core/tauri-build/src/config.rs b/core/tauri-build/src/config.rs new file mode 100644 index 00000000000..2bb2631c0d5 --- /dev/null +++ b/core/tauri-build/src/config.rs @@ -0,0 +1,22 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::de::DeserializeOwned; + +use std::{env::var, io::Cursor}; + +pub fn plugin_config(name: &str) -> Option { + let config_env_var_name = format!( + "TAURI_{}_PLUGIN_CONFIG", + name.to_uppercase().replace('-', "_") + ); + if let Ok(config_str) = var(&config_env_var_name) { + println!("cargo:rerun-if-env-changed={config_env_var_name}"); + serde_json::from_reader(Cursor::new(config_str)) + .map(Some) + .expect("failed to parse configuration") + } else { + None + } +} diff --git a/core/tauri-build/src/lib.rs b/core/tauri-build/src/lib.rs index 231f7d649a8..8e46612f429 100644 --- a/core/tauri-build/src/lib.rs +++ b/core/tauri-build/src/lib.rs @@ -2,8 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! [![](https://github.com/tauri-apps/tauri/raw/dev/.github/splash.png)](https://tauri.app) +//! +//! This applies the macros at build-time in order to rig some special features needed by `cargo`. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] #![cfg_attr(doc_cfg, feature(doc_cfg))] +use anyhow::Context; pub use anyhow::Result; use cargo_toml::Manifest; use heck::AsShoutySnakeCase; @@ -13,11 +22,18 @@ use tauri_utils::{ resources::{external_binaries, resource_relpath, ResourcePaths}, }; -use std::path::{Path, PathBuf}; +use std::{ + env::var_os, + path::{Path, PathBuf}, +}; mod allowlist; #[cfg(feature = "codegen")] mod codegen; +/// Tauri configuration functions. +pub mod config; +/// Mobile build functions. +pub mod mobile; mod static_vcruntime; #[cfg(feature = "codegen")] @@ -103,17 +119,6 @@ fn cfg_alias(alias: &str, has_feature: bool) { #[derive(Debug, Default)] pub struct WindowsAttributes { window_icon_path: Option, - /// The path to the sdk location. - /// - /// For the GNU toolkit this has to be the path where MinGW put windres.exe and ar.exe. - /// This could be something like: "C:\Program Files\mingw-w64\x86_64-5.3.0-win32-seh-rt_v4-rev0\mingw64\bin" - /// - /// For MSVC the Windows SDK has to be installed. It comes with the resource compiler rc.exe. - /// This should be set to the root directory of the Windows SDK, e.g., "C:\Program Files (x86)\Windows Kits\10" or, - /// if multiple 10 versions are installed, set it directly to the correct bin directory "C:\Program Files (x86)\Windows Kits\10\bin\10.0.14393.0\x64" - /// - /// 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, /// A string containing an [application manifest] to be included with the application on Windows. /// /// Defaults to: @@ -159,14 +164,6 @@ impl WindowsAttributes { self } - /// Sets the sdk dir for windows. Currently only used on Windows. This must be a valid UTF-8 - /// path. Defaults to whatever the `winres` crate determines is best. - #[must_use] - pub fn sdk_dir>(mut self, sdk_dir: P) -> Self { - self.sdk_dir = Some(sdk_dir.as_ref().into()); - self - } - /// Sets the [application manifest] to be included with the application on Windows. /// /// Defaults to: @@ -305,6 +302,22 @@ pub fn try_build(attributes: Attributes) -> Result<()> { } let config: Config = serde_json::from_value(config)?; + let s = config.tauri.bundle.identifier.split('.'); + let last = s.clone().count() - 1; + let mut android_package_prefix = String::new(); + for (i, w) in s.enumerate() { + if i == 0 || i != last { + android_package_prefix.push_str(w); + android_package_prefix.push('_'); + } + } + android_package_prefix.pop(); + println!("cargo:rustc-env=TAURI_ANDROID_PACKAGE_PREFIX={android_package_prefix}"); + + if let Some(project_dir) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { + mobile::generate_gradle_files(project_dir)?; + } + cfg_alias("dev", !has_feature("custom-protocol")); let ws_path = get_workspace_dir()?; @@ -324,6 +337,9 @@ pub fn try_build(attributes: Attributes) -> Result<()> { allowlist::check(&config, &mut manifest)?; let target_triple = std::env::var("TARGET").unwrap(); + + println!("cargo:rustc-env=TAURI_TARGET_TRIPLE={target_triple}"); + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); // TODO: far from ideal, but there's no other way to get the target dir, see let target_dir = out_dir @@ -361,7 +377,6 @@ pub fn try_build(attributes: Attributes) -> Result<()> { } if target_triple.contains("windows") { - use anyhow::Context; use semver::Version; use tauri_winres::{VersionInfo, WindowsResource}; @@ -382,54 +397,47 @@ pub fn try_build(attributes: Attributes) -> Result<()> { .window_icon_path .unwrap_or_else(|| find_icon(&config, |i| i.ends_with(".ico"), "icons/icon.ico")); - if window_icon_path.exists() { - let mut res = WindowsResource::new(); + if target_triple.contains("windows") { + if window_icon_path.exists() { + let mut res = WindowsResource::new(); - 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() { - res.set_toolkit_path(sdk_dir_str); + if let Some(manifest) = attributes.windows_attributes.app_manifest { + res.set_manifest(&manifest); } else { - return Err(anyhow!( - "sdk_dir path is not valid; only UTF-8 characters are allowed" - )); + res.set_manifest(include_str!("window-app-manifest.xml")); } - } - if let Some(version_str) = &config.package.version { - if let Ok(v) = Version::parse(version_str) { - let version = v.major << 48 | v.minor << 32 | v.patch << 16; - res.set_version_info(VersionInfo::FILEVERSION, version); - res.set_version_info(VersionInfo::PRODUCTVERSION, version); + + if let Some(version_str) = &config.package.version { + if let Ok(v) = Version::parse(version_str) { + let version = v.major << 48 | v.minor << 32 | v.patch << 16; + res.set_version_info(VersionInfo::FILEVERSION, version); + res.set_version_info(VersionInfo::PRODUCTVERSION, version); + } + res.set("FileVersion", version_str); + res.set("ProductVersion", version_str); } - res.set("FileVersion", version_str); - res.set("ProductVersion", version_str); - } - if let Some(product_name) = &config.package.product_name { - res.set("ProductName", product_name); - } - if let Some(short_description) = &config.tauri.bundle.short_description { - res.set("FileDescription", short_description); - } - if let Some(copyright) = &config.tauri.bundle.copyright { - res.set("LegalCopyright", copyright); - } - res.set_icon_with_id(&window_icon_path.display().to_string(), "32512"); - res.compile().with_context(|| { - format!( - "failed to compile `{}` into a Windows Resource file during tauri-build", + if let Some(product_name) = &config.package.product_name { + res.set("ProductName", product_name); + } + if let Some(short_description) = &config.tauri.bundle.short_description { + res.set("FileDescription", short_description); + } + if let Some(copyright) = &config.tauri.bundle.copyright { + res.set("LegalCopyright", copyright); + } + res.set_icon_with_id(&window_icon_path.display().to_string(), "32512"); + res.compile().with_context(|| { + format!( + "failed to compile `{}` into a Windows Resource file during tauri-build", + window_icon_path.display() + ) + })?; + } else { + return Err(anyhow!(format!( + "`{}` not found; required for generating a Windows Resource file during tauri-build", window_icon_path.display() - ) - })?; - } else { - return Err(anyhow!(format!( - "`{}` not found; required for generating a Windows Resource file during tauri-build", - window_icon_path.display() - ))); + ))); + } } let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap(); diff --git a/core/tauri-build/src/mobile.rs b/core/tauri-build/src/mobile.rs new file mode 100644 index 00000000000..4f134ef78d8 --- /dev/null +++ b/core/tauri-build/src/mobile.rs @@ -0,0 +1,325 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + env::{var, var_os}, + fs::{copy, create_dir, create_dir_all, read_to_string, remove_dir_all, write}, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Deserialize, Serialize, Eq, PartialEq)] +pub(crate) struct PluginMetadata { + pub path: PathBuf, +} + +#[derive(Default)] +pub struct PluginBuilder { + android_path: Option, + ios_path: Option, +} + +impl PluginBuilder { + /// Creates a new builder for mobile plugin functionality. + pub fn new() -> Self { + Self::default() + } + + /// Sets the Android project path. + pub fn android_path>(mut self, android_path: P) -> Self { + self.android_path.replace(android_path.into()); + self + } + + /// Sets the iOS project path. + pub fn ios_path>(mut self, ios_path: P) -> Self { + self.ios_path.replace(ios_path.into()); + self + } + + /// Injects the mobile templates in the given path relative to the manifest root. + pub fn run(self) -> Result<()> { + let target_os = var("CARGO_CFG_TARGET_OS").unwrap(); + let mobile = target_os == "android" || target_os == "ios"; + crate::cfg_alias("mobile", mobile); + crate::cfg_alias("desktop", !mobile); + + match target_os.as_str() { + "android" => { + if let Some(path) = self.android_path { + let manifest_dir = var_os("CARGO_MANIFEST_DIR").map(PathBuf::from).unwrap(); + let source = manifest_dir.join(path); + + let tauri_library_path = std::env::var("DEP_TAURI_ANDROID_LIBRARY_PATH") + .expect("missing `DEP_TAURI_ANDROID_LIBRARY_PATH` environment variable. Make sure `tauri` is a dependency of the plugin."); + println!("cargo:rerun-if-env-changed=DEP_TAURI_ANDROID_LIBRARY_PATH"); + + create_dir_all(source.join(".tauri")).context("failed to create .tauri directory")?; + copy_folder( + Path::new(&tauri_library_path), + &source.join(".tauri").join("tauri-api"), + &[], + ) + .context("failed to copy tauri-api to the plugin project")?; + + println!("cargo:android_library_path={}", source.display()); + } + } + #[cfg(target_os = "macos")] + "ios" => { + if let Some(path) = self.ios_path { + let manifest_dir = var_os("CARGO_MANIFEST_DIR").map(PathBuf::from).unwrap(); + let tauri_library_path = std::env::var("DEP_TAURI_IOS_LIBRARY_PATH") + .expect("missing `DEP_TAURI_IOS_LIBRARY_PATH` environment variable. Make sure `tauri` is a dependency of the plugin."); + + let tauri_dep_path = path.parent().unwrap().join(".tauri"); + create_dir_all(&tauri_dep_path).context("failed to create .tauri directory")?; + copy_folder( + Path::new(&tauri_library_path), + &tauri_dep_path.join("tauri-api"), + &[".build", "Package.resolved", "Tests"], + ) + .context("failed to copy tauri-api to the plugin project")?; + link_swift_library(&var("CARGO_PKG_NAME").unwrap(), manifest_dir.join(path)); + } + } + _ => (), + } + + Ok(()) + } +} + +#[cfg(target_os = "macos")] +#[doc(hidden)] +pub fn link_swift_library(name: &str, source: impl AsRef) { + let source = source.as_ref(); + + let sdk_root = std::env::var_os("SDKROOT"); + std::env::remove_var("SDKROOT"); + + swift_rs::SwiftLinker::new( + &std::env::var("MACOSX_DEPLOYMENT_TARGET").unwrap_or_else(|_| "10.13".into()), + ) + .with_ios(&std::env::var("IPHONEOS_DEPLOYMENT_TARGET").unwrap_or_else(|_| "13.0".into())) + .with_package(name, source) + .link(); + + if let Some(root) = sdk_root { + std::env::set_var("SDKROOT", root); + } +} + +fn copy_folder(source: &Path, target: &Path, ignore_paths: &[&str]) -> Result<()> { + let _ = remove_dir_all(target); + + for entry in walkdir::WalkDir::new(source) { + let entry = entry?; + let rel_path = entry.path().strip_prefix(source)?; + let rel_path_str = rel_path.to_string_lossy(); + if ignore_paths + .iter() + .any(|path| rel_path_str.starts_with(path)) + { + continue; + } + let dest_path = target.join(rel_path); + + if entry.file_type().is_dir() { + create_dir(&dest_path) + .with_context(|| format!("failed to create directory {}", dest_path.display()))?; + } else { + copy(entry.path(), &dest_path).with_context(|| { + format!( + "failed to copy {} to {}", + entry.path().display(), + dest_path.display() + ) + })?; + println!("cargo:rerun-if-changed={}", entry.path().display()); + } + } + + Ok(()) +} + +#[cfg(target_os = "macos")] +fn update_plist_file, F: FnOnce(&mut plist::Dictionary)>( + path: P, + f: F, +) -> Result<()> { + use std::io::Cursor; + + let path = path.as_ref(); + if path.exists() { + let plist_str = read_to_string(path)?; + let mut plist = plist::Value::from_reader(Cursor::new(&plist_str))?; + if let Some(dict) = plist.as_dictionary_mut() { + f(dict); + let mut plist_buf = Vec::new(); + let writer = Cursor::new(&mut plist_buf); + plist::to_writer_xml(writer, &plist)?; + let new_plist_str = String::from_utf8(plist_buf)?; + if new_plist_str != plist_str { + write(path, new_plist_str)?; + } + } + } + + Ok(()) +} + +#[cfg(target_os = "macos")] +pub fn update_entitlements(f: F) -> Result<()> { + if let (Some(project_path), Ok(app_name)) = ( + var_os("TAURI_IOS_PROJECT_PATH").map(PathBuf::from), + var("TAURI_IOS_APP_NAME"), + ) { + update_plist_file( + project_path + .join(format!("{app_name}_iOS")) + .join(format!("{app_name}_iOS.entitlements")), + f, + )?; + } + + Ok(()) +} + +fn xml_block_comment(id: &str) -> String { + format!("") +} + +fn insert_into_xml(xml: &str, block_identifier: &str, parent_tag: &str, contents: &str) -> String { + let block_comment = xml_block_comment(block_identifier); + + let mut rewritten = Vec::new(); + let mut found_block = false; + let parent_closing_tag = format!(""); + for line in xml.split('\n') { + if line.contains(&block_comment) { + found_block = !found_block; + continue; + } + + // found previous block which should be removed + if found_block { + continue; + } + + if let Some(index) = line.find(&parent_closing_tag) { + let identation = " ".repeat(index + 4); + rewritten.push(format!("{}{}", identation, block_comment)); + for l in contents.split('\n') { + rewritten.push(format!("{}{}", identation, l)); + } + rewritten.push(format!("{}{}", identation, block_comment)); + } + + rewritten.push(line.to_string()); + } + + rewritten.join("\n") +} + +pub fn update_android_manifest(block_identifier: &str, parent: &str, insert: String) -> Result<()> { + if let Some(project_path) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { + let manifest_path = project_path.join("app/src/main/AndroidManifest.xml"); + let manifest = read_to_string(&manifest_path)?; + let rewritten = insert_into_xml(&manifest, block_identifier, parent, &insert); + if rewritten != manifest { + write(manifest_path, rewritten)?; + } + } + Ok(()) +} + +pub(crate) fn generate_gradle_files(project_dir: PathBuf) -> Result<()> { + let gradle_settings_path = project_dir.join("tauri.settings.gradle"); + let app_build_gradle_path = project_dir.join("app").join("tauri.build.gradle.kts"); + + let mut gradle_settings = + "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n".to_string(); + let mut app_build_gradle = "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +val implementation by configurations +dependencies {" + .to_string(); + + for (env, value) in std::env::vars_os() { + let env = env.to_string_lossy(); + if env.starts_with("DEP_") && env.ends_with("_ANDROID_LIBRARY_PATH") { + let name_len = env.len() - "DEP_".len() - "_ANDROID_LIBRARY_PATH".len(); + let mut plugin_name = env + .chars() + .skip("DEP_".len()) + .take(name_len) + .collect::() + .to_lowercase() + .replace('_', "-"); + if plugin_name == "tauri" { + plugin_name = "tauri-android".into(); + } + let plugin_path = PathBuf::from(value); + + gradle_settings.push_str(&format!("include ':{plugin_name}'")); + gradle_settings.push('\n'); + gradle_settings.push_str(&format!( + "project(':{plugin_name}').projectDir = new File({:?})", + tauri_utils::display_path(plugin_path) + )); + gradle_settings.push('\n'); + + app_build_gradle.push('\n'); + app_build_gradle.push_str(&format!(r#" implementation(project(":{plugin_name}"))"#)); + } + } + + app_build_gradle.push_str("\n}"); + + write(&gradle_settings_path, gradle_settings).context("failed to write tauri.settings.gradle")?; + + write(&app_build_gradle_path, app_build_gradle) + .context("failed to write tauri.build.gradle.kts")?; + + println!("cargo:rerun-if-changed={}", gradle_settings_path.display()); + println!("cargo:rerun-if-changed={}", app_build_gradle_path.display()); + + Ok(()) +} + +#[cfg(test)] +mod tests { + #[test] + fn insert_into_xml() { + let manifest = r#" + + + + +"#; + let id = "tauritest"; + let new = super::insert_into_xml(manifest, id, "application", ""); + + let block_id_comment = super::xml_block_comment(id); + let expected = format!( + r#" + + + + {block_id_comment} + + {block_id_comment} + +"# + ); + + assert_eq!(new, expected); + + // assert it's still the same after an empty update + let new = super::insert_into_xml(&expected, id, "application", ""); + assert_eq!(new, expected); + } +} diff --git a/core/tauri-codegen/CHANGELOG.md b/core/tauri-codegen/CHANGELOG.md index 6081cbc7b40..61929c296ba 100644 --- a/core/tauri-codegen/CHANGELOG.md +++ b/core/tauri-codegen/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## \[2.0.0-alpha.7] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.7` + +## \[2.0.0-alpha.6] + +### Dependencies + +- Updated to latest `tauri-utils` + +## \[2.0.0-alpha.5] + +- [`96639ca2`](https://www.github.com/tauri-apps/tauri/commit/96639ca239c9e4f75142fc07868ac46822111cff)([#6749](https://www.github.com/tauri-apps/tauri/pull/6749)) Moved the `shell` functionality to its own plugin in the plugins-workspace repository. +- [`3188f376`](https://www.github.com/tauri-apps/tauri/commit/3188f3764978c6d1452ee31d5a91469691e95094)([#6883](https://www.github.com/tauri-apps/tauri/pull/6883)) Bump the MSRV to 1.65. +- [`ae102980`](https://www.github.com/tauri-apps/tauri/commit/ae102980fcdde3f55effdc0623ea425b48d07dd1)([#6719](https://www.github.com/tauri-apps/tauri/pull/6719)) Refactor the `Context` conditional fields and only parse the tray icon on desktop. + +## \[2.0.0-alpha.4] + +- Added `android` configuration object under `tauri > bundle`. + - Bumped due to a bump in tauri-utils. + - [db4c9dc6](https://www.github.com/tauri-apps/tauri/commit/db4c9dc655e07ee2184fe04571f500f7910890cd) feat(core): add option to configure Android's minimum SDK version ([#6651](https://www.github.com/tauri-apps/tauri/pull/6651)) on 2023-04-07 + +## \[2.0.0-alpha.3] + +- Pull changes from Tauri 1.3 release. + - [](https://www.github.com/tauri-apps/tauri/commit/undefined) on undefined + +## \[2.0.0-alpha.2] + +- Return `bool` in the invoke handler. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 + +## \[2.0.0-alpha.1] + +- Bump the MSRV to 1.64. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Added `crate_name` field on `PackageInfo`. + - [630a7f4b](https://www.github.com/tauri-apps/tauri/commit/630a7f4b18cef169bfd48673609306fec434e397) refactor: remove mobile log initialization, ref [#6049](https://www.github.com/tauri-apps/tauri/pull/6049) ([#6081](https://www.github.com/tauri-apps/tauri/pull/6081)) on 2023-01-17 + +## \[2.0.0-alpha.0] + +- Change `devPath` URL to use the local IP address on iOS and Android. + - [6f061504](https://www.github.com/tauri-apps/tauri/commit/6f0615044d09ec58393a7ebca5e45bb175e20db3) feat(cli): add `android dev` and `ios dev` commands ([#4982](https://www.github.com/tauri-apps/tauri/pull/4982)) on 2022-08-20 +- First mobile alpha release! + - [fa3a1098](https://www.github.com/tauri-apps/tauri/commit/fa3a10988a03aed1b66fb17d893b1a9adb90f7cd) feat(ci): prepare 2.0.0-alpha.0 ([#5786](https://www.github.com/tauri-apps/tauri/pull/5786)) on 2022-12-08 + ## \[1.4.0] ### Enhancements diff --git a/core/tauri-codegen/Cargo.toml b/core/tauri-codegen/Cargo.toml index 16d201e5067..ebc64341c6e 100644 --- a/core/tauri-codegen/Cargo.toml +++ b/core/tauri-codegen/Cargo.toml @@ -1,16 +1,16 @@ [package] name = "tauri-codegen" -version = "1.4.0" -authors = [ "Tauri Programme within The Commons Conservancy" ] -categories = [ "gui", "web-programming" ] -license = "Apache-2.0 OR MIT" -homepage = "https://tauri.app" -repository = "https://github.com/tauri-apps/tauri/tree/dev/core/tauri-codegen" +version = "2.0.0-alpha.7" description = "code generation meant to be consumed inside of `tauri` through `tauri-build` or `tauri-macros`" -edition = "2021" -rust-version = "1.60" exclude = [ "CHANGELOG.md", "/target" ] readme = "README.md" +authors = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +categories = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } [dependencies] sha2 = "0.10" @@ -19,16 +19,16 @@ proc-macro2 = "1" quote = "1" serde = { version = "1", features = [ "derive" ] } serde_json = "1" -tauri-utils = { version = "1.4.0", path = "../tauri-utils", features = [ "build" ] } +tauri-utils = { version = "2.0.0-alpha.7", path = "../tauri-utils", features = [ "build" ] } thiserror = "1" walkdir = "2" brotli = { version = "3", optional = true, default-features = false, features = [ "std" ] } -regex = { version = "1.7.1", optional = true } uuid = { version = "1", features = [ "v4" ] } semver = "1" ico = "0.3" png = "0.17" json-patch = "1.0" +url = "2" [target."cfg(target_os = \"macos\")".dependencies] plist = "1" @@ -38,6 +38,5 @@ time = { version = "0.3", features = [ "parsing", "formatting" ] } default = [ "compression" ] compression = [ "brotli", "tauri-utils/compression" ] isolation = [ "tauri-utils/isolation" ] -shell-scope = [ "regex" ] config-json5 = [ "tauri-utils/config-json5" ] config-toml = [ "tauri-utils/config-toml" ] diff --git a/core/tauri-codegen/README.md b/core/tauri-codegen/README.md index 3f3e15990f2..8d5518db7b7 100644 --- a/core/tauri-codegen/README.md +++ b/core/tauri-codegen/README.md @@ -24,7 +24,7 @@ Tauri apps can have custom menus and have tray-type interfaces. They can be upda ## This module -- Embed, hash, and compress assets, including icons for the app as well as the system-tray. +- Embed, hash, and compress assets, including icons for the app as well as the tray icon. - Parse `tauri.conf.json` at compile time and generate the Config struct. To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document. diff --git a/core/tauri-codegen/src/context.rs b/core/tauri-codegen/src/context.rs index 778a7fc294b..788db7db5d4 100644 --- a/core/tauri-codegen/src/context.rs +++ b/core/tauri-codegen/src/context.rs @@ -16,9 +16,6 @@ use tauri_utils::html::{ inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node, }; -#[cfg(feature = "shell-scope")] -use tauri_utils::config::{ShellAllowedArg, ShellAllowedArgs, ShellAllowlistScope}; - use crate::embedded_assets::{AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsError}; /// Necessary data needed by [`context_codegen`] to generate code for a Tauri application context. @@ -125,6 +122,16 @@ enum Target { Ios, } +impl Target { + fn is_mobile(&self) -> bool { + matches!(self, Target::Android | Target::Ios) + } + + fn is_desktop(&self) -> bool { + !self.is_mobile() + } +} + /// Build a `tauri::Context` for including in application code. pub fn context_codegen(data: ContextData) -> Result { let ContextData { @@ -134,33 +141,34 @@ pub fn context_codegen(data: ContextData) -> Result Result Result Result Result Result quote!(::std::option::Option::None), - ShellAllowlistOpen::Flag(true) => { - 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())), - Err(error) => { - let error = error.to_string(); - quote!({ - compile_error!(#error); - ::std::option::Option::Some(#root::regex::Regex::new(#regex).unwrap()) - }) - } - }, - _ => panic!("unknown shell open format, unable to prepare"), - }; - - quote!(#root::ShellScopeConfig { - open: #shell_scope_open, - scopes: #shell_scopes - }) - }; - - #[cfg(not(feature = "shell-scope"))] - let shell_scope_config = quote!(); - - Ok(quote!(#root::Context::new( - #config, - ::std::sync::Arc::new(#assets), - #default_window_icon, - #app_icon, - #system_tray_icon, - #package_info, - #info_plist, - #pattern, - #shell_scope_config - ))) + Ok(quote!({ + #[allow(unused_mut, clippy::let_and_return)] + let mut context = #root::Context::new( + #config, + ::std::sync::Arc::new(#assets), + #default_window_icon, + #app_icon, + #package_info, + #info_plist, + #pattern, + ); + #with_tray_icon_code + context + })) } fn ico_icon>( @@ -481,7 +463,7 @@ fn ico_icon>( let out_path = out_path.display().to_string(); - let icon = quote!(Some(#root::Icon::Rgba { rgba: include_bytes!(#out_path).to_vec(), width: #width, height: #height })); + let icon = quote!(#root::Icon::Rgba { rgba: include_bytes!(#out_path).to_vec(), width: #width, height: #height }); Ok(icon) } @@ -499,7 +481,9 @@ fn raw_icon>(out_dir: &Path, path: P) -> Result>( let out_path = out_path.display().to_string(); - let icon = quote!(Some(#root::Icon::Rgba { rgba: include_bytes!(#out_path).to_vec(), width: #width, height: #height })); + let icon = quote!(#root::Icon::Rgba { rgba: include_bytes!(#out_path).to_vec(), width: #width, height: #height }); Ok(icon) } @@ -572,78 +556,3 @@ fn find_icon bool>( .unwrap_or_else(|| default.to_string()); config_parent.join(icon_path) } - -#[cfg(feature = "shell-scope")] -fn get_allowed_clis(root: &TokenStream, scope: &ShellAllowlistScope) -> TokenStream { - let commands = scope - .0 - .iter() - .map(|scope| { - let sidecar = &scope.sidecar; - - let name = &scope.name; - let name = quote!(#name.into()); - - let command = scope.command.to_string_lossy(); - let command = quote!(::std::path::PathBuf::from(#command)); - - let args = match &scope.args { - ShellAllowedArgs::Flag(true) => quote!(::std::option::Option::None), - ShellAllowedArgs::Flag(false) => quote!(::std::option::Option::Some(::std::vec![])), - ShellAllowedArgs::List(list) => { - let list = list.iter().map(|arg| match arg { - ShellAllowedArg::Fixed(fixed) => { - quote!(#root::scope::ShellScopeAllowedArg::Fixed(#fixed.into())) - } - ShellAllowedArg::Var { validator } => { - let validator = match regex::Regex::new(validator) { - Ok(regex) => { - let regex = regex.as_str(); - quote!(#root::regex::Regex::new(#regex).unwrap()) - } - Err(error) => { - let error = error.to_string(); - quote!({ - compile_error!(#error); - #root::regex::Regex::new(#validator).unwrap() - }) - } - }; - - quote!(#root::scope::ShellScopeAllowedArg::Var { validator: #validator }) - } - _ => panic!("unknown shell scope arg, unable to prepare"), - }); - - quote!(::std::option::Option::Some(::std::vec![#(#list),*])) - } - _ => panic!("unknown shell scope command, unable to prepare"), - }; - - ( - quote!(#name), - quote!( - #root::scope::ShellScopeAllowedCommand { - command: #command, - args: #args, - sidecar: #sidecar, - } - ), - ) - }) - .collect::>(); - - if commands.is_empty() { - quote!(::std::collections::HashMap::new()) - } else { - let insertions = commands - .iter() - .map(|(name, value)| quote!(hashmap.insert(#name, #value);)); - - quote!({ - let mut hashmap = ::std::collections::HashMap::new(); - #(#insertions)* - hashmap - }) - } -} diff --git a/core/tauri-codegen/src/embedded_assets.rs b/core/tauri-codegen/src/embedded_assets.rs index cb9f2f45957..ce9e0fcb472 100644 --- a/core/tauri-codegen/src/embedded_assets.rs +++ b/core/tauri-codegen/src/embedded_assets.rs @@ -433,7 +433,7 @@ impl ToTokens for EmbeddedAssets { // we expect phf related items to be in path when generating the path code tokens.append_all(quote! {{ - #[allow(unused)] + #[allow(unused_imports)] use ::tauri::utils::assets::{CspHash, EmbeddedAssets, phf, phf::phf_map}; EmbeddedAssets::new(phf_map! { #assets }, &[#global_hashes], phf_map! { #html_hashes }) }}); diff --git a/core/tauri-codegen/src/lib.rs b/core/tauri-codegen/src/lib.rs index 7e7395a946a..cf55797f169 100644 --- a/core/tauri-codegen/src/lib.rs +++ b/core/tauri-codegen/src/lib.rs @@ -2,6 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! [![](https://github.com/tauri-apps/tauri/raw/dev/.github/splash.png)](https://tauri.app) +//! +//! - Embed, hash, and compress assets, including icons for the app as well as the tray icon. +//! - Parse `tauri.conf.json` at compile time and generate the Config struct. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + pub use self::context::{context_codegen, ContextData}; use std::{ borrow::Cow, diff --git a/core/tauri-config-schema/Cargo.toml b/core/tauri-config-schema/Cargo.toml index 7c5fbbd90e6..64188842f46 100644 --- a/core/tauri-config-schema/Cargo.toml +++ b/core/tauri-config-schema/Cargo.toml @@ -5,11 +5,10 @@ edition = "2021" publish = false [build-dependencies] -tauri-utils = { version = "1.0.0", features = [ +tauri-utils = { version = "2.0.0-alpha.4", features = [ "schema", ], path = "../tauri-utils" } schemars = { version = "0.8", features = ["url", "preserve_order"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -serde_with = "1.12" url = { version = "2.3", features = ["serde"] } diff --git a/core/tauri-config-schema/schema.json b/core/tauri-config-schema/schema.json index 9eff6b9bfcb..803da9425ef 100644 --- a/core/tauri-config-schema/schema.json +++ b/core/tauri-config-schema/schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", - "description": "The Tauri configuration object. It is read from a file where you can define your frontend assets, configure the bundler, enable the app updater, define a system tray, enable APIs via the allowlist and more.\n\nThe configuration file is generated by the [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in your Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively. The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`. The TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can read a platform-specific configuration from `tauri.linux.conf.json`, `tauri.windows.conf.json`, and `tauri.macos.conf.json` (or `Tauri.linux.toml`, `Tauri.windows.toml` and `Tauri.macos.toml` if the `Tauri.toml` format is used), which gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`package`](#packageconfig): Package settings - [`tauri`](#tauriconfig): The Tauri config - [`build`](#buildconfig): The build configuration - [`plugins`](#pluginconfig): The plugins config\n\n```json title=\"Example tauri.config.json file\" { \"build\": { \"beforeBuildCommand\": \"\", \"beforeDevCommand\": \"\", \"devPath\": \"../dist\", \"distDir\": \"../dist\" }, \"package\": { \"productName\": \"tauri-app\", \"version\": \"0.1.0\" }, \"tauri\": { \"allowlist\": { \"all\": true }, \"bundle\": {}, \"security\": { \"csp\": null }, \"updater\": { \"active\": false }, \"windows\": [ { \"fullscreen\": false, \"height\": 600, \"resizable\": true, \"title\": \"Tauri App\", \"width\": 800 } ] } } ```", + "description": "The Tauri configuration object. It is read from a file where you can define your frontend assets, configure the bundler and define a tray icon.\n\nThe configuration file is generated by the [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in your Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively. The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`. The TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can read a platform-specific configuration from `tauri.linux.conf.json`, `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json` (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used), which gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`package`](#packageconfig): Package settings - [`tauri`](#tauriconfig): The Tauri config - [`build`](#buildconfig): The build configuration - [`plugins`](#pluginconfig): The plugins config\n\n```json title=\"Example tauri.config.json file\" { \"build\": { \"beforeBuildCommand\": \"\", \"beforeDevCommand\": \"\", \"devPath\": \"../dist\", \"distDir\": \"../dist\" }, \"package\": { \"productName\": \"tauri-app\", \"version\": \"0.1.0\" }, \"tauri\": { \"bundle\": {}, \"security\": { \"csp\": null }, \"windows\": [ { \"fullscreen\": false, \"height\": 600, \"resizable\": true, \"title\": \"Tauri App\", \"width\": 800 } ] } } ```", "type": "object", "properties": { "$schema": { @@ -26,125 +26,31 @@ "tauri": { "description": "The Tauri configuration.", "default": { - "allowlist": { - "all": false, - "app": { - "all": false, - "hide": false, - "show": false - }, - "clipboard": { - "all": false, - "readText": false, - "writeText": false - }, - "dialog": { - "all": false, - "ask": false, - "confirm": false, - "message": false, - "open": false, - "save": false - }, - "fs": { - "all": false, - "copyFile": false, - "createDir": false, - "exists": false, - "readDir": false, - "readFile": false, - "removeDir": false, - "removeFile": false, - "renameFile": false, - "scope": [], - "writeFile": false - }, - "globalShortcut": { - "all": false - }, - "http": { - "all": false, - "request": false, - "scope": [] - }, - "notification": { - "all": false - }, - "os": { - "all": false - }, - "path": { - "all": false - }, - "process": { - "all": false, - "exit": false, - "relaunch": false, - "relaunchDangerousAllowSymlinkMacos": false - }, - "protocol": { - "all": false, - "asset": false, - "assetScope": [] - }, - "shell": { - "all": false, - "execute": false, - "open": false, - "scope": [], - "sidecar": false - }, - "window": { - "all": false, - "center": false, - "close": false, - "create": false, - "hide": false, - "maximize": false, - "minimize": false, - "print": false, - "requestUserAttention": false, - "setAlwaysOnTop": false, - "setClosable": false, - "setContentProtected": false, - "setCursorGrab": false, - "setCursorIcon": false, - "setCursorPosition": false, - "setCursorVisible": false, - "setDecorations": false, - "setFocus": false, - "setFullscreen": false, - "setIcon": false, - "setIgnoreCursorEvents": false, - "setMaxSize": false, - "setMaximizable": false, - "setMinSize": false, - "setMinimizable": false, - "setPosition": false, - "setResizable": false, - "setSize": false, - "setSkipTaskbar": false, - "setTitle": false, - "show": false, - "startDragging": false, - "unmaximize": false, - "unminimize": false - } - }, "bundle": { "active": false, + "android": { + "minSdkVersion": 24 + }, "appimage": { "bundleMediaFramework": false }, "deb": { "files": {} }, + "iOS": {}, "icon": [], "identifier": "", "macOS": { "minimumSystemVersion": "10.13" }, "targets": "all", + "updater": { + "active": false, + "pubkey": "", + "windows": { + "installMode": "passive" + } + }, "windows": { "allowDowngrades": true, "certificateThumbprint": null, @@ -165,19 +71,14 @@ "use": "brownfield" }, "security": { + "assetProtocol": { + "enable": false, + "scope": [] + }, "dangerousDisableAssetCspModification": false, "dangerousRemoteDomainIpcAccess": [], "freezePrototype": false }, - "updater": { - "active": false, - "dialog": true, - "pubkey": "", - "windows": { - "installMode": "passive", - "installerArgs": [] - } - }, "windows": [] }, "allOf": [ @@ -257,33 +158,33 @@ "$ref": "#/definitions/WindowConfig" } }, - "cli": { - "description": "The CLI configuration.", - "anyOf": [ - { - "$ref": "#/definitions/CliConfig" - }, - { - "type": "null" - } - ] - }, "bundle": { "description": "The bundler configuration.", "default": { "active": false, + "android": { + "minSdkVersion": 24 + }, "appimage": { "bundleMediaFramework": false }, "deb": { "files": {} }, + "iOS": {}, "icon": [], "identifier": "", "macOS": { "minimumSystemVersion": "10.13" }, "targets": "all", + "updater": { + "active": false, + "pubkey": "", + "windows": { + "installMode": "passive" + } + }, "windows": { "allowDowngrades": true, "certificateThumbprint": null, @@ -305,122 +206,13 @@ } ] }, - "allowlist": { - "description": "The allowlist configuration.", - "default": { - "all": false, - "app": { - "all": false, - "hide": false, - "show": false - }, - "clipboard": { - "all": false, - "readText": false, - "writeText": false - }, - "dialog": { - "all": false, - "ask": false, - "confirm": false, - "message": false, - "open": false, - "save": false - }, - "fs": { - "all": false, - "copyFile": false, - "createDir": false, - "exists": false, - "readDir": false, - "readFile": false, - "removeDir": false, - "removeFile": false, - "renameFile": false, - "scope": [], - "writeFile": false - }, - "globalShortcut": { - "all": false - }, - "http": { - "all": false, - "request": false, - "scope": [] - }, - "notification": { - "all": false - }, - "os": { - "all": false - }, - "path": { - "all": false - }, - "process": { - "all": false, - "exit": false, - "relaunch": false, - "relaunchDangerousAllowSymlinkMacos": false - }, - "protocol": { - "all": false, - "asset": false, - "assetScope": [] - }, - "shell": { - "all": false, - "execute": false, - "open": false, - "scope": [], - "sidecar": false - }, - "window": { - "all": false, - "center": false, - "close": false, - "create": false, - "hide": false, - "maximize": false, - "minimize": false, - "print": false, - "requestUserAttention": false, - "setAlwaysOnTop": false, - "setClosable": false, - "setContentProtected": false, - "setCursorGrab": false, - "setCursorIcon": false, - "setCursorPosition": false, - "setCursorVisible": false, - "setDecorations": false, - "setFocus": false, - "setFullscreen": false, - "setIcon": false, - "setIgnoreCursorEvents": false, - "setMaxSize": false, - "setMaximizable": false, - "setMinSize": false, - "setMinimizable": false, - "setPosition": false, - "setResizable": false, - "setSize": false, - "setSkipTaskbar": false, - "setTitle": false, - "show": false, - "startDragging": false, - "unmaximize": false, - "unminimize": false - } - }, - "allOf": [ - { - "$ref": "#/definitions/AllowlistConfig" - } - ] - }, "security": { "description": "Security configuration.", "default": { + "assetProtocol": { + "enable": false, + "scope": [] + }, "dangerousDisableAssetCspModification": false, "dangerousRemoteDomainIpcAccess": [], "freezePrototype": false @@ -431,28 +223,11 @@ } ] }, - "updater": { - "description": "The updater configuration.", - "default": { - "active": false, - "dialog": true, - "pubkey": "", - "windows": { - "installMode": "passive", - "installerArgs": [] - } - }, - "allOf": [ - { - "$ref": "#/definitions/UpdaterConfig" - } - ] - }, - "systemTray": { - "description": "Configuration for app system tray.", + "trayIcon": { + "description": "Configuration for app tray icon.", "anyOf": [ { - "$ref": "#/definitions/SystemTrayConfig" + "$ref": "#/definitions/TrayIconConfig" }, { "type": "null" @@ -670,6 +445,11 @@ "default": false, "type": "boolean" }, + "visibleOnAllWorkspaces": { + "description": "Whether the window should be visible on all workspaces or virtual desktops.", + "default": false, + "type": "boolean" + }, "contentProtected": { "description": "Prevents the window contents from being captured by other apps.", "default": false, @@ -723,6 +503,27 @@ "string", "null" ] + }, + "shadow": { + "description": "Whether or not the window has shadow.\n\n## Platform-specific\n\n- **Windows:** - `false` has no effect on decorated window, shadow are always ON. - `true` will make ndecorated window have a 1px white border, and on Windows 11, it will have a rounded corners. - **Linux:** Unsupported.", + "default": true, + "type": "boolean" + }, + "windowEffects": { + "description": "Window effects.\n\nRequires the window to be transparent.\n\n## Platform-specific:\n\n- **Windows**: If using decorations or shadows, you may want to try this workaround https://github.com/tauri-apps/tao/issues/72#issuecomment-975607891 - **Linux**: Unsupported", + "anyOf": [ + { + "$ref": "#/definitions/WindowEffectsConfig" + }, + { + "type": "null" + } + ] + }, + "incognito": { + "description": "Whether or not the webview should be launched in incognito mode.\n\n## Platform-specific:\n\n- **Android**: Unsupported.", + "default": false, + "type": "boolean" } }, "additionalProperties": false @@ -786,251 +587,284 @@ } ] }, - "CliConfig": { - "description": "describes a CLI configuration\n\nSee more: https://tauri.app/v1/api/config#cliconfig", + "WindowEffectsConfig": { + "description": "The window effects configuration object", "type": "object", + "required": [ + "effects" + ], "properties": { - "description": { - "description": "Command description which will be shown on the help information.", - "type": [ - "string", - "null" - ] - }, - "longDescription": { - "description": "Command long description which will be shown on the help information.", - "type": [ - "string", - "null" - ] - }, - "beforeHelp": { - "description": "Adds additional help information to be displayed in addition to auto-generated help. This information is displayed before the auto-generated help information. This is often used for header information.", - "type": [ - "string", - "null" - ] + "effects": { + "description": "List of Window effects to apply to the Window. Conflicting effects will apply the first one and ignore the rest.", + "type": "array", + "items": { + "$ref": "#/definitions/WindowEffect" + } }, - "afterHelp": { - "description": "Adds additional help information to be displayed in addition to auto-generated help. This information is displayed after the auto-generated help information. This is often used to describe how to use the arguments, or caveats to be noted.", - "type": [ - "string", - "null" + "state": { + "description": "Window effect state **macOS Only**", + "anyOf": [ + { + "$ref": "#/definitions/WindowEffectState" + }, + { + "type": "null" + } ] }, - "args": { - "description": "List of arguments for the command", + "radius": { + "description": "Window effect corner radius **macOS Only**", "type": [ - "array", + "number", "null" ], - "items": { - "$ref": "#/definitions/CliArg" - } + "format": "double" }, - "subcommands": { - "description": "List of subcommands of this command", - "type": [ - "object", - "null" - ], - "additionalProperties": { - "$ref": "#/definitions/CliConfig" - } + "color": { + "description": "Window effect color. Affects [`WindowEffect::Blur`] and [`WindowEffect::Acrylic`] only on Windows 10 v1903+. Doesn't have any effect on Windows 7 or Windows 11.", + "anyOf": [ + { + "$ref": "#/definitions/Color" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false }, - "CliArg": { - "description": "A CLI argument definition.", - "type": "object", - "required": [ - "name" - ], - "properties": { - "short": { - "description": "The short version of the argument, without the preceding -.\n\nNOTE: Any leading `-` characters will be stripped, and only the first non-character will be used as the short version.", - "type": [ - "string", - "null" - ], - "maxLength": 1, - "minLength": 1 + "WindowEffect": { + "description": "Platform-specific window effects", + "oneOf": [ + { + "description": "A default material appropriate for the view's effectiveAppearance. **macOS 10.14-**", + "deprecated": true, + "type": "string", + "enum": [ + "appearanceBased" + ] }, - "name": { - "description": "The unique argument name", - "type": "string" + { + "description": "*macOS 10.14-**", + "deprecated": true, + "type": "string", + "enum": [ + "light" + ] }, - "description": { - "description": "The argument description which will be shown on the help information. Typically, this is a short (one line) description of the arg.", - "type": [ - "string", - "null" + { + "description": "*macOS 10.14-**", + "deprecated": true, + "type": "string", + "enum": [ + "dark" ] }, - "longDescription": { - "description": "The argument long description which will be shown on the help information. Typically this a more detailed (multi-line) message that describes the argument.", - "type": [ - "string", - "null" + { + "description": "*macOS 10.14-**", + "deprecated": true, + "type": "string", + "enum": [ + "mediumLight" ] }, - "takesValue": { - "description": "Specifies that the argument takes a value at run time.\n\nNOTE: values for arguments may be specified in any of the following methods - Using a space such as -o value or --option value - Using an equals and no space such as -o=value or --option=value - Use a short and no space such as -ovalue", - "default": false, - "type": "boolean" + { + "description": "*macOS 10.14-**", + "deprecated": true, + "type": "string", + "enum": [ + "ultraDark" + ] }, - "multiple": { - "description": "Specifies that the argument may have an unknown number of multiple values. Without any other settings, this argument may appear only once.\n\nFor example, --opt val1 val2 is allowed, but --opt val1 val2 --opt val3 is not.\n\nNOTE: Setting this requires `takes_value` to be set to true.", - "default": false, - "type": "boolean" + { + "description": "*macOS 10.10+**", + "type": "string", + "enum": [ + "titlebar" + ] }, - "multipleOccurrences": { - "description": "Specifies that the argument may appear more than once. For flags, this results in the number of occurrences of the flag being recorded. For example -ddd or -d -d -d would count as three occurrences. For options or arguments that take a value, this does not affect how many values they can accept. (i.e. only one at a time is allowed)\n\nFor example, --opt val1 --opt val2 is allowed, but --opt val1 val2 is not.", - "default": false, - "type": "boolean" + { + "description": "*macOS 10.10+**", + "type": "string", + "enum": [ + "selection" + ] }, - "numberOfValues": { - "description": "Specifies how many values are required to satisfy this argument. For example, if you had a `-f ` argument where you wanted exactly 3 'files' you would set `number_of_values = 3`, and this argument wouldn't be satisfied unless the user provided 3 and only 3 values.\n\n**NOTE:** Does *not* require `multiple_occurrences = true` to be set. Setting `multiple_occurrences = true` would allow `-f -f ` where as *not* setting it would only allow one occurrence of this argument.\n\n**NOTE:** implicitly sets `takes_value = true` and `multiple_values = true`.", - "type": [ - "integer", - "null" - ], - "format": "uint", - "minimum": 0.0 + { + "description": "*macOS 10.11+**", + "type": "string", + "enum": [ + "menu" + ] }, - "possibleValues": { - "description": "Specifies a list of possible values for this argument. At runtime, the CLI verifies that only one of the specified values was used, or fails with an error message.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } + { + "description": "*macOS 10.11+**", + "type": "string", + "enum": [ + "popover" + ] }, - "minValues": { - "description": "Specifies the minimum number of values for this argument. For example, if you had a -f `` argument where you wanted at least 2 'files', you would set `minValues: 2`, and this argument would be satisfied if the user provided, 2 or more values.", - "type": [ - "integer", - "null" - ], - "format": "uint", - "minimum": 0.0 + { + "description": "*macOS 10.11+**", + "type": "string", + "enum": [ + "sidebar" + ] }, - "maxValues": { - "description": "Specifies the maximum number of values are for this argument. For example, if you had a -f `` argument where you wanted up to 3 'files', you would set .max_values(3), and this argument would be satisfied if the user provided, 1, 2, or 3 values.", - "type": [ - "integer", - "null" - ], - "format": "uint", - "minimum": 0.0 + { + "description": "*macOS 10.14+**", + "type": "string", + "enum": [ + "headerView" + ] }, - "required": { - "description": "Sets whether or not the argument is required by default.\n\n- Required by default means it is required, when no other conflicting rules have been evaluated - Conflicting rules take precedence over being required.", - "default": false, - "type": "boolean" + { + "description": "*macOS 10.14+**", + "type": "string", + "enum": [ + "sheet" + ] }, - "requiredUnlessPresent": { - "description": "Sets an arg that override this arg's required setting i.e. this arg will be required unless this other argument is present.", - "type": [ - "string", - "null" + { + "description": "*macOS 10.14+**", + "type": "string", + "enum": [ + "windowBackground" ] }, - "requiredUnlessPresentAll": { - "description": "Sets args that override this arg's required setting i.e. this arg will be required unless all these other arguments are present.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } + { + "description": "*macOS 10.14+**", + "type": "string", + "enum": [ + "hudWindow" + ] }, - "requiredUnlessPresentAny": { - "description": "Sets args that override this arg's required setting i.e. this arg will be required unless at least one of these other arguments are present.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } + { + "description": "*macOS 10.14+**", + "type": "string", + "enum": [ + "fullScreenUI" + ] }, - "conflictsWith": { - "description": "Sets a conflicting argument by name i.e. when using this argument, the following argument can't be present and vice versa.", - "type": [ - "string", - "null" + { + "description": "*macOS 10.14+**", + "type": "string", + "enum": [ + "tooltip" ] }, - "conflictsWithAll": { - "description": "The same as conflictsWith but allows specifying multiple two-way conflicts per argument.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } + { + "description": "*macOS 10.14+**", + "type": "string", + "enum": [ + "contentBackground" + ] }, - "requires": { - "description": "Tets an argument by name that is required when this one is present i.e. when using this argument, the following argument must be present.", - "type": [ - "string", - "null" + { + "description": "*macOS 10.14+**", + "type": "string", + "enum": [ + "underWindowBackground" ] }, - "requiresAll": { - "description": "Sts multiple arguments by names that are required when this one is present i.e. when using this argument, the following arguments must be present.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } + { + "description": "*macOS 10.14+**", + "type": "string", + "enum": [ + "underPageBackground" + ] }, - "requiresIf": { - "description": "Allows a conditional requirement with the signature [arg, value] the requirement will only become valid if `arg`'s value equals `${value}`.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } + { + "description": "Mica effect that matches the system dark perefence **Windows 11 Only**", + "type": "string", + "enum": [ + "mica" + ] }, - "requiredIfEq": { - "description": "Allows specifying that an argument is required conditionally with the signature [arg, value] the requirement will only become valid if the `arg`'s value equals `${value}`.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } + { + "description": "Mica effect with dark mode but only if dark mode is enabled on the system **Windows 11 Only**", + "type": "string", + "enum": [ + "micaDark" + ] }, - "requireEquals": { - "description": "Requires that options use the --option=val syntax i.e. an equals between the option and associated value.", - "type": [ - "boolean", - "null" + { + "description": "Mica effect with light mode **Windows 11 Only**", + "type": "string", + "enum": [ + "micaLight" ] }, - "index": { - "description": "The positional argument index, starting at 1.\n\nThe index refers to position according to other positional argument. It does not define position in the argument list as a whole. When utilized with multiple=true, only the last positional argument may be defined as multiple (i.e. the one with the highest index).", - "type": [ - "integer", - "null" - ], - "format": "uint", - "minimum": 1.0 + { + "description": "**Windows 7/10/11(22H1) Only**\n\n## Notes\n\nThis effect has bad performance when resizing/dragging the window on Windows 11 build 22621.", + "type": "string", + "enum": [ + "blur" + ] + }, + { + "description": "**Windows 10/11 Only**\n\n## Notes\n\nThis effect has bad performance when resizing/dragging the window on Windows 10 v1903+ and Windows 11 build 22000.", + "type": "string", + "enum": [ + "acrylic" + ] } - }, - "additionalProperties": false + ] + }, + "WindowEffectState": { + "description": "Window effect state **macOS only**\n\n", + "oneOf": [ + { + "description": "Make window effect state follow the window's active state", + "type": "string", + "enum": [ + "followsWindowActiveState" + ] + }, + { + "description": "Make window effect state always active", + "type": "string", + "enum": [ + "active" + ] + }, + { + "description": "Make window effect state always inactive", + "type": "string", + "enum": [ + "inactive" + ] + } + ] + }, + "Color": { + "description": "a tuple struct of RGBA colors. Each value has minimum of 0 and maximum of 255.", + "type": "array", + "items": [ + { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + } + ], + "maxItems": 4, + "minItems": 4 }, "BundleConfig": { "description": "Configuration for tauri-bundler.\n\nSee more: https://tauri.app/v1/api/config#bundleconfig", @@ -1096,6 +930,16 @@ "null" ] }, + "fileAssociations": { + "description": "File associations to application.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/FileAssociation" + } + }, "shortDescription": { "description": "A short description of your application.", "type": [ @@ -1174,6 +1018,41 @@ "$ref": "#/definitions/WindowsConfig" } ] + }, + "iOS": { + "description": "iOS configuration.", + "default": {}, + "allOf": [ + { + "$ref": "#/definitions/IosConfig" + } + ] + }, + "android": { + "description": "Android configuration.", + "default": { + "minSdkVersion": 24 + }, + "allOf": [ + { + "$ref": "#/definitions/AndroidConfig" + } + ] + }, + "updater": { + "description": "The updater configuration.", + "default": { + "active": false, + "pubkey": "", + "windows": { + "installMode": "passive" + } + }, + "allOf": [ + { + "$ref": "#/definitions/UpdaterConfig" + } + ] } }, "additionalProperties": false @@ -1258,6 +1137,97 @@ } ] }, + "FileAssociation": { + "description": "File association", + "type": "object", + "required": [ + "ext" + ], + "properties": { + "ext": { + "description": "File extensions to associate with this app. e.g. 'png'", + "type": "array", + "items": { + "$ref": "#/definitions/AssociationExt" + } + }, + "name": { + "description": "The name. Maps to `CFBundleTypeName` on macOS. Default to ext[0]", + "type": [ + "string", + "null" + ] + }, + "description": { + "description": "The association description. Windows-only. It is displayed on the `Type` column on Windows Explorer.", + "type": [ + "string", + "null" + ] + }, + "role": { + "description": "The app’s role with respect to the type. Maps to `CFBundleTypeRole` on macOS.", + "default": "Editor", + "allOf": [ + { + "$ref": "#/definitions/BundleTypeRole" + } + ] + }, + "mimeType": { + "description": "The mime-type e.g. 'image/png' or 'text/plain'. Linux-only.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "AssociationExt": { + "description": "An extension for a [`FileAssociation`].\n\nA leading `.` is automatically stripped.", + "type": "string" + }, + "BundleTypeRole": { + "description": "macOS-only. Corresponds to CFBundleTypeRole", + "oneOf": [ + { + "description": "CFBundleTypeRole.Editor. Files can be read and edited.", + "type": "string", + "enum": [ + "Editor" + ] + }, + { + "description": "CFBundleTypeRole.Viewer. Files can be read.", + "type": "string", + "enum": [ + "Viewer" + ] + }, + { + "description": "CFBundleTypeRole.Shell", + "type": "string", + "enum": [ + "Shell" + ] + }, + { + "description": "CFBundleTypeRole.QLGenerator", + "type": "string", + "enum": [ + "QLGenerator" + ] + }, + { + "description": "CFBundleTypeRole.None", + "type": "string", + "enum": [ + "None" + ] + } + ] + }, "AppImageConfig": { "description": "Configuration for AppImage bundles.\n\nSee more: https://tauri.app/v1/api/config#appimageconfig", "type": "object", @@ -1757,6 +1727,17 @@ "description": "Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not. By default the OS language is selected, with a fallback to the first language in the `languages` array.", "default": false, "type": "boolean" + }, + "compression": { + "description": "Set the compression algorithm used to compress files in the installer.\n\nSee ", + "anyOf": [ + { + "$ref": "#/definitions/NsisCompression" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -1787,854 +1768,129 @@ } ] }, - "AllowlistConfig": { - "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": { - "description": "Use this flag to enable all API features.", - "default": false, - "type": "boolean" - }, - "fs": { - "description": "File system API allowlist.", - "default": { - "all": false, - "copyFile": false, - "createDir": false, - "exists": false, - "readDir": false, - "readFile": false, - "removeDir": false, - "removeFile": false, - "renameFile": false, - "scope": [], - "writeFile": false - }, - "allOf": [ - { - "$ref": "#/definitions/FsAllowlistConfig" - } - ] - }, - "window": { - "description": "Window API allowlist.", - "default": { - "all": false, - "center": false, - "close": false, - "create": false, - "hide": false, - "maximize": false, - "minimize": false, - "print": false, - "requestUserAttention": false, - "setAlwaysOnTop": false, - "setClosable": false, - "setContentProtected": false, - "setCursorGrab": false, - "setCursorIcon": false, - "setCursorPosition": false, - "setCursorVisible": false, - "setDecorations": false, - "setFocus": false, - "setFullscreen": false, - "setIcon": false, - "setIgnoreCursorEvents": false, - "setMaxSize": false, - "setMaximizable": false, - "setMinSize": false, - "setMinimizable": false, - "setPosition": false, - "setResizable": false, - "setSize": false, - "setSkipTaskbar": false, - "setTitle": false, - "show": false, - "startDragging": false, - "unmaximize": false, - "unminimize": false - }, - "allOf": [ - { - "$ref": "#/definitions/WindowAllowlistConfig" - } + "NsisCompression": { + "description": "Compression algorithms used in the NSIS installer.\n\nSee ", + "oneOf": [ + { + "description": "ZLIB uses the deflate algorithm, it is a quick and simple method. With the default compression level it uses about 300 KB of memory.", + "type": "string", + "enum": [ + "zlib" ] }, - "shell": { - "description": "Shell API allowlist.", - "default": { - "all": false, - "execute": false, - "open": false, - "scope": [], - "sidecar": false - }, - "allOf": [ - { - "$ref": "#/definitions/ShellAllowlistConfig" - } + { + "description": "BZIP2 usually gives better compression ratios than ZLIB, but it is a bit slower and uses more memory. With the default compression level it uses about 4 MB of memory.", + "type": "string", + "enum": [ + "bzip2" ] }, - "dialog": { - "description": "Dialog API allowlist.", - "default": { - "all": false, - "ask": false, - "confirm": false, - "message": false, - "open": false, - "save": false - }, - "allOf": [ - { - "$ref": "#/definitions/DialogAllowlistConfig" - } + { + "description": "LZMA (default) is a new compression method that gives very good compression ratios. The decompression speed is high (10-20 MB/s on a 2 GHz CPU), the compression speed is lower. The memory size that will be used for decompression is the dictionary size plus a few KBs, the default is 8 MB.", + "type": "string", + "enum": [ + "lzma" ] - }, - "http": { - "description": "HTTP API allowlist.", - "default": { - "all": false, - "request": false, - "scope": [] - }, - "allOf": [ - { - "$ref": "#/definitions/HttpAllowlistConfig" - } - ] - }, - "notification": { - "description": "Notification API allowlist.", - "default": { - "all": false - }, - "allOf": [ - { - "$ref": "#/definitions/NotificationAllowlistConfig" - } - ] - }, - "globalShortcut": { - "description": "Global shortcut API allowlist.", - "default": { - "all": false - }, - "allOf": [ - { - "$ref": "#/definitions/GlobalShortcutAllowlistConfig" - } - ] - }, - "os": { - "description": "OS allowlist.", - "default": { - "all": false - }, - "allOf": [ - { - "$ref": "#/definitions/OsAllowlistConfig" - } - ] - }, - "path": { - "description": "Path API allowlist.", - "default": { - "all": false - }, - "allOf": [ - { - "$ref": "#/definitions/PathAllowlistConfig" - } - ] - }, - "protocol": { - "description": "Custom protocol allowlist.", - "default": { - "all": false, - "asset": false, - "assetScope": [] - }, - "allOf": [ - { - "$ref": "#/definitions/ProtocolAllowlistConfig" - } - ] - }, - "process": { - "description": "Process API allowlist.", - "default": { - "all": false, - "exit": false, - "relaunch": false, - "relaunchDangerousAllowSymlinkMacos": false - }, - "allOf": [ - { - "$ref": "#/definitions/ProcessAllowlistConfig" - } - ] - }, - "clipboard": { - "description": "Clipboard APIs allowlist.", - "default": { - "all": false, - "readText": false, - "writeText": false - }, - "allOf": [ - { - "$ref": "#/definitions/ClipboardAllowlistConfig" - } - ] - }, - "app": { - "description": "App APIs allowlist.", - "default": { - "all": false, - "hide": false, - "show": false - }, - "allOf": [ - { - "$ref": "#/definitions/AppAllowlistConfig" - } - ] - } - }, - "additionalProperties": false - }, - "FsAllowlistConfig": { - "description": "Allowlist for the file system APIs.\n\nSee more: https://tauri.app/v1/api/config#fsallowlistconfig", - "type": "object", - "properties": { - "scope": { - "description": "The access scope for the filesystem APIs.", - "default": [], - "allOf": [ - { - "$ref": "#/definitions/FsAllowlistScope" - } - ] - }, - "all": { - "description": "Use this flag to enable all file system API features.", - "default": false, - "type": "boolean" - }, - "readFile": { - "description": "Read file from local filesystem.", - "default": false, - "type": "boolean" - }, - "writeFile": { - "description": "Write file to local filesystem.", - "default": false, - "type": "boolean" - }, - "readDir": { - "description": "Read directory from local filesystem.", - "default": false, - "type": "boolean" - }, - "copyFile": { - "description": "Copy file from local filesystem.", - "default": false, - "type": "boolean" - }, - "createDir": { - "description": "Create directory from local filesystem.", - "default": false, - "type": "boolean" - }, - "removeDir": { - "description": "Remove directory from local filesystem.", - "default": false, - "type": "boolean" - }, - "removeFile": { - "description": "Remove file from local filesystem.", - "default": false, - "type": "boolean" - }, - "renameFile": { - "description": "Rename file from local filesystem.", - "default": false, - "type": "boolean" - }, - "exists": { - "description": "Check if path exists on the local filesystem.", - "default": false, - "type": "boolean" - } - }, - "additionalProperties": false - }, - "FsAllowlistScope": { - "description": "Filesystem scope definition. It is a list of glob patterns that restrict the API access from the webview.\n\nEach pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "anyOf": [ - { - "description": "A list of paths that are allowed by this scope.", - "type": "array", - "items": { - "type": "string" - } - }, - { - "description": "A complete scope configuration.", - "type": "object", - "properties": { - "allow": { - "description": "A list of paths that are allowed by this scope.", - "default": [], - "type": "array", - "items": { - "type": "string" - } - }, - "deny": { - "description": "A list of paths that are not allowed by this scope. This gets precedence over the [`Self::Scope::allow`] list.", - "default": [], - "type": "array", - "items": { - "type": "string" - } - }, - "requireLiteralLeadingDot": { - "description": "Whether or not paths that contain components that start with a `.` will require that `.` appears literally in the pattern; `*`, `?`, `**`, or `[...]` will not match. This is useful because such files are conventionally considered hidden on Unix systems and it might be desirable to skip them when listing files.\n\nDefaults to `true` on Unix systems and `false` on Windows", - "type": [ - "boolean", - "null" - ] - } - } - } - ] - }, - "WindowAllowlistConfig": { - "description": "Allowlist for the window APIs.\n\nSee more: https://tauri.app/v1/api/config#windowallowlistconfig", - "type": "object", - "properties": { - "all": { - "description": "Use this flag to enable all window API features.", - "default": false, - "type": "boolean" - }, - "create": { - "description": "Allows dynamic window creation.", - "default": false, - "type": "boolean" - }, - "center": { - "description": "Allows centering the window.", - "default": false, - "type": "boolean" - }, - "requestUserAttention": { - "description": "Allows requesting user attention on the window.", - "default": false, - "type": "boolean" - }, - "setResizable": { - "description": "Allows setting the resizable flag of the window.", - "default": false, - "type": "boolean" - }, - "setMaximizable": { - "description": "Allows setting whether the window's native maximize button is enabled or not.", - "default": false, - "type": "boolean" - }, - "setMinimizable": { - "description": "Allows setting whether the window's native minimize button is enabled or not.", - "default": false, - "type": "boolean" - }, - "setClosable": { - "description": "Allows setting whether the window's native close button is enabled or not.", - "default": false, - "type": "boolean" - }, - "setTitle": { - "description": "Allows changing the window title.", - "default": false, - "type": "boolean" - }, - "maximize": { - "description": "Allows maximizing the window.", - "default": false, - "type": "boolean" - }, - "unmaximize": { - "description": "Allows unmaximizing the window.", - "default": false, - "type": "boolean" - }, - "minimize": { - "description": "Allows minimizing the window.", - "default": false, - "type": "boolean" - }, - "unminimize": { - "description": "Allows unminimizing the window.", - "default": false, - "type": "boolean" - }, - "show": { - "description": "Allows showing the window.", - "default": false, - "type": "boolean" - }, - "hide": { - "description": "Allows hiding the window.", - "default": false, - "type": "boolean" - }, - "close": { - "description": "Allows closing the window.", - "default": false, - "type": "boolean" - }, - "setDecorations": { - "description": "Allows setting the decorations flag of the window.", - "default": false, - "type": "boolean" - }, - "setAlwaysOnTop": { - "description": "Allows setting the always_on_top flag of the window.", - "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, - "type": "boolean" - }, - "setMinSize": { - "description": "Allows setting the window minimum size.", - "default": false, - "type": "boolean" - }, - "setMaxSize": { - "description": "Allows setting the window maximum size.", - "default": false, - "type": "boolean" - }, - "setPosition": { - "description": "Allows changing the position of the window.", - "default": false, - "type": "boolean" - }, - "setFullscreen": { - "description": "Allows setting the fullscreen flag of the window.", - "default": false, - "type": "boolean" - }, - "setFocus": { - "description": "Allows focusing the window.", - "default": false, - "type": "boolean" - }, - "setIcon": { - "description": "Allows changing the window icon.", - "default": false, - "type": "boolean" - }, - "setSkipTaskbar": { - "description": "Allows setting the skip_taskbar flag of the window.", - "default": false, - "type": "boolean" - }, - "setCursorGrab": { - "description": "Allows grabbing the cursor.", - "default": false, - "type": "boolean" - }, - "setCursorVisible": { - "description": "Allows setting the cursor visibility.", - "default": false, - "type": "boolean" - }, - "setCursorIcon": { - "description": "Allows changing the cursor icon.", - "default": false, - "type": "boolean" - }, - "setCursorPosition": { - "description": "Allows setting the cursor position.", - "default": false, - "type": "boolean" - }, - "setIgnoreCursorEvents": { - "description": "Allows ignoring cursor events.", - "default": false, - "type": "boolean" - }, - "startDragging": { - "description": "Allows start dragging on the window.", - "default": false, - "type": "boolean" - }, - "print": { - "description": "Allows opening the system dialog to print the window content.", - "default": false, - "type": "boolean" - } - }, - "additionalProperties": false - }, - "ShellAllowlistConfig": { - "description": "Allowlist for the shell APIs.\n\nSee more: https://tauri.app/v1/api/config#shellallowlistconfig", - "type": "object", - "properties": { - "scope": { - "description": "Access scope for the binary execution APIs. Sidecars are automatically enabled.", - "default": [], - "allOf": [ - { - "$ref": "#/definitions/ShellAllowlistScope" - } - ] - }, - "all": { - "description": "Use this flag to enable all shell API features.", - "default": false, - "type": "boolean" - }, - "execute": { - "description": "Enable binary execution.", - "default": false, - "type": "boolean" - }, - "sidecar": { - "description": "Enable sidecar execution, allowing the JavaScript layer to spawn a sidecar command, an executable that is shipped with the application. For more information see .", - "default": false, - "type": "boolean" - }, - "open": { - "description": "Open URL with the user's default application.", - "default": false, - "allOf": [ - { - "$ref": "#/definitions/ShellAllowlistOpen" - } - ] - } - }, - "additionalProperties": false - }, - "ShellAllowlistScope": { - "description": "Shell scope definition. It is a list of command names and associated CLI arguments that restrict the API access from the webview.", - "type": "array", - "items": { - "$ref": "#/definitions/ShellAllowedCommand" - } - }, - "ShellAllowedCommand": { - "description": "A command allowed to be executed by the webview API.", - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "default": "", - "type": "string" - }, - "args": { - "description": "The allowed arguments for the command execution.", - "default": false, - "allOf": [ - { - "$ref": "#/definitions/ShellAllowedArgs" - } - ] - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "default": false, - "type": "boolean" - } - } - }, - "ShellAllowedArgs": { - "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", - "anyOf": [ - { - "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", - "type": "boolean" - }, - { - "description": "A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.", - "type": "array", - "items": { - "$ref": "#/definitions/ShellAllowedArg" - } - } - ] - }, - "ShellAllowedArg": { - "description": "A command argument allowed to be executed by the webview API.", - "anyOf": [ - { - "description": "A non-configurable argument that is passed to the command in the order it was specified.", - "type": "string" - }, - { - "description": "A variable that is set while calling the command from the webview API.", - "type": "object", - "required": [ - "validator" - ], - "properties": { - "validator": { - "description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\n[regex]: https://docs.rs/regex/latest/regex/#syntax", - "type": "string" - } - }, - "additionalProperties": false - } - ] - }, - "ShellAllowlistOpen": { - "description": "Defines the `shell > open` api scope.", - "anyOf": [ - { - "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" - }, - { - "description": "Enable the shell open API, with a custom regex that the opened path must match against.\n\nIf using a custom regex to support a non-http(s) schema, care should be used to prevent values that allow flag-like strings to pass validation. e.g. `--enable-debugging`, `-i`, `/R`.", - "type": "string" - } - ] - }, - "DialogAllowlistConfig": { - "description": "Allowlist for the dialog APIs.\n\nSee more: https://tauri.app/v1/api/config#dialogallowlistconfig", - "type": "object", - "properties": { - "all": { - "description": "Use this flag to enable all dialog API features.", - "default": false, - "type": "boolean" - }, - "open": { - "description": "Allows the API to open a dialog window to pick files.", - "default": false, - "type": "boolean" - }, - "save": { - "description": "Allows the API to open a dialog window to pick where to save files.", - "default": false, - "type": "boolean" - }, - "message": { - "description": "Allows the API to show a message dialog window.", - "default": false, - "type": "boolean" - }, - "ask": { - "description": "Allows the API to show a dialog window with Yes/No buttons.", - "default": false, - "type": "boolean" - }, - "confirm": { - "description": "Allows the API to show a dialog window with Ok/Cancel buttons.", - "default": false, - "type": "boolean" } - }, - "additionalProperties": false + ] }, - "HttpAllowlistConfig": { - "description": "Allowlist for the HTTP APIs.\n\nSee more: https://tauri.app/v1/api/config#httpallowlistconfig", + "IosConfig": { + "description": "General configuration for the iOS target.", "type": "object", "properties": { - "scope": { - "description": "The access scope for the HTTP APIs.", - "default": [], - "allOf": [ - { - "$ref": "#/definitions/HttpAllowlistScope" - } + "developmentTeam": { + "description": "The development team. This value is required for iOS development because code signing is enforced. The `TAURI_APPLE_DEVELOPMENT_TEAM` environment variable can be set to overwrite it.", + "type": [ + "string", + "null" ] - }, - "all": { - "description": "Use this flag to enable all HTTP API features.", - "default": false, - "type": "boolean" - }, - "request": { - "description": "Allows making HTTP requests.", - "default": false, - "type": "boolean" - } - }, - "additionalProperties": false - }, - "HttpAllowlistScope": { - "description": "HTTP API scope definition. It is a list of URLs that can be accessed by the webview when using the HTTP APIs. The scoped URL is matched against the request URL using a glob pattern.\n\nExamples: - \"https://*\": allows all HTTPS urls - \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path - \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"", - "type": "array", - "items": { - "type": "string", - "format": "uri" - } - }, - "NotificationAllowlistConfig": { - "description": "Allowlist for the notification APIs.\n\nSee more: https://tauri.app/v1/api/config#notificationallowlistconfig", - "type": "object", - "properties": { - "all": { - "description": "Use this flag to enable all notification API features.", - "default": false, - "type": "boolean" - } - }, - "additionalProperties": false - }, - "GlobalShortcutAllowlistConfig": { - "description": "Allowlist for the global shortcut APIs.\n\nSee more: https://tauri.app/v1/api/config#globalshortcutallowlistconfig", - "type": "object", - "properties": { - "all": { - "description": "Use this flag to enable all global shortcut API features.", - "default": false, - "type": "boolean" } }, "additionalProperties": false }, - "OsAllowlistConfig": { - "description": "Allowlist for the OS APIs.\n\nSee more: https://tauri.app/v1/api/config#osallowlistconfig", + "AndroidConfig": { + "description": "General configuration for the iOS target.", "type": "object", "properties": { - "all": { - "description": "Use this flag to enable all OS API features.", - "default": false, - "type": "boolean" + "minSdkVersion": { + "description": "The minimum API level required for the application to run. The Android system will prevent the user from installing the application if the system's API level is lower than the value specified.", + "default": 24, + "type": "integer", + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false }, - "PathAllowlistConfig": { - "description": "Allowlist for the path APIs.\n\nSee more: https://tauri.app/v1/api/config#pathallowlistconfig", + "UpdaterConfig": { + "description": "The Updater configuration object.\n\nSee more: https://tauri.app/v1/api/config#updaterconfig", "type": "object", "properties": { - "all": { - "description": "Use this flag to enable all path API features.", + "active": { + "description": "Whether the updater is active or not.", "default": false, "type": "boolean" - } - }, - "additionalProperties": false - }, - "ProtocolAllowlistConfig": { - "description": "Allowlist for the custom protocols.\n\nSee more: https://tauri.app/v1/api/config#protocolallowlistconfig", - "type": "object", - "properties": { - "assetScope": { - "description": "The access scope for the asset protocol.", - "default": [], + }, + "pubkey": { + "description": "Signature public key.", + "default": "", + "type": "string" + }, + "windows": { + "description": "The Windows configuration for the updater.", + "default": { + "installMode": "passive" + }, "allOf": [ { - "$ref": "#/definitions/FsAllowlistScope" + "$ref": "#/definitions/UpdaterWindowsConfig" } ] - }, - "all": { - "description": "Use this flag to enable all custom protocols.", - "default": false, - "type": "boolean" - }, - "asset": { - "description": "Enables the asset protocol.", - "default": false, - "type": "boolean" - } - }, - "additionalProperties": false - }, - "ProcessAllowlistConfig": { - "description": "Allowlist for the process APIs.\n\nSee more: https://tauri.app/v1/api/config#processallowlistconfig", - "type": "object", - "properties": { - "all": { - "description": "Use this flag to enable all process APIs.", - "default": false, - "type": "boolean" - }, - "relaunch": { - "description": "Enables the relaunch API.", - "default": false, - "type": "boolean" - }, - "relaunchDangerousAllowSymlinkMacos": { - "description": "Dangerous option that allows macOS to relaunch even if the binary contains a symlink.\n\nThis is due to macOS having less symlink protection. Highly recommended to not set this flag unless you have a very specific reason too, and understand the implications of it.", - "default": false, - "type": "boolean" - }, - "exit": { - "description": "Enables the exit API.", - "default": false, - "type": "boolean" } }, "additionalProperties": false }, - "ClipboardAllowlistConfig": { - "description": "Allowlist for the clipboard APIs.\n\nSee more: https://tauri.app/v1/api/config#clipboardallowlistconfig", + "UpdaterWindowsConfig": { + "description": "The updater configuration for Windows.\n\nSee more: https://tauri.app/v1/api/config#updaterwindowsconfig", "type": "object", "properties": { - "all": { - "description": "Use this flag to enable all clipboard APIs.", - "default": false, - "type": "boolean" - }, - "writeText": { - "description": "Enables the clipboard's `writeText` API.", - "default": false, - "type": "boolean" - }, - "readText": { - "description": "Enables the clipboard's `readText` API.", - "default": false, - "type": "boolean" + "installMode": { + "description": "The installation mode for the update on Windows. Defaults to `passive`.", + "default": "passive", + "allOf": [ + { + "$ref": "#/definitions/WindowsUpdateInstallMode" + } + ] } }, "additionalProperties": false }, - "AppAllowlistConfig": { - "description": "Allowlist for the app APIs.\n\nSee more: https://tauri.app/v1/api/config#appallowlistconfig", - "type": "object", - "properties": { - "all": { - "description": "Use this flag to enable all app APIs.", - "default": false, - "type": "boolean" + "WindowsUpdateInstallMode": { + "description": "Install modes for the Windows update.", + "oneOf": [ + { + "description": "Specifies there's a basic UI during the installation process, including a final dialog box at the end.", + "type": "string", + "enum": [ + "basicUi" + ] }, - "show": { - "description": "Enables the app's `show` API.", - "default": false, - "type": "boolean" + { + "description": "The quiet mode means there's no user interaction required. Requires admin privileges if the installer does.", + "type": "string", + "enum": [ + "quiet" + ] }, - "hide": { - "description": "Enables the app's `hide` API.", - "default": false, - "type": "boolean" + { + "description": "Specifies unattended mode, which means the installation only shows a progress bar.", + "type": "string", + "enum": [ + "passive" + ] } - }, - "additionalProperties": false + ] }, "SecurityConfig": { "description": "Security configuration.\n\nSee more: https://tauri.app/v1/api/config#securityconfig", @@ -2683,6 +1939,18 @@ "items": { "$ref": "#/definitions/RemoteDomainAccessScope" } + }, + "assetProtocol": { + "description": "Custom protocol config.", + "default": { + "enable": false, + "scope": [] + }, + "allOf": [ + { + "$ref": "#/definitions/AssetProtocolConfig" + } + ] } }, "additionalProperties": false @@ -2768,123 +2036,81 @@ "items": { "type": "string" } - }, - "enableTauriAPI": { - "description": "Enables access to the Tauri API.", - "default": false, - "type": "boolean" } }, "additionalProperties": false }, - "UpdaterConfig": { - "description": "The Updater configuration object.\n\nSee more: https://tauri.app/v1/api/config#updaterconfig", + "AssetProtocolConfig": { + "description": "Config for the asset custom protocol.\n\nSee more: https://tauri.app/v1/api/config#assetprotocolconfig", "type": "object", "properties": { - "active": { - "description": "Whether the updater is active or not.", - "default": false, - "type": "boolean" - }, - "dialog": { - "description": "Display built-in dialog or use event system if disabled.", - "default": true, - "type": "boolean" - }, - "endpoints": { - "description": "The updater endpoints. TLS is enforced on production.\n\nThe updater URL can contain the following variables: - {{current_version}}: The version of the app that is requesting the update - {{target}}: The operating system name (one of `linux`, `windows` or `darwin`). - {{arch}}: The architecture of the machine (one of `x86_64`, `i686`, `aarch64` or `armv7`).\n\n# Examples - \"https://my.cdn.com/latest.json\": a raw JSON endpoint that returns the latest version and download links for each platform. - \"https://updates.app.dev/{{target}}?version={{current_version}}&arch={{arch}}\": a dedicated API with positional and query string arguments.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/UpdaterEndpoint" - } - }, - "pubkey": { - "description": "Signature public key.", - "default": "", - "type": "string" - }, - "windows": { - "description": "The Windows configuration for the updater.", - "default": { - "installMode": "passive", - "installerArgs": [] - }, + "scope": { + "description": "The access scope for the asset protocol.", + "default": [], "allOf": [ { - "$ref": "#/definitions/UpdaterWindowsConfig" + "$ref": "#/definitions/FsScope" } ] + }, + "enable": { + "description": "Enables the asset protocol.", + "default": false, + "type": "boolean" } }, "additionalProperties": false }, - "UpdaterEndpoint": { - "description": "A URL to an updater server.\n\nThe URL must use the `https` scheme on production.", - "type": "string", - "format": "uri" - }, - "UpdaterWindowsConfig": { - "description": "The updater configuration for Windows.\n\nSee more: https://tauri.app/v1/api/config#updaterwindowsconfig", - "type": "object", - "properties": { - "installerArgs": { - "description": "Additional arguments given to the NSIS or WiX installer.", - "default": [], + "FsScope": { + "description": "Protocol scope definition. It is a list of glob patterns that restrict the API access from the webview.\n\nEach pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", + "anyOf": [ + { + "description": "A list of paths that are allowed by this scope.", "type": "array", "items": { "type": "string" } }, - "installMode": { - "description": "The installation mode for the update on Windows. Defaults to `passive`.", - "default": "passive", - "allOf": [ - { - "$ref": "#/definitions/WindowsUpdateInstallMode" - } - ] - } - }, - "additionalProperties": false - }, - "WindowsUpdateInstallMode": { - "description": "Install modes for the Windows update.", - "oneOf": [ - { - "description": "Specifies there's a basic UI during the installation process, including a final dialog box at the end.", - "type": "string", - "enum": [ - "basicUi" - ] - }, - { - "description": "The quiet mode means there's no user interaction required. Requires admin privileges if the installer does.", - "type": "string", - "enum": [ - "quiet" - ] - }, { - "description": "Specifies unattended mode, which means the installation only shows a progress bar.", - "type": "string", - "enum": [ - "passive" - ] + "description": "A complete scope configuration.", + "type": "object", + "properties": { + "allow": { + "description": "A list of paths that are allowed by this scope.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "A list of paths that are not allowed by this scope. This gets precedence over the [`Self::Scope::allow`] list.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "requireLiteralLeadingDot": { + "description": "Whether or not paths that contain components that start with a `.` will require that `.` appears literally in the pattern; `*`, `?`, `**`, or `[...]` will not match. This is useful because such files are conventionally considered hidden on Unix systems and it might be desirable to skip them when listing files.\n\nDefaults to `true` on Unix systems and `false` on Windows", + "type": [ + "boolean", + "null" + ] + } + } } ] }, - "SystemTrayConfig": { - "description": "Configuration for application system tray icon.\n\nSee more: https://tauri.app/v1/api/config#systemtrayconfig", + "TrayIconConfig": { + "description": "Configuration for application tray icon.\n\nSee more: https://tauri.app/v1/api/config#trayiconconfig", "type": "object", "required": [ "iconPath" ], "properties": { "iconPath": { - "description": "Path to the default icon to use on the system tray.", + "description": "Path to the default icon to use for the tray icon.", "type": "string" }, "iconAsTemplate": { @@ -2903,6 +2129,13 @@ "string", "null" ] + }, + "tooltip": { + "description": "Tray icon tooltip on Windows and macOS", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/core/tauri-config-schema/src/main.rs b/core/tauri-config-schema/src/main.rs index e6c5244c75c..df66520e3bc 100644 --- a/core/tauri-config-schema/src/main.rs +++ b/core/tauri-config-schema/src/main.rs @@ -2,4 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! [![](https://github.com/tauri-apps/tauri/raw/dev/.github/splash.png)](https://tauri.app) +//! +//! Hosts the schema for the Tauri configuration file. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + fn main() {} diff --git a/core/tauri-macros/CHANGELOG.md b/core/tauri-macros/CHANGELOG.md index f3a4f4b782a..20c0cffaf76 100644 --- a/core/tauri-macros/CHANGELOG.md +++ b/core/tauri-macros/CHANGELOG.md @@ -1,5 +1,65 @@ # Changelog +## \[2.0.0-alpha.7] + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.7` +- Upgraded to `tauri-codegen@2.0.0-alpha.7` + +### Breaking Changes + +- [`fbeb5b91`](https://www.github.com/tauri-apps/tauri/commit/fbeb5b9185baeda19e865228179e3e44c165f1d9)([#7170](https://www.github.com/tauri-apps/tauri/pull/7170)) Moved `tauri::api::ipc` to `tauri::ipc` and refactored all types. + +## \[2.0.0-alpha.6] + +### Dependencies + +- Updated to latest `tauri-utils` + +## \[2.0.0-alpha.5] + +- [`7a4b1fb9`](https://www.github.com/tauri-apps/tauri/commit/7a4b1fb96da475053c61960f362bbecf18cd00d4)([#6839](https://www.github.com/tauri-apps/tauri/pull/6839)) Added support to attibutes for each command path in the `generate_handler` macro. +- [`96639ca2`](https://www.github.com/tauri-apps/tauri/commit/96639ca239c9e4f75142fc07868ac46822111cff)([#6749](https://www.github.com/tauri-apps/tauri/pull/6749)) Moved the `shell` functionality to its own plugin in the plugins-workspace repository. +- [`3188f376`](https://www.github.com/tauri-apps/tauri/commit/3188f3764978c6d1452ee31d5a91469691e95094)([#6883](https://www.github.com/tauri-apps/tauri/pull/6883)) Bump the MSRV to 1.65. +- [`9a79dc08`](https://www.github.com/tauri-apps/tauri/commit/9a79dc085870e0c1a5df13481ff271b8c6cc3b78)([#6947](https://www.github.com/tauri-apps/tauri/pull/6947)) Removed the module command macros. + +## \[2.0.0-alpha.4] + +- Added `android` configuration object under `tauri > bundle`. + - Bumped due to a bump in tauri-utils. + - [db4c9dc6](https://www.github.com/tauri-apps/tauri/commit/db4c9dc655e07ee2184fe04571f500f7910890cd) feat(core): add option to configure Android's minimum SDK version ([#6651](https://www.github.com/tauri-apps/tauri/pull/6651)) on 2023-04-07 + +## \[2.0.0-alpha.3] + +- Pull changes from Tauri 1.3 release. + - [](https://www.github.com/tauri-apps/tauri/commit/undefined) on undefined + +## \[2.0.0-alpha.2] + +- Resolve Android package name from single word bundle identifiers. + - [60a8b07d](https://www.github.com/tauri-apps/tauri/commit/60a8b07dc7c56c9c45331cb57d9afb410e7eadf3) fix: handle single word bundle identifier when resolving Android domain ([#6313](https://www.github.com/tauri-apps/tauri/pull/6313)) on 2023-02-19 +- Return `bool` in the invoke handler. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 +- Refactored the implementation of the `mobile_entry_point` macro. + - [9feab904](https://www.github.com/tauri-apps/tauri/commit/9feab904bf08b5c168d4779c21d0419409a68d30) feat(core): add API to call Android plugin ([#6239](https://www.github.com/tauri-apps/tauri/pull/6239)) on 2023-02-10 + +## \[2.0.0-alpha.1] + +- Refactor mobile environment variables. + - [dee9460f](https://www.github.com/tauri-apps/tauri/commit/dee9460f9c9bc92e9c638e7691e616849ac2085b) feat: keep CLI alive when iOS app exits, show logs, closes [#5855](https://www.github.com/tauri-apps/tauri/pull/5855) ([#5902](https://www.github.com/tauri-apps/tauri/pull/5902)) on 2022-12-27 +- Bump the MSRV to 1.64. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Removed mobile logging initialization, which will be handled by `tauri-plugin-log`. + - [](https://www.github.com/tauri-apps/tauri/commit/undefined) on undefined + +## \[2.0.0-alpha.0] + +- Added the `mobile_entry_point` macro. + - [98904863](https://www.github.com/tauri-apps/tauri/commit/9890486321c9c79ccfb7c547fafee85b5c3ffa71) feat(core): add `mobile_entry_point` macro ([#4983](https://www.github.com/tauri-apps/tauri/pull/4983)) on 2022-08-21 +- First mobile alpha release! + - [fa3a1098](https://www.github.com/tauri-apps/tauri/commit/fa3a10988a03aed1b66fb17d893b1a9adb90f7cd) feat(ci): prepare 2.0.0-alpha.0 ([#5786](https://www.github.com/tauri-apps/tauri/pull/5786)) on 2022-12-08 + ## \[1.4.0] ### Enhancements diff --git a/core/tauri-macros/Cargo.toml b/core/tauri-macros/Cargo.toml index 2381ebfaba2..4d7aa467663 100644 --- a/core/tauri-macros/Cargo.toml +++ b/core/tauri-macros/Cargo.toml @@ -1,16 +1,16 @@ [package] name = "tauri-macros" -version = "1.4.0" -authors = [ "Tauri Programme within The Commons Conservancy" ] -categories = [ "gui", "os", "filesystem", "web-programming" ] -license = "Apache-2.0 OR MIT" -homepage = "https://tauri.app" -repository = "https://github.com/tauri-apps/tauri" +version = "2.0.0-alpha.7" description = "Macros for the tauri crate." -edition = "2021" -rust-version = "1.60" exclude = [ "CHANGELOG.md", "/target" ] readme = "README.md" +authors = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +categories = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } [lib] proc-macro = true @@ -20,13 +20,12 @@ proc-macro2 = "1" quote = "1" syn = { version = "1", features = [ "full" ] } heck = "0.4" -tauri-codegen = { version = "1.4.0", default-features = false, path = "../tauri-codegen" } -tauri-utils = { version = "1.4.0", path = "../tauri-utils" } +tauri-codegen = { version = "2.0.0-alpha.7", default-features = false, path = "../tauri-codegen" } +tauri-utils = { version = "2.0.0-alpha.7", path = "../tauri-utils" } [features] custom-protocol = [ ] compression = [ "tauri-codegen/compression" ] isolation = [ "tauri-codegen/isolation" ] -shell-scope = [ "tauri-codegen/shell-scope" ] config-json5 = [ "tauri-codegen/config-json5", "tauri-utils/config-json5" ] config-toml = [ "tauri-codegen/config-toml", "tauri-utils/config-toml" ] diff --git a/core/tauri-macros/src/command/handler.rs b/core/tauri-macros/src/command/handler.rs index 5c805185653..62a9ace4584 100644 --- a/core/tauri-macros/src/command/handler.rs +++ b/core/tauri-macros/src/command/handler.rs @@ -4,26 +4,40 @@ use quote::format_ident; use syn::{ - parse::{Parse, ParseBuffer}, - Ident, Path, Token, + parse::{Parse, ParseBuffer, ParseStream}, + Attribute, Ident, Path, Token, }; +struct CommandDef { + path: Path, + attrs: Vec, +} + +impl Parse for CommandDef { + fn parse(input: ParseStream) -> syn::Result { + let attrs = input.call(Attribute::parse_outer)?; + let path = input.parse()?; + + Ok(CommandDef { path, attrs }) + } +} + /// The items parsed from [`generate_handle!`](crate::generate_handle). pub struct Handler { - paths: Vec, + command_defs: Vec, commands: Vec, wrappers: Vec, } impl Parse for Handler { fn parse(input: &ParseBuffer<'_>) -> syn::Result { - let paths = input.parse_terminated::(Path::parse)?; + let command_defs = input.parse_terminated::(CommandDef::parse)?; // parse the command names and wrappers from the passed paths - let (commands, wrappers) = paths + let (commands, wrappers) = command_defs .iter() - .map(|path| { - let mut wrapper = path.clone(); + .map(|command_def| { + let mut wrapper = command_def.path.clone(); let last = super::path_to_command(&mut wrapper); // the name of the actual command function @@ -37,7 +51,7 @@ impl Parse for Handler { .unzip(); Ok(Self { - paths: paths.into_iter().collect(), // remove punctuation separators + command_defs: command_defs.into_iter().collect(), // remove punctuation separators commands, wrappers, }) @@ -47,19 +61,23 @@ impl Parse for Handler { impl From for proc_macro::TokenStream { fn from( Handler { - paths, + command_defs, commands, wrappers, }: Handler, ) -> Self { let cmd = format_ident!("__tauri_cmd__"); let invoke = format_ident!("__tauri_invoke__"); + let (paths, attrs): (Vec, Vec>) = command_defs + .into_iter() + .map(|def| (def.path, def.attrs)) + .unzip(); quote::quote!(move |#invoke| { let #cmd = #invoke.message.command(); match #cmd { - #(stringify!(#commands) => #wrappers!(#paths, #invoke),)* + #(#(#attrs)* stringify!(#commands) => #wrappers!(#paths, #invoke),)* _ => { - #invoke.resolver.reject(format!("command {} not found", #cmd)) + return false; }, } }) diff --git a/core/tauri-macros/src/command/wrapper.rs b/core/tauri-macros/src/command/wrapper.rs index e529d8fcdc8..fab592417eb 100644 --- a/core/tauri-macros/src/command/wrapper.rs +++ b/core/tauri-macros/src/command/wrapper.rs @@ -4,17 +4,18 @@ use heck::{ToLowerCamelCase, ToSnakeCase}; use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use quote::{format_ident, quote, quote_spanned}; use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, parse_macro_input, spanned::Spanned, - FnArg, Ident, ItemFn, Lit, Meta, Pat, Token, Visibility, + FnArg, ItemFn, Lit, Meta, Pat, Token, Visibility, }; struct WrapperAttributes { + root: TokenStream2, execution_context: ExecutionContext, argument_case: ArgumentCase, } @@ -22,6 +23,7 @@ struct WrapperAttributes { impl Parse for WrapperAttributes { fn parse(input: ParseStream) -> syn::Result { let mut wrapper_attributes = WrapperAttributes { + root: quote!(::tauri), execution_context: ExecutionContext::Blocking, argument_case: ArgumentCase::Camel, }; @@ -43,6 +45,17 @@ impl Parse for WrapperAttributes { } }; } + } else if v.path.is_ident("root") { + if let Lit::Str(s) = v.lit { + let lit = s.value(); + + wrapper_attributes.root = if lit == "crate" { + quote!($crate) + } else { + let ident = Ident::new(&lit, Span::call_site()); + quote!(#ident) + }; + } } } Ok(Meta::Path(p)) => { @@ -161,21 +174,28 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { } // body to the command wrapper or a `compile_error!` of an error occurred while parsing it. - let body = syn::parse::(attributes) + let (body, attributes) = syn::parse::(attributes) .map(|mut attrs| { if function.sig.asyncness.is_some() { attrs.execution_context = ExecutionContext::Async; } attrs }) - .and_then(|attrs| match attrs.execution_context { - ExecutionContext::Async => body_async(&function, &invoke, attrs.argument_case), - ExecutionContext::Blocking => body_blocking(&function, &invoke, attrs.argument_case), + .and_then(|attrs| { + let body = match attrs.execution_context { + ExecutionContext::Async => body_async(&function, &invoke, &attrs), + ExecutionContext::Blocking => body_blocking(&function, &invoke, &attrs), + }; + body.map(|b| (b, Some(attrs))) }) - .unwrap_or_else(syn::Error::into_compile_error); + .unwrap_or_else(|e| (syn::Error::into_compile_error(e), None)); let Invoke { message, resolver } = invoke; + let root = attributes + .map(|a| a.root) + .unwrap_or_else(|| quote!(::tauri)); + // Rely on rust 2018 edition to allow importing a macro from a path. quote!( #async_command_check @@ -188,10 +208,10 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { // double braces because the item is expected to be a block expression ($path:path, $invoke:ident) => {{ #[allow(unused_imports)] - use ::tauri::command::private::*; + use #root::command::private::*; // prevent warnings when the body is a `compile_error!` or if the command has no arguments #[allow(unused_variables)] - let ::tauri::Invoke { message: #message, resolver: #resolver } = $invoke; + let #root::ipc::Invoke { message: #message, resolver: #resolver } = $invoke; #body }}; @@ -209,15 +229,20 @@ pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream { /// See the [`tauri::command`] module for all the items and traits that make this possible. /// /// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html -fn body_async(function: &ItemFn, invoke: &Invoke, case: ArgumentCase) -> syn::Result { +fn body_async( + function: &ItemFn, + invoke: &Invoke, + attributes: &WrapperAttributes, +) -> syn::Result { let Invoke { message, resolver } = invoke; - parse_args(function, message, case).map(|args| { + parse_args(function, message, attributes).map(|args| { quote! { #resolver.respond_async_serialized(async move { let result = $path(#(#args?),*); let kind = (&result).async_kind(); kind.future(result).await }); + return true; } }) } @@ -230,21 +255,22 @@ fn body_async(function: &ItemFn, invoke: &Invoke, case: ArgumentCase) -> syn::Re fn body_blocking( function: &ItemFn, invoke: &Invoke, - case: ArgumentCase, + attributes: &WrapperAttributes, ) -> syn::Result { let Invoke { message, resolver } = invoke; - let args = parse_args(function, message, case)?; + let args = parse_args(function, message, attributes)?; // the body of a `match` to early return any argument that wasn't successful in parsing. let match_body = quote!({ Ok(arg) => arg, - Err(err) => return #resolver.invoke_error(err), + Err(err) => { #resolver.invoke_error(err); return true }, }); Ok(quote! { let result = $path(#(match #args #match_body),*); let kind = (&result).blocking_kind(); kind.block(result, #resolver); + return true; }) } @@ -252,13 +278,13 @@ fn body_blocking( fn parse_args( function: &ItemFn, message: &Ident, - case: ArgumentCase, + attributes: &WrapperAttributes, ) -> syn::Result> { function .sig .inputs .iter() - .map(|arg| parse_arg(&function.sig.ident, arg, message, case)) + .map(|arg| parse_arg(&function.sig.ident, arg, message, attributes)) .collect() } @@ -267,7 +293,7 @@ fn parse_arg( command: &Ident, arg: &FnArg, message: &Ident, - case: ArgumentCase, + attributes: &WrapperAttributes, ) -> syn::Result { // we have no use for self arguments let mut arg = match arg { @@ -302,7 +328,7 @@ fn parse_arg( )); } - match case { + match attributes.argument_case { ArgumentCase::Camel => { key = key.to_lower_camel_case(); } @@ -311,8 +337,10 @@ fn parse_arg( } } - Ok(quote!(::tauri::command::CommandArg::from_command( - ::tauri::command::CommandItem { + let root = &attributes.root; + + Ok(quote!(#root::command::CommandArg::from_command( + #root::command::CommandItem { name: stringify!(#command), key: #key, message: &#message, diff --git a/core/tauri-macros/src/command_module.rs b/core/tauri-macros/src/command_module.rs deleted file mode 100644 index 6bbbd16eb25..00000000000 --- a/core/tauri-macros/src/command_module.rs +++ /dev/null @@ -1,303 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use heck::{ToLowerCamelCase, ToSnakeCase}; -use proc_macro::TokenStream; -use proc_macro2::{Span, TokenStream as TokenStream2}; - -use quote::{format_ident, quote, quote_spanned}; -use syn::{ - parse::{Parse, ParseStream}, - parse_quote, - spanned::Spanned, - Data, DeriveInput, Error, Fields, Ident, ItemFn, LitStr, Token, -}; - -pub(crate) fn generate_command_enum(mut input: DeriveInput) -> TokenStream { - let mut deserialize_functions = TokenStream2::new(); - let mut errors = TokenStream2::new(); - - input.attrs.push(parse_quote!(#[allow(dead_code)])); - - match &mut input.data { - Data::Enum(data_enum) => { - for variant in &mut data_enum.variants { - let mut feature: Option = None; - let mut error_message: Option = None; - - for attr in &variant.attrs { - if attr.path.is_ident("cmd") { - let r = attr - .parse_args_with(|input: ParseStream| { - if let Ok(f) = input.parse::() { - feature.replace(f); - input.parse::()?; - let error_message_raw: LitStr = input.parse()?; - error_message.replace(error_message_raw.value()); - } - Ok(quote!()) - }) - .unwrap_or_else(syn::Error::into_compile_error); - errors.extend(r); - } - } - - if let Some(f) = feature { - let error_message = if let Some(e) = error_message { - let e = e.to_string(); - quote!(#e) - } else { - quote!("This API is not enabled in the allowlist.") - }; - - let deserialize_function_name = quote::format_ident!("__{}_deserializer", variant.ident); - deserialize_functions.extend(quote! { - #[cfg(not(#f))] - #[allow(non_snake_case)] - fn #deserialize_function_name<'de, D, T>(deserializer: D) -> ::std::result::Result - where - D: ::serde::de::Deserializer<'de>, - { - ::std::result::Result::Err(::serde::de::Error::custom(crate::Error::ApiNotAllowlisted(#error_message.into()).to_string())) - } - }); - - let deserialize_function_name = deserialize_function_name.to_string(); - - variant - .attrs - .push(parse_quote!(#[cfg_attr(not(#f), serde(deserialize_with = #deserialize_function_name))])); - } - } - } - _ => { - return Error::new( - Span::call_site(), - "`command_enum` is only implemented for enums", - ) - .to_compile_error() - .into() - } - }; - - TokenStream::from(quote! { - #errors - #input - #deserialize_functions - }) -} - -pub(crate) fn generate_run_fn(input: DeriveInput) -> TokenStream { - let name = &input.ident; - let data = &input.data; - - let mut errors = TokenStream2::new(); - - let mut is_async = false; - - let attrs = input.attrs; - for attr in attrs { - if attr.path.is_ident("cmd") { - let r = attr - .parse_args_with(|input: ParseStream| { - if let Ok(token) = input.parse::() { - is_async = token == "async"; - } - Ok(quote!()) - }) - .unwrap_or_else(syn::Error::into_compile_error); - errors.extend(r); - } - } - - let maybe_await = if is_async { quote!(.await) } else { quote!() }; - let maybe_async = if is_async { quote!(async) } else { quote!() }; - - let mut matcher; - - match data { - Data::Enum(data_enum) => { - matcher = TokenStream2::new(); - - for variant in &data_enum.variants { - let variant_name = &variant.ident; - - let mut feature = None; - - for attr in &variant.attrs { - if attr.path.is_ident("cmd") { - let r = attr - .parse_args_with(|input: ParseStream| { - if let Ok(f) = input.parse::() { - feature.replace(f); - input.parse::()?; - let _: LitStr = input.parse()?; - } - Ok(quote!()) - }) - .unwrap_or_else(syn::Error::into_compile_error); - errors.extend(r); - } - } - - let maybe_feature_check = if let Some(f) = feature { - quote!(#[cfg(#f)]) - } else { - quote!() - }; - - let (fields_in_variant, variables) = match &variant.fields { - Fields::Unit => (quote_spanned! { variant.span() => }, quote!()), - Fields::Unnamed(fields) => { - let mut variables = TokenStream2::new(); - for i in 0..fields.unnamed.len() { - let variable_name = format_ident!("value{}", i); - variables.extend(quote!(#variable_name,)); - } - (quote_spanned! { variant.span() => (#variables) }, variables) - } - Fields::Named(fields) => { - let mut variables = TokenStream2::new(); - for field in &fields.named { - let ident = field.ident.as_ref().unwrap(); - variables.extend(quote!(#ident,)); - } - ( - quote_spanned! { variant.span() => { #variables } }, - variables, - ) - } - }; - - let mut variant_execute_function_name = format_ident!( - "{}", - variant_name.to_string().to_snake_case().to_lowercase() - ); - variant_execute_function_name.set_span(variant_name.span()); - - matcher.extend(quote_spanned! { - variant.span() => #maybe_feature_check #name::#variant_name #fields_in_variant => #name::#variant_execute_function_name(context, #variables)#maybe_await.map(Into::into), - }); - } - - matcher.extend(quote! { - _ => Err(crate::error::into_anyhow("API not in the allowlist (https://tauri.app/docs/api/config#tauri.allowlist)")), - }); - } - _ => { - return Error::new( - Span::call_site(), - "CommandModule is only implemented for enums", - ) - .to_compile_error() - .into() - } - }; - - let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - - let expanded = quote! { - #errors - impl #impl_generics #name #ty_generics #where_clause { - pub #maybe_async fn run(self, context: crate::endpoints::InvokeContext) -> super::Result { - match self { - #matcher - } - } - } - }; - - TokenStream::from(expanded) -} - -/// Attributes for the module enum variant handler. -pub struct HandlerAttributes { - allowlist: Ident, -} - -impl Parse for HandlerAttributes { - fn parse(input: ParseStream) -> syn::Result { - Ok(Self { - allowlist: input.parse()?, - }) - } -} - -pub enum AllowlistCheckKind { - Runtime, - Serde, -} - -pub struct HandlerTestAttributes { - allowlist: Ident, - error_message: String, - allowlist_check_kind: AllowlistCheckKind, -} - -impl Parse for HandlerTestAttributes { - fn parse(input: ParseStream) -> syn::Result { - let allowlist = input.parse()?; - input.parse::()?; - let error_message_raw: LitStr = input.parse()?; - let error_message = error_message_raw.value(); - let allowlist_check_kind = - if let (Ok(_), Ok(i)) = (input.parse::(), input.parse::()) { - if i == "runtime" { - AllowlistCheckKind::Runtime - } else { - AllowlistCheckKind::Serde - } - } else { - AllowlistCheckKind::Serde - }; - - Ok(Self { - allowlist, - error_message, - allowlist_check_kind, - }) - } -} - -pub fn command_handler(attributes: HandlerAttributes, function: ItemFn) -> TokenStream2 { - let allowlist = attributes.allowlist; - - quote!( - #[cfg(#allowlist)] - #function - ) -} - -pub fn command_test(attributes: HandlerTestAttributes, function: ItemFn) -> TokenStream2 { - let allowlist = attributes.allowlist; - let error_message = attributes.error_message.as_str(); - let signature = &function.sig; - - let enum_variant_name = function.sig.ident.to_string().to_lower_camel_case(); - let response = match attributes.allowlist_check_kind { - AllowlistCheckKind::Runtime => { - let test_name = &signature.ident; - quote!(super::Cmd::#test_name(crate::test::mock_invoke_context())) - } - AllowlistCheckKind::Serde => quote! { - serde_json::from_str::(&format!(r#"{{ "cmd": "{}", "data": null }}"#, #enum_variant_name)) - }, - }; - - quote!( - #[cfg(#allowlist)] - #function - - #[cfg(not(#allowlist))] - #[allow(unused_variables)] - #[quickcheck_macros::quickcheck] - #signature { - if let Err(e) = #response { - assert!(e.to_string().contains(#error_message)); - } else { - panic!("unexpected response"); - } - } - ) -} diff --git a/core/tauri-macros/src/lib.rs b/core/tauri-macros/src/lib.rs index 93462d134c9..3962b1012d1 100644 --- a/core/tauri-macros/src/lib.rs +++ b/core/tauri-macros/src/lib.rs @@ -2,12 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! [![](https://github.com/tauri-apps/tauri/raw/dev/.github/splash.png)](https://tauri.app) +//! +//! Create macros for `tauri::Context`, invoke handler and commands leveraging the `tauri-codegen` crate. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + use crate::context::ContextItems; use proc_macro::TokenStream; -use syn::{parse_macro_input, DeriveInput, ItemFn}; +use syn::{parse_macro_input, DeriveInput}; mod command; -mod command_module; +mod mobile; mod runtime; #[macro_use] @@ -24,6 +33,11 @@ pub fn command(attributes: TokenStream, item: TokenStream) -> TokenStream { command::wrapper(attributes, item) } +#[proc_macro_attribute] +pub fn mobile_entry_point(attributes: TokenStream, item: TokenStream) -> TokenStream { + mobile::entry_point(attributes, item) +} + /// Accepts a list of commands functions. Creates a handler that allows commands to be called from JS with invoke(). /// /// # Examples @@ -75,40 +89,3 @@ pub fn default_runtime(attributes: TokenStream, input: TokenStream) -> TokenStre let input = parse_macro_input!(input as DeriveInput); runtime::default_runtime(attributes, input).into() } - -/// Prepares the command module enum. -#[doc(hidden)] -#[proc_macro_derive(CommandModule, attributes(cmd))] -pub fn derive_command_module(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - command_module::generate_run_fn(input) -} - -/// Adds a `run` method to an enum (one of the tauri endpoint modules). -/// The `run` method takes a `tauri::endpoints::InvokeContext` -/// and returns a `tauri::Result`. -/// It matches on each enum variant and call a method with name equal to the variant name, lowercased and snake_cased, -/// passing the context and the variant's fields as arguments. -/// That function must also return the same `Result`. -#[doc(hidden)] -#[proc_macro_attribute] -pub fn command_enum(_: TokenStream, input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - command_module::generate_command_enum(input) -} - -#[doc(hidden)] -#[proc_macro_attribute] -pub fn module_command_handler(attributes: TokenStream, input: TokenStream) -> TokenStream { - let attributes = parse_macro_input!(attributes as command_module::HandlerAttributes); - let input = parse_macro_input!(input as ItemFn); - command_module::command_handler(attributes, input).into() -} - -#[doc(hidden)] -#[proc_macro_attribute] -pub fn module_command_test(attributes: TokenStream, input: TokenStream) -> TokenStream { - let attributes = parse_macro_input!(attributes as command_module::HandlerTestAttributes); - let input = parse_macro_input!(input as ItemFn); - command_module::command_test(attributes, input).into() -} diff --git a/core/tauri-macros/src/mobile.rs b/core/tauri-macros/src/mobile.rs new file mode 100644 index 00000000000..eae55fa68ca --- /dev/null +++ b/core/tauri-macros/src/mobile.rs @@ -0,0 +1,83 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use std::env::var; +use syn::{parse_macro_input, spanned::Spanned, ItemFn}; + +fn get_env_var String>( + name: &str, + replacer: R, + error: &mut Option, + function: &ItemFn, +) -> TokenStream2 { + match var(name) { + Ok(value) => { + let ident = format_ident!("{}", replacer(value)); + quote!(#ident) + } + Err(_) => { + error.replace( + syn::Error::new( + function.span(), + format!("`{name}` env var not set, do you have a build script with tauri-build?",), + ) + .into_compile_error(), + ); + quote!() + } + } +} + +pub fn entry_point(_attributes: TokenStream, item: TokenStream) -> TokenStream { + let function = parse_macro_input!(item as ItemFn); + let function_name = &function.sig.ident; + + let mut error = None; + let domain = get_env_var("TAURI_ANDROID_PACKAGE_PREFIX", |r| r, &mut error, &function); + let app_name = get_env_var( + "CARGO_PKG_NAME", + |r| r.replace('-', "_"), + &mut error, + &function, + ); + + if let Some(e) = error { + quote!(#e).into() + } else { + quote!( + fn stop_unwind T, T>(f: F) -> T { + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) { + Ok(t) => t, + Err(err) => { + eprintln!("attempt to unwind out of `rust` with err: {:?}", err); + std::process::abort() + } + } + } + + #function + + fn _start_app() { + #[cfg(target_os = "ios")] + ::tauri::log_stdout(); + #[cfg(target_os = "android")] + { + ::tauri::android_binding!(#domain, #app_name, _start_app, ::tauri::wry); + } + stop_unwind(#function_name); + } + + #[cfg(not(target_os = "android"))] + #[no_mangle] + #[inline(never)] + pub extern "C" fn start_app() { + _start_app() + } + ) + .into() + } +} diff --git a/core/tauri-runtime-wry/CHANGELOG.md b/core/tauri-runtime-wry/CHANGELOG.md index 2840b148f3b..a3a2a3536d2 100644 --- a/core/tauri-runtime-wry/CHANGELOG.md +++ b/core/tauri-runtime-wry/CHANGELOG.md @@ -1,5 +1,77 @@ # Changelog +## \[1.0.0-alpha.0] + +### New Features + +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) Add `Dispatch::default_vbox` +- [`84c41597`](https://www.github.com/tauri-apps/tauri/commit/84c4159754b2e59244211ed9e1fc702d851a0562)([#6394](https://www.github.com/tauri-apps/tauri/pull/6394)) Added `primary_monitor` and `available_monitors` to `Runtime` and `RuntimeHandle`. +- [`3b98141a`](https://www.github.com/tauri-apps/tauri/commit/3b98141aa26f74c641a4090874247b97079bd58a)([#3736](https://www.github.com/tauri-apps/tauri/pull/3736)) Added the `Opened` variant to `RunEvent`. +- [`2a000e15`](https://www.github.com/tauri-apps/tauri/commit/2a000e150d02dff28c8b20ad097b29e209160045)([#7235](https://www.github.com/tauri-apps/tauri/pull/7235)) Implement navigate method + +### Dependencies + +- Upgraded to `tauri-runtime@1.0.0-alpha.0` +- Upgraded to `tauri-utils@2.0.0-alpha.7` + +### Breaking Changes + +- [`fbeb5b91`](https://www.github.com/tauri-apps/tauri/commit/fbeb5b9185baeda19e865228179e3e44c165f1d9)([#7170](https://www.github.com/tauri-apps/tauri/pull/7170)) Removed the `linux-headers` feature (now always enabled) and added `linux-protocol-body`. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) `Dispatch::create_window`, `Runtime::create_window` and `RuntimeHandle::create_window` has been changed to accept a 3rd parameter which is a closure that takes `RawWindow` and to be executed right after the window is created and before the webview is added to the window. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) System tray and menu related APIs and structs have all been removed and are now implemented in tauri outside of the runtime-space. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) `Runtime::new` and `Runtime::new_any_thread` now accept a `RuntimeInitArgs`. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) Removed `system-tray` feature flag + +## \[0.13.0-alpha.6] + +### New Features + +- [`e0f0dce2`](https://www.github.com/tauri-apps/tauri/commit/e0f0dce220730e2822fc202463aedf0166145de7)([#6442](https://www.github.com/tauri-apps/tauri/pull/6442)) Added the `window_effects` option when creating a window and `Window::set_effects` to change it at runtime. + +## \[0.13.0-alpha.5] + +- [`39f1b04f`](https://www.github.com/tauri-apps/tauri/commit/39f1b04f7be4966488484829cd54c8ce72a04200)([#6943](https://www.github.com/tauri-apps/tauri/pull/6943)) Moved the `event` JS APIs to a plugin. +- [`3188f376`](https://www.github.com/tauri-apps/tauri/commit/3188f3764978c6d1452ee31d5a91469691e95094)([#6883](https://www.github.com/tauri-apps/tauri/pull/6883)) Bump the MSRV to 1.65. +- [`cebd7526`](https://www.github.com/tauri-apps/tauri/commit/cebd75261ac71b98976314a450cb292eeeec1515)([#6728](https://www.github.com/tauri-apps/tauri/pull/6728)) Moved the `clipboard` feature to its own plugin in the plugins-workspace repository. +- [`3f17ee82`](https://www.github.com/tauri-apps/tauri/commit/3f17ee82f6ff21108806edb7b00500b8512b8dc7)([#6737](https://www.github.com/tauri-apps/tauri/pull/6737)) Moved the `global-shortcut` feature to its own plugin in the plugins-workspace repository. +- [`31444ac1`](https://www.github.com/tauri-apps/tauri/commit/31444ac196add770f2ad18012d7c18bce7538f22)([#6725](https://www.github.com/tauri-apps/tauri/pull/6725)) Update `wry` to `0.28` + +## \[0.13.0-alpha.4] + +- Added `android` configuration object under `tauri > bundle`. + - Bumped due to a bump in tauri-utils. + - [db4c9dc6](https://www.github.com/tauri-apps/tauri/commit/db4c9dc655e07ee2184fe04571f500f7910890cd) feat(core): add option to configure Android's minimum SDK version ([#6651](https://www.github.com/tauri-apps/tauri/pull/6651)) on 2023-04-07 + +## \[0.13.0-alpha.3] + +- Pull changes from Tauri 1.3 release. + - [](https://www.github.com/tauri-apps/tauri/commit/undefined) on undefined + +## \[0.13.0-alpha.2] + +- Add `find_class`, `run_on_android_context` on `RuntimeHandle`. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 +- Allow a wry plugin to be registered at runtime. + - [ae296f3d](https://www.github.com/tauri-apps/tauri/commit/ae296f3de16fb6a8badbad5555075a5861681fe5) refactor(tauri-runtime-wry): register runtime plugin after run() ([#6478](https://www.github.com/tauri-apps/tauri/pull/6478)) on 2023-03-17 +- Added the `shadow` option when creating a window and `Window::set_shadow`. + - [a81750d7](https://www.github.com/tauri-apps/tauri/commit/a81750d779bc72f0fdb7de90b7fbddfd8049b328) feat(core): add shadow APIs ([#6206](https://www.github.com/tauri-apps/tauri/pull/6206)) on 2023-02-08 +- Implemented `with_webview` on Android and iOS. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 + +## \[0.13.0-alpha.1] + +- Update gtk to 0.16. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Bump the MSRV to 1.64. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Update wry to 0.26. + - [f0a1d9cd](https://www.github.com/tauri-apps/tauri/commit/f0a1d9cdbcfb645ce1c5f1cdd597f764991772cd) chore: update rfd and wry versions ([#6174](https://www.github.com/tauri-apps/tauri/pull/6174)) on 2023-02-03 + +## \[0.13.0-alpha.0] + +- Support `with_webview` for Android platform alowing execution of JNI code in context. + - [8ea87e9c](https://www.github.com/tauri-apps/tauri/commit/8ea87e9c9ca8ba4c7017c8281f78aacd08f45785) feat(android): with_webview access for jni execution ([#5148](https://www.github.com/tauri-apps/tauri/pull/5148)) on 2022-09-08 + ## \[0.14.0] ### New Features diff --git a/core/tauri-runtime-wry/Cargo.toml b/core/tauri-runtime-wry/Cargo.toml index 843a95ff379..3d17bf3a2b9 100644 --- a/core/tauri-runtime-wry/Cargo.toml +++ b/core/tauri-runtime-wry/Cargo.toml @@ -1,50 +1,53 @@ [package] name = "tauri-runtime-wry" -version = "0.14.0" -authors = [ "Tauri Programme within The Commons Conservancy" ] -categories = [ "gui", "web-programming" ] -license = "Apache-2.0 OR MIT" -homepage = "https://tauri.app" -repository = "https://github.com/tauri-apps/tauri" +version = "1.0.0-alpha.0" description = "Wry bindings to the Tauri runtime" -edition = "2021" -rust-version = "1.60" exclude = [ "CHANGELOG.md", "/target" ] readme = "README.md" +authors = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +categories = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } + +[package.metadata.docs.rs] +features = [ "dox" ] [dependencies] -wry = { version = "0.24.1", default-features = false, features = [ "file-drop", "protocol" ] } -tauri-runtime = { version = "0.14.0", path = "../tauri-runtime" } -tauri-utils = { version = "1.4.0", path = "../tauri-utils" } +wry = { version = "0.31", default-features = false, features = [ "file-drop", "protocol" ] } +tauri-runtime = { version = "1.0.0-alpha.0", path = "../tauri-runtime" } +tauri-utils = { version = "2.0.0-alpha.7", path = "../tauri-utils" } uuid = { version = "1", features = [ "v4" ] } rand = "0.8" raw-window-handle = "0.5" [target."cfg(windows)".dependencies] -webview2-com = "0.19.1" +webview2-com = "0.25" [target."cfg(windows)".dependencies.windows] - version = "0.39.0" + version = "0.48" features = [ "Win32_Foundation" ] [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] -gtk = { version = "0.15", features = [ "v3_20" ] } -webkit2gtk = { version = "0.18.2", features = [ "v2_22" ] } +gtk = { version = "0.16", features = [ "v3_24" ] } +webkit2gtk = { version = "1.1", features = [ "v2_38" ] } percent-encoding = "2.1" [target."cfg(any(target_os = \"ios\", target_os = \"macos\"))".dependencies] cocoa = "0.24" +[target."cfg(target_os = \"android\")".dependencies] +jni = "0.21" + [features] dox = [ "wry/dox" ] devtools = [ "wry/devtools", "tauri-runtime/devtools" ] -system-tray = [ "tauri-runtime/system-tray", "wry/tray" ] macos-private-api = [ "wry/fullscreen", "wry/transparent", "tauri-runtime/macos-private-api" ] objc-exception = [ "wry/objc-exception" ] -global-shortcut = [ "tauri-runtime/global-shortcut" ] -clipboard = [ "tauri-runtime/clipboard" ] -linux-headers = [ "wry/linux-headers", "webkit2gtk/v2_36" ] +linux-protocol-body = [ "wry/linux-body", "webkit2gtk/v2_40" ] diff --git a/core/tauri-runtime-wry/src/clipboard.rs b/core/tauri-runtime-wry/src/clipboard.rs deleted file mode 100644 index 428f1abb3a1..00000000000 --- a/core/tauri-runtime-wry/src/clipboard.rs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! Clipboard implementation. - -use crate::{getter, Context, Message}; - -use std::sync::{ - mpsc::{channel, Sender}, - Arc, Mutex, -}; - -use tauri_runtime::{ClipboardManager, Result, UserEvent}; -pub use wry::application::clipboard::Clipboard; - -#[derive(Debug, Clone)] -pub enum ClipboardMessage { - WriteText(String, Sender<()>), - ReadText(Sender>), -} - -#[derive(Debug, Clone)] -pub struct ClipboardManagerWrapper { - pub context: Context, -} - -// SAFETY: this is safe since the `Context` usage is guarded on `send_user_message`. -#[allow(clippy::non_send_fields_in_send_ty)] -unsafe impl Sync for ClipboardManagerWrapper {} - -impl ClipboardManager for ClipboardManagerWrapper { - fn read_text(&self) -> Result> { - let (tx, rx) = channel(); - getter!(self, rx, Message::Clipboard(ClipboardMessage::ReadText(tx))) - } - - fn write_text>(&mut self, text: V) -> Result<()> { - let (tx, rx) = channel(); - getter!( - self, - rx, - Message::Clipboard(ClipboardMessage::WriteText(text.into(), tx)) - )?; - Ok(()) - } -} - -pub fn handle_clipboard_message( - message: ClipboardMessage, - clipboard_manager: &Arc>, -) { - match message { - ClipboardMessage::WriteText(text, tx) => { - clipboard_manager.lock().unwrap().write_text(text); - tx.send(()).unwrap(); - } - ClipboardMessage::ReadText(tx) => tx - .send(clipboard_manager.lock().unwrap().read_text()) - .unwrap(), - } -} diff --git a/core/tauri-runtime-wry/src/global_shortcut.rs b/core/tauri-runtime-wry/src/global_shortcut.rs deleted file mode 100644 index 4b3bbb61c55..00000000000 --- a/core/tauri-runtime-wry/src/global_shortcut.rs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! Global shortcut implementation. - -use std::{ - collections::HashMap, - error::Error as StdError, - fmt, - sync::{ - mpsc::{channel, Sender}, - Arc, Mutex, - }, -}; - -use crate::{getter, Context, Message}; - -use tauri_runtime::{Error, GlobalShortcutManager, Result, UserEvent}; -#[cfg(desktop)] -pub use wry::application::{ - accelerator::{Accelerator, AcceleratorId, AcceleratorParseError}, - global_shortcut::{GlobalShortcut, ShortcutManager as WryShortcutManager}, -}; - -pub type GlobalShortcutListeners = Arc>>>; - -#[derive(Debug, Clone)] -pub enum GlobalShortcutMessage { - IsRegistered(Accelerator, Sender), - Register(Accelerator, Sender>), - Unregister(GlobalShortcutWrapper, Sender>), - UnregisterAll(Sender>), -} - -#[derive(Debug, Clone)] -pub struct GlobalShortcutWrapper(GlobalShortcut); - -// SAFETY: usage outside of main thread is guarded, we use the event loop on such cases. -#[allow(clippy::non_send_fields_in_send_ty)] -unsafe impl Send for GlobalShortcutWrapper {} - -#[derive(Debug, Clone)] -struct AcceleratorParseErrorWrapper(AcceleratorParseError); -impl fmt::Display for AcceleratorParseErrorWrapper { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} -impl StdError for AcceleratorParseErrorWrapper {} - -/// Wrapper around [`WryShortcutManager`]. -#[derive(Clone)] -pub struct GlobalShortcutManagerHandle { - pub context: Context, - pub shortcuts: Arc>>, - pub listeners: GlobalShortcutListeners, -} - -// SAFETY: this is safe since the `Context` usage is guarded on `send_user_message`. -#[allow(clippy::non_send_fields_in_send_ty)] -unsafe impl Sync for GlobalShortcutManagerHandle {} - -impl fmt::Debug for GlobalShortcutManagerHandle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("GlobalShortcutManagerHandle") - .field("context", &self.context) - .field("shortcuts", &self.shortcuts) - .finish() - } -} - -impl GlobalShortcutManager for GlobalShortcutManagerHandle { - fn is_registered(&self, accelerator: &str) -> Result { - let (tx, rx) = channel(); - getter!( - self, - rx, - Message::GlobalShortcut(GlobalShortcutMessage::IsRegistered( - accelerator - .parse() - .map_err(|e: AcceleratorParseError| Error::GlobalShortcut(Box::new( - AcceleratorParseErrorWrapper(e) - )))?, - tx - )) - ) - } - - fn register(&mut self, accelerator: &str, handler: F) -> Result<()> { - let wry_accelerator: Accelerator = - accelerator.parse().map_err(|e: AcceleratorParseError| { - Error::GlobalShortcut(Box::new(AcceleratorParseErrorWrapper(e))) - })?; - let id = wry_accelerator.clone().id(); - let (tx, rx) = channel(); - let shortcut = getter!( - self, - rx, - Message::GlobalShortcut(GlobalShortcutMessage::Register(wry_accelerator, tx)) - )??; - - self.listeners.lock().unwrap().insert(id, Box::new(handler)); - self - .shortcuts - .lock() - .unwrap() - .insert(accelerator.into(), (id, shortcut)); - - Ok(()) - } - - fn unregister_all(&mut self) -> Result<()> { - let (tx, rx) = channel(); - getter!( - self, - rx, - Message::GlobalShortcut(GlobalShortcutMessage::UnregisterAll(tx)) - )??; - self.listeners.lock().unwrap().clear(); - self.shortcuts.lock().unwrap().clear(); - Ok(()) - } - - fn unregister(&mut self, accelerator: &str) -> Result<()> { - if let Some((accelerator_id, shortcut)) = self.shortcuts.lock().unwrap().remove(accelerator) { - let (tx, rx) = channel(); - getter!( - self, - rx, - Message::GlobalShortcut(GlobalShortcutMessage::Unregister(shortcut, tx)) - )??; - self.listeners.lock().unwrap().remove(&accelerator_id); - } - Ok(()) - } -} - -pub fn handle_global_shortcut_message( - message: GlobalShortcutMessage, - global_shortcut_manager: &Arc>, -) { - match message { - GlobalShortcutMessage::IsRegistered(accelerator, tx) => tx - .send( - global_shortcut_manager - .lock() - .unwrap() - .is_registered(&accelerator), - ) - .unwrap(), - GlobalShortcutMessage::Register(accelerator, tx) => tx - .send( - global_shortcut_manager - .lock() - .unwrap() - .register(accelerator) - .map(GlobalShortcutWrapper) - .map_err(|e| Error::GlobalShortcut(Box::new(e))), - ) - .unwrap(), - GlobalShortcutMessage::Unregister(shortcut, tx) => tx - .send( - global_shortcut_manager - .lock() - .unwrap() - .unregister(shortcut.0) - .map_err(|e| Error::GlobalShortcut(Box::new(e))), - ) - .unwrap(), - GlobalShortcutMessage::UnregisterAll(tx) => tx - .send( - global_shortcut_manager - .lock() - .unwrap() - .unregister_all() - .map_err(|e| Error::GlobalShortcut(Box::new(e))), - ) - .unwrap(), - } -} diff --git a/core/tauri-runtime-wry/src/lib.rs b/core/tauri-runtime-wry/src/lib.rs index efd76c9b2e0..f19c7435af2 100644 --- a/core/tauri-runtime-wry/src/lib.rs +++ b/core/tauri-runtime-wry/src/lib.rs @@ -2,25 +2,28 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! [![](https://github.com/tauri-apps/tauri/raw/dev/.github/splash.png)](https://tauri.app) +//! //! The [`wry`] Tauri [`Runtime`]. +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] + use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle}; use tauri_runtime::{ http::{header::CONTENT_TYPE, Request as HttpRequest, RequestParts, Response as HttpResponse}, - menu::{AboutMetadata, CustomMenuItem, Menu, MenuEntry, MenuHash, MenuId, MenuItem, MenuUpdate}, monitor::Monitor, webview::{WebviewIpcHandler, WindowBuilder, WindowBuilderBase}, window::{ dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}, - CursorIcon, DetachedWindow, FileDropEvent, JsEventListenerKey, PendingWindow, WindowEvent, + CursorIcon, DetachedWindow, FileDropEvent, PendingWindow, RawWindow, WindowEvent, }, DeviceEventFilter, Dispatch, Error, EventLoopProxy, ExitRequestedEventAction, Icon, Result, - RunEvent, RunIteration, Runtime, RuntimeHandle, UserAttentionType, UserEvent, + RunEvent, RunIteration, Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType, UserEvent, }; -use tauri_runtime::window::MenuEvent; -#[cfg(all(desktop, feature = "system-tray"))] -use tauri_runtime::{SystemTray, SystemTrayEvent}; #[cfg(windows)] use webview2_com::FocusChangedEventHandler; #[cfg(windows)] @@ -49,14 +52,9 @@ use wry::{ }, event::{Event, StartCause, WindowEvent as WryWindowEvent}, event_loop::{ - ControlFlow, DeviceEventFilter as WryDeviceEventFilter, EventLoop, + ControlFlow, DeviceEventFilter as WryDeviceEventFilter, EventLoop, EventLoopBuilder, EventLoopProxy as WryEventLoopProxy, EventLoopWindowTarget, }, - menu::{ - AboutMetadata as WryAboutMetadata, CustomMenuItem as WryCustomMenuItem, MenuBar, - MenuId as WryMenuId, MenuItem as WryMenuItem, MenuItemAttributes as WryMenuItemAttributes, - MenuType, - }, monitor::MonitorHandle, window::{ CursorIcon as WryCursorIcon, Fullscreen, Icon as WryWindowIcon, Theme as WryTheme, @@ -67,18 +65,23 @@ use wry::{ webview::{FileDropEvent as WryFileDropEvent, Url, WebContext, WebView, WebViewBuilder}, }; +pub use wry; pub use wry::application::window::{Window, WindowBuilder as WryWindowBuilder, WindowId}; pub use wry::webview::webview_version; #[cfg(windows)] use wry::webview::WebviewExtWindows; +#[cfg(target_os = "android")] +use wry::webview::{ + prelude::{dispatch, find_class}, + WebViewBuilderExtAndroid, WebviewExtAndroid, +}; #[cfg(target_os = "macos")] -use tauri_runtime::{menu::NativeImage, ActivationPolicy}; +use tauri_runtime::ActivationPolicy; #[cfg(target_os = "macos")] pub use wry::application::platform::macos::{ - ActivationPolicy as WryActivationPolicy, CustomMenuItemExtMacOS, EventLoopExtMacOS, - NativeImage as WryNativeImage, WindowExtMacOS, + ActivationPolicy as WryActivationPolicy, EventLoopExtMacOS, WindowExtMacOS, }; use std::{ @@ -86,7 +89,7 @@ use std::{ cell::RefCell, collections::{ hash_map::Entry::{Occupied, Vacant}, - HashMap, HashSet, + HashMap, }, fmt, ops::Deref, @@ -101,36 +104,14 @@ use std::{ pub type WebviewId = u64; type IpcHandler = dyn Fn(&Window, String) + 'static; type FileDropHandler = dyn Fn(&Window, WryFileDropEvent) -> bool + 'static; -#[cfg(all(desktop, feature = "system-tray"))] -pub use tauri_runtime::TrayId; -#[cfg(desktop)] mod webview; -#[cfg(desktop)] pub use webview::Webview; -#[cfg(all(desktop, feature = "system-tray"))] -mod system_tray; -#[cfg(all(desktop, feature = "system-tray"))] -use system_tray::*; - -#[cfg(all(desktop, feature = "global-shortcut"))] -mod global_shortcut; -#[cfg(all(desktop, feature = "global-shortcut"))] -use global_shortcut::*; - -#[cfg(feature = "clipboard")] -mod clipboard; -#[cfg(feature = "clipboard")] -use clipboard::*; - pub type WebContextStore = Arc, WebContext>>>; // window pub type WindowEventHandler = Box; pub type WindowEventListeners = Arc>>; -// menu -pub type MenuEventHandler = Box; -pub type WindowMenuEventListeners = Arc>>; #[derive(Debug, Clone, Default)] pub struct WebviewIdStore(Arc>>); @@ -172,13 +153,7 @@ pub(crate) fn send_user_message( message, UserMessageContext { webview_id_map: context.webview_id_map.clone(), - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager: context.main_thread.global_shortcut_manager.clone(), - #[cfg(feature = "clipboard")] - clipboard_manager: context.main_thread.clipboard_manager.clone(), windows: context.main_thread.windows.clone(), - #[cfg(all(desktop, feature = "system-tray"))] - system_tray_manager: context.main_thread.system_tray_manager.clone(), }, &context.main_thread.web_context, ); @@ -197,6 +172,7 @@ pub struct Context { main_thread_id: ThreadId, pub proxy: WryEventLoopProxy>, main_thread: DispatcherMainThreadContext, + plugins: Arc + Send>>>>, } impl Context { @@ -213,10 +189,12 @@ impl Context { } impl Context { - fn create_webview(&self, pending: PendingWindow>) -> Result>> { + fn create_webview( + &self, + pending: PendingWindow>, + before_webview_creation: Option, + ) -> Result>> { let label = pending.label.clone(); - let menu_ids = pending.menu_ids.clone(); - let js_event_listeners = pending.js_event_listeners.clone(); let context = self.clone(); let window_id = rand::random(); @@ -225,7 +203,14 @@ impl Context { Message::CreateWebview( window_id, Box::new(move |event_loop, web_context| { - create_webview(window_id, event_loop, web_context, context, pending) + create_webview( + window_id, + event_loop, + web_context, + context, + pending, + before_webview_creation, + ) }), ), )?; @@ -234,26 +219,25 @@ impl Context { window_id, context: self.clone(), }; - Ok(DetachedWindow { - label, - dispatcher, - menu_ids, - js_event_listeners, - }) + Ok(DetachedWindow { label, dispatcher }) } } -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct DispatcherMainThreadContext { pub window_target: EventLoopWindowTarget>, pub web_context: WebContextStore, - #[cfg(all(desktop, feature = "global-shortcut"))] - pub global_shortcut_manager: Arc>, - #[cfg(feature = "clipboard")] - pub clipboard_manager: Arc>, pub windows: Arc>>, - #[cfg(all(desktop, feature = "system-tray"))] - system_tray_manager: SystemTrayManager, +} + +impl std::fmt::Debug for DispatcherMainThreadContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DispatcherMainThreadContext") + .field("window_target", &self.window_target) + .field("web_context", &self.web_context) + .field("windows", &self.windows) + .finish() + } } // SAFETY: we ensure this type is only used on the main thread. @@ -302,72 +286,11 @@ impl From for HttpResponseWrapper { res_builder = res_builder.header(name, val); } - let res = res_builder.body(Cow::Owned(body)).unwrap(); + let res = res_builder.body(body).unwrap(); Self(res) } } -pub struct MenuItemAttributesWrapper<'a>(pub WryMenuItemAttributes<'a>); - -impl<'a> From<&'a CustomMenuItem> for MenuItemAttributesWrapper<'a> { - fn from(item: &'a CustomMenuItem) -> Self { - let mut attributes = WryMenuItemAttributes::new(&item.title) - .with_enabled(item.enabled) - .with_selected(item.selected) - .with_id(WryMenuId(item.id)); - if let Some(accelerator) = item.keyboard_accelerator.as_ref() { - attributes = attributes.with_accelerators(&accelerator.parse().expect("invalid accelerator")); - } - Self(attributes) - } -} - -pub struct AboutMetadataWrapper(pub WryAboutMetadata); - -impl From for AboutMetadataWrapper { - fn from(metadata: AboutMetadata) -> Self { - Self(WryAboutMetadata { - version: metadata.version, - authors: metadata.authors, - comments: metadata.comments, - copyright: metadata.copyright, - license: metadata.license, - website: metadata.website, - website_label: metadata.website_label, - }) - } -} - -pub struct MenuItemWrapper(pub WryMenuItem); - -impl From for MenuItemWrapper { - fn from(item: MenuItem) -> Self { - match item { - MenuItem::About(name, metadata) => Self(WryMenuItem::About( - name, - AboutMetadataWrapper::from(metadata).0, - )), - MenuItem::Hide => Self(WryMenuItem::Hide), - MenuItem::Services => Self(WryMenuItem::Services), - MenuItem::HideOthers => Self(WryMenuItem::HideOthers), - MenuItem::ShowAll => Self(WryMenuItem::ShowAll), - MenuItem::CloseWindow => Self(WryMenuItem::CloseWindow), - MenuItem::Quit => Self(WryMenuItem::Quit), - MenuItem::Copy => Self(WryMenuItem::Copy), - MenuItem::Cut => Self(WryMenuItem::Cut), - MenuItem::Undo => Self(WryMenuItem::Undo), - MenuItem::Redo => Self(WryMenuItem::Redo), - MenuItem::SelectAll => Self(WryMenuItem::SelectAll), - MenuItem::Paste => Self(WryMenuItem::Paste), - MenuItem::EnterFullScreen => Self(WryMenuItem::EnterFullScreen), - MenuItem::Minimize => Self(WryMenuItem::Minimize), - MenuItem::Zoom => Self(WryMenuItem::Zoom), - MenuItem::Separator => Self(WryMenuItem::Separator), - _ => unimplemented!(), - } - } -} - pub struct DeviceEventFilterWrapper(pub WryDeviceEventFilter); impl From for DeviceEventFilterWrapper { @@ -380,82 +303,6 @@ impl From for DeviceEventFilterWrapper { } } -#[cfg(target_os = "macos")] -pub struct NativeImageWrapper(pub WryNativeImage); - -#[cfg(target_os = "macos")] -impl std::fmt::Debug for NativeImageWrapper { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NativeImageWrapper").finish() - } -} - -#[cfg(target_os = "macos")] -impl From for NativeImageWrapper { - fn from(image: NativeImage) -> NativeImageWrapper { - let wry_image = match image { - NativeImage::Add => WryNativeImage::Add, - NativeImage::Advanced => WryNativeImage::Advanced, - NativeImage::Bluetooth => WryNativeImage::Bluetooth, - NativeImage::Bookmarks => WryNativeImage::Bookmarks, - NativeImage::Caution => WryNativeImage::Caution, - NativeImage::ColorPanel => WryNativeImage::ColorPanel, - NativeImage::ColumnView => WryNativeImage::ColumnView, - NativeImage::Computer => WryNativeImage::Computer, - NativeImage::EnterFullScreen => WryNativeImage::EnterFullScreen, - NativeImage::Everyone => WryNativeImage::Everyone, - NativeImage::ExitFullScreen => WryNativeImage::ExitFullScreen, - NativeImage::FlowView => WryNativeImage::FlowView, - NativeImage::Folder => WryNativeImage::Folder, - NativeImage::FolderBurnable => WryNativeImage::FolderBurnable, - NativeImage::FolderSmart => WryNativeImage::FolderSmart, - NativeImage::FollowLinkFreestanding => WryNativeImage::FollowLinkFreestanding, - NativeImage::FontPanel => WryNativeImage::FontPanel, - NativeImage::GoLeft => WryNativeImage::GoLeft, - NativeImage::GoRight => WryNativeImage::GoRight, - NativeImage::Home => WryNativeImage::Home, - NativeImage::IChatTheater => WryNativeImage::IChatTheater, - NativeImage::IconView => WryNativeImage::IconView, - NativeImage::Info => WryNativeImage::Info, - NativeImage::InvalidDataFreestanding => WryNativeImage::InvalidDataFreestanding, - NativeImage::LeftFacingTriangle => WryNativeImage::LeftFacingTriangle, - NativeImage::ListView => WryNativeImage::ListView, - NativeImage::LockLocked => WryNativeImage::LockLocked, - NativeImage::LockUnlocked => WryNativeImage::LockUnlocked, - NativeImage::MenuMixedState => WryNativeImage::MenuMixedState, - NativeImage::MenuOnState => WryNativeImage::MenuOnState, - NativeImage::MobileMe => WryNativeImage::MobileMe, - NativeImage::MultipleDocuments => WryNativeImage::MultipleDocuments, - NativeImage::Network => WryNativeImage::Network, - NativeImage::Path => WryNativeImage::Path, - NativeImage::PreferencesGeneral => WryNativeImage::PreferencesGeneral, - NativeImage::QuickLook => WryNativeImage::QuickLook, - NativeImage::RefreshFreestanding => WryNativeImage::RefreshFreestanding, - NativeImage::Refresh => WryNativeImage::Refresh, - NativeImage::Remove => WryNativeImage::Remove, - NativeImage::RevealFreestanding => WryNativeImage::RevealFreestanding, - NativeImage::RightFacingTriangle => WryNativeImage::RightFacingTriangle, - NativeImage::Share => WryNativeImage::Share, - NativeImage::Slideshow => WryNativeImage::Slideshow, - NativeImage::SmartBadge => WryNativeImage::SmartBadge, - NativeImage::StatusAvailable => WryNativeImage::StatusAvailable, - NativeImage::StatusNone => WryNativeImage::StatusNone, - NativeImage::StatusPartiallyAvailable => WryNativeImage::StatusPartiallyAvailable, - NativeImage::StatusUnavailable => WryNativeImage::StatusUnavailable, - NativeImage::StopProgressFreestanding => WryNativeImage::StopProgressFreestanding, - NativeImage::StopProgress => WryNativeImage::StopProgress, - - NativeImage::TrashEmpty => WryNativeImage::TrashEmpty, - NativeImage::TrashFull => WryNativeImage::TrashFull, - NativeImage::User => WryNativeImage::User, - NativeImage::UserAccounts => WryNativeImage::UserAccounts, - NativeImage::UserGroup => WryNativeImage::UserGroup, - NativeImage::UserGuest => WryNativeImage::UserGuest, - }; - Self(wry_image) - } -} - /// Wrapper around a [`wry::application::window::Icon`] that can be created from an [`Icon`]. pub struct WryIcon(pub WryWindowIcon); @@ -692,13 +539,24 @@ impl From for CursorIconWrapper { } } -#[derive(Debug, Clone, Default)] +#[derive(Clone, Default)] pub struct WindowBuilderWrapper { inner: WryWindowBuilder, center: bool, #[cfg(target_os = "macos")] tabbing_identifier: Option, - menu: Option, +} + +impl std::fmt::Debug for WindowBuilderWrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut s = f.debug_struct("WindowBuilderWrapper"); + s.field("inner", &self.inner).field("center", &self.center); + #[cfg(target_os = "macos")] + { + s.field("tabbing_identifier", &self.tabbing_identifier); + } + s.finish() + } } // SAFETY: this type is `Send` since `menu_items` are read only here @@ -712,21 +570,7 @@ impl WindowBuilder for WindowBuilderWrapper { } fn with_config(config: WindowConfig) -> Self { - let mut window = WindowBuilderWrapper::new() - .title(config.title.to_string()) - .inner_size(config.width, config.height) - .visible(config.visible) - .resizable(config.resizable) - .maximizable(config.maximizable) - .minimizable(config.minimizable) - .closable(config.closable) - .fullscreen(config.fullscreen) - .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); + let mut window = WindowBuilderWrapper::new(); #[cfg(target_os = "macos")] { @@ -760,28 +604,41 @@ impl WindowBuilder for WindowBuilderWrapper { window.inner = window.inner.with_cursor_moved_event(false); } - if let (Some(min_width), Some(min_height)) = (config.min_width, config.min_height) { - window = window.min_inner_size(min_width, min_height); - } - if let (Some(max_width), Some(max_height)) = (config.max_width, config.max_height) { - window = window.max_inner_size(max_width, max_height); - } - if let (Some(x), Some(y)) = (config.x, config.y) { - window = window.position(x, y); - } + #[cfg(desktop)] + { + window = window + .title(config.title.to_string()) + .inner_size(config.width, config.height) + .visible(config.visible) + .resizable(config.resizable) + .fullscreen(config.fullscreen) + .decorations(config.decorations) + .maximized(config.maximized) + .always_on_top(config.always_on_top) + .visible_on_all_workspaces(config.visible_on_all_workspaces) + .content_protected(config.content_protected) + .skip_taskbar(config.skip_taskbar) + .theme(config.theme) + .shadow(config.shadow); + + if let (Some(min_width), Some(min_height)) = (config.min_width, config.min_height) { + window = window.min_inner_size(min_width, min_height); + } + if let (Some(max_width), Some(max_height)) = (config.max_width, config.max_height) { + window = window.max_inner_size(max_width, max_height); + } + if let (Some(x), Some(y)) = (config.x, config.y) { + window = window.position(x, y); + } - if config.center { - window = window.center(); + if config.center { + window = window.center(); + } } window } - fn menu(mut self, menu: Menu) -> Self { - self.menu.replace(menu); - self - } - fn center(mut self) -> Self { self.center = true; self @@ -880,14 +737,33 @@ impl WindowBuilder for WindowBuilderWrapper { self } + fn visible_on_all_workspaces(mut self, visible_on_all_workspaces: bool) -> Self { + self.inner = self + .inner + .with_visible_on_all_workspaces(visible_on_all_workspaces); + self + } + fn content_protected(mut self, protected: bool) -> Self { self.inner = self.inner.with_content_protection(protected); self } + fn shadow(#[allow(unused_mut)] mut self, _enable: bool) -> Self { + #[cfg(windows)] + { + self.inner = self.inner.with_undecorated_shadow(_enable); + } + #[cfg(target_os = "macos")] + { + self.inner = self.inner.with_has_shadow(_enable); + } + self + } + #[cfg(windows)] fn parent_window(mut self, parent: HWND) -> Self { - self.inner = self.inner.with_parent_window(parent); + self.inner = self.inner.with_parent_window(parent.0); self } @@ -899,7 +775,7 @@ impl WindowBuilder for WindowBuilderWrapper { #[cfg(windows)] fn owner_window(mut self, owner: HWND) -> Self { - self.inner = self.inner.with_owner_window(owner); + self.inner = self.inner.with_owner_window(owner.0); self } @@ -971,10 +847,6 @@ impl WindowBuilder for WindowBuilderWrapper { fn has_icon(&self) -> bool { self.inner.window.window_icon.is_some() } - - fn get_menu(&self) -> Option<&Menu> { - self.menu.as_ref() - } } pub struct FileDropEventWrapper(WryFileDropEvent); @@ -1009,12 +881,14 @@ fn decode_path(path: PathBuf) -> PathBuf { impl From for FileDropEvent { fn from(event: FileDropEventWrapper) -> Self { match event.0 { - WryFileDropEvent::Hovered(paths) => { - FileDropEvent::Hovered(paths.into_iter().map(decode_path).collect()) - } - WryFileDropEvent::Dropped(paths) => { - FileDropEvent::Dropped(paths.into_iter().map(decode_path).collect()) - } + WryFileDropEvent::Hovered { paths, position } => FileDropEvent::Hovered { + paths: paths.into_iter().map(decode_path).collect(), + position: PhysicalPositionWrapper(position).into(), + }, + WryFileDropEvent::Dropped { paths, position } => FileDropEvent::Dropped { + paths: paths.into_iter().map(decode_path).collect(), + position: PhysicalPositionWrapper(position).into(), + }, // default to cancelled // FIXME(maybe): Add `FileDropEvent::Unknown` event? _ => FileDropEvent::Cancelled, @@ -1040,6 +914,24 @@ pub struct GtkWindow(pub gtk::ApplicationWindow); #[allow(clippy::non_send_fields_in_send_ty)] unsafe impl Send for GtkWindow {} +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +pub struct GtkBox(pub gtk::Box); +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Send for GtkBox {} + pub struct RawWindowHandle(pub raw_window_handle::RawWindowHandle); unsafe impl Send for RawWindowHandle {} @@ -1051,10 +943,8 @@ pub enum ApplicationMessage { } pub enum WindowMessage { - #[cfg(desktop)] WithWebview(Box), AddEventListener(Uuid, Box), - AddMenuEventListener(Uuid, Box), // Devtools #[cfg(any(debug_assertions, feature = "devtools"))] OpenDevTools, @@ -1080,7 +970,6 @@ pub enum WindowMessage { IsClosable(Sender), IsVisible(Sender), Title(Sender), - IsMenuVisible(Sender), CurrentMonitor(Sender>), PrimaryMonitor(Sender>), AvailableMonitors(Sender>), @@ -1092,6 +981,14 @@ pub enum WindowMessage { target_os = "openbsd" ))] GtkWindow(Sender), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + GtkBox(Sender), RawWindowHandle(Sender), Theme(Sender), // Setters @@ -1102,17 +999,18 @@ pub enum WindowMessage { SetMinimizable(bool), SetClosable(bool), SetTitle(String), + Navigate(Url), Maximize, Unmaximize, Minimize, Unminimize, - ShowMenu, - HideMenu, Show, Hide, Close, SetDecorations(bool), + SetShadow(bool), SetAlwaysOnTop(bool), + SetVisibleOnAllWorkspaces(bool), SetContentProtected(bool), SetSize(Size), SetMinSize(Option), @@ -1128,7 +1026,6 @@ pub enum WindowMessage { SetCursorPosition(Position), SetIgnoreCursorEvents(bool), DragWindow, - UpdateMenuItem(u16, MenuUpdate), RequestRedraw, } @@ -1146,21 +1043,6 @@ pub enum WebviewEvent { Focused(bool), } -#[cfg(all(desktop, feature = "system-tray"))] -#[derive(Debug, Clone)] -pub enum TrayMessage { - UpdateItem(u16, MenuUpdate), - UpdateMenu(SystemTrayMenu), - UpdateIcon(Icon), - #[cfg(target_os = "macos")] - UpdateIconAsTemplate(bool), - #[cfg(target_os = "macos")] - UpdateTitle(String), - UpdateTooltip(String), - Create(SystemTray, Sender>), - Destroy(Sender>), -} - pub type CreateWebviewClosure = Box< dyn FnOnce(&EventLoopWindowTarget>, &WebContextStore) -> Result + Send, >; @@ -1171,18 +1053,12 @@ pub enum Message { Application(ApplicationMessage), Window(WebviewId, WindowMessage), Webview(WebviewId, WebviewMessage), - #[cfg(all(desktop, feature = "system-tray"))] - Tray(TrayId, TrayMessage), CreateWebview(WebviewId, CreateWebviewClosure), CreateWindow( WebviewId, Box (String, WryWindowBuilder) + Send>, Sender>>, ), - #[cfg(all(desktop, feature = "global-shortcut"))] - GlobalShortcut(GlobalShortcutMessage), - #[cfg(feature = "clipboard")] - Clipboard(ClipboardMessage), UserEvent(T), } @@ -1190,12 +1066,6 @@ impl Clone for Message { fn clone(&self) -> Self { match self { Self::Webview(i, m) => Self::Webview(*i, m.clone()), - #[cfg(all(desktop, feature = "system-tray"))] - Self::Tray(i, m) => Self::Tray(*i, m.clone()), - #[cfg(all(desktop, feature = "global-shortcut"))] - Self::GlobalShortcut(m) => Self::GlobalShortcut(m.clone()), - #[cfg(feature = "clipboard")] - Self::Clipboard(m) => Self::Clipboard(m.clone()), Self::UserEvent(t) => Self::UserEvent(t.clone()), _ => unimplemented!(), } @@ -1213,16 +1083,6 @@ pub struct WryDispatcher { #[allow(clippy::non_send_fields_in_send_ty)] unsafe impl Sync for WryDispatcher {} -impl WryDispatcher { - #[cfg(desktop)] - pub fn with_webview(&self, f: F) -> Result<()> { - send_user_message( - &self.context, - Message::Window(self.window_id, WindowMessage::WithWebview(Box::new(f))), - ) - } -} - impl Dispatch for WryDispatcher { type Runtime = Wry; type WindowBuilder = WindowBuilderWrapper; @@ -1240,13 +1100,14 @@ impl Dispatch for WryDispatcher { id } - fn on_menu_event(&self, f: F) -> Uuid { - let id = Uuid::new_v4(); - let _ = self.context.proxy.send_event(Message::Window( - self.window_id, - WindowMessage::AddMenuEventListener(id, Box::new(f)), - )); - id + fn with_webview) + Send + 'static>(&self, f: F) -> Result<()> { + send_user_message( + &self.context, + Message::Window( + self.window_id, + WindowMessage::WithWebview(Box::new(move |webview| f(Box::new(webview)))), + ), + ) } #[cfg(any(debug_assertions, feature = "devtools"))] @@ -1346,10 +1207,6 @@ impl Dispatch for WryDispatcher { window_getter!(self, WindowMessage::Title) } - fn is_menu_visible(&self) -> Result { - window_getter!(self, WindowMessage::IsMenuVisible) - } - fn current_monitor(&self) -> Result> { Ok(window_getter!(self, WindowMessage::CurrentMonitor)?.map(|m| MonitorHandleWrapper(m).into())) } @@ -1371,7 +1228,6 @@ impl Dispatch for WryDispatcher { window_getter!(self, WindowMessage::Theme) } - /// Returns the `ApplicationWindow` from gtk crate that is used by this window. #[cfg(any( target_os = "linux", target_os = "dragonfly", @@ -1383,6 +1239,17 @@ impl Dispatch for WryDispatcher { window_getter!(self, WindowMessage::GtkWindow).map(|w| w.0) } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn default_vbox(&self) -> Result { + window_getter!(self, WindowMessage::GtkBox).map(|w| w.0) + } + fn raw_window_handle(&self) -> Result { window_getter!(self, WindowMessage::RawWindowHandle).map(|w| w.0) } @@ -1415,11 +1282,14 @@ impl Dispatch for WryDispatcher { // Creates a window by dispatching a message to the event loop. // Note that this must be called from a separate thread, otherwise the channel will introduce a deadlock. - fn create_window( + fn create_window( &mut self, pending: PendingWindow, + before_webview_creation: Option, ) -> Result> { - self.context.create_webview(pending) + self + .context + .create_webview(pending, before_webview_creation) } fn set_resizable(&self, resizable: bool) -> Result<()> { @@ -1457,6 +1327,13 @@ impl Dispatch for WryDispatcher { ) } + fn navigate(&self, url: Url) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::Navigate(url)), + ) + } + fn maximize(&self) -> Result<()> { send_user_message( &self.context, @@ -1485,20 +1362,6 @@ impl Dispatch for WryDispatcher { ) } - fn show_menu(&self) -> Result<()> { - send_user_message( - &self.context, - Message::Window(self.window_id, WindowMessage::ShowMenu), - ) - } - - fn hide_menu(&self) -> Result<()> { - send_user_message( - &self.context, - Message::Window(self.window_id, WindowMessage::HideMenu), - ) - } - fn show(&self) -> Result<()> { send_user_message( &self.context, @@ -1529,6 +1392,13 @@ impl Dispatch for WryDispatcher { ) } + fn set_shadow(&self, enable: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window(self.window_id, WindowMessage::SetShadow(enable)), + ) + } + fn set_always_on_top(&self, always_on_top: bool) -> Result<()> { send_user_message( &self.context, @@ -1536,6 +1406,16 @@ impl Dispatch for WryDispatcher { ) } + fn set_visible_on_all_workspaces(&self, visible_on_all_workspaces: bool) -> Result<()> { + send_user_message( + &self.context, + Message::Window( + self.window_id, + WindowMessage::SetVisibleOnAllWorkspaces(visible_on_all_workspaces), + ), + ) + } + fn set_content_protected(&self, protected: bool) -> Result<()> { send_user_message( &self.context, @@ -1659,13 +1539,6 @@ impl Dispatch for WryDispatcher { ), ) } - - fn update_menu_item(&self, id: u16, update: MenuUpdate) -> Result<()> { - send_user_message( - &self.context, - Message::Window(self.window_id, WindowMessage::UpdateMenuItem(id, update)), - ) - } } #[derive(Clone)] @@ -1724,9 +1597,7 @@ impl WindowHandle { pub struct WindowWrapper { label: String, inner: Option, - menu_items: Option>, window_event_listeners: WindowEventListeners, - menu_event_listeners: WindowMenuEventListeners, } impl fmt::Debug for WindowWrapper { @@ -1734,7 +1605,6 @@ impl fmt::Debug for WindowWrapper { f.debug_struct("WindowWrapper") .field("label", &self.label) .field("inner", &self.inner) - .field("menu_items", &self.menu_items) .finish() } } @@ -1775,51 +1645,17 @@ pub trait Plugin { /// A Tauri [`Runtime`] wrapper around wry. pub struct Wry { context: Context, - - plugins: Vec>>, - - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager_handle: GlobalShortcutManagerHandle, - - #[cfg(feature = "clipboard")] - clipboard_manager_handle: ClipboardManagerWrapper, - event_loop: EventLoop>, } impl fmt::Debug for Wry { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut d = f.debug_struct("Wry"); - d.field("main_thread_id", &self.context.main_thread_id) + f.debug_struct("Wry") + .field("main_thread_id", &self.context.main_thread_id) .field("event_loop", &self.event_loop) .field("windows", &self.context.main_thread.windows) - .field("web_context", &self.context.main_thread.web_context); - - #[cfg(all(desktop, feature = "system-tray"))] - d.field( - "system_tray_manager", - &self.context.main_thread.system_tray_manager, - ); - - #[cfg(all(desktop, feature = "global-shortcut"))] - #[cfg(feature = "global-shortcut")] - d.field( - "global_shortcut_manager", - &self.context.main_thread.global_shortcut_manager, - ) - .field( - "global_shortcut_manager_handle", - &self.global_shortcut_manager_handle, - ); - - #[cfg(feature = "clipboard")] - d.field( - "clipboard_manager", - &self.context.main_thread.clipboard_manager, - ) - .field("clipboard_manager_handle", &self.clipboard_manager_handle); - - d.finish() + .field("web_context", &self.context.main_thread.web_context) + .finish() } } @@ -1868,6 +1704,18 @@ impl WryHandle { .map_err(|_| Error::FailedToSendMessage)?; Ok(()) } + + pub fn plugin + 'static>(&mut self, plugin: P) + where +

>::Plugin: Send, + { + self + .context + .plugins + .lock() + .unwrap() + .push(Box::new(plugin.build(self.context.clone()))); + } } impl RuntimeHandle for WryHandle { @@ -1879,40 +1727,43 @@ impl RuntimeHandle for WryHandle { // Creates a window by dispatching a message to the event loop. // Note that this must be called from a separate thread, otherwise the channel will introduce a deadlock. - fn create_window( + fn create_window( &self, pending: PendingWindow, + before_webview_creation: Option, ) -> Result> { - self.context.create_webview(pending) + self + .context + .create_webview(pending, before_webview_creation) } fn run_on_main_thread(&self, f: F) -> Result<()> { send_user_message(&self.context, Message::Task(Box::new(f))) } - #[cfg(all(desktop, feature = "system-tray"))] - fn system_tray( - &self, - system_tray: SystemTray, - ) -> Result<>::TrayHandler> { - let id = system_tray.id; - let (tx, rx) = channel(); - send_user_message( - &self.context, - Message::Tray(id, TrayMessage::Create(system_tray, tx)), - )?; - rx.recv().unwrap()?; - Ok(SystemTrayHandle { - context: self.context.clone(), - id, - proxy: self.context.proxy.clone(), - }) - } - fn raw_display_handle(&self) -> RawDisplayHandle { self.context.main_thread.window_target.raw_display_handle() } + fn primary_monitor(&self) -> Option { + self + .context + .main_thread + .window_target + .primary_monitor() + .map(|m| MonitorHandleWrapper(m).into()) + } + + fn available_monitors(&self) -> Vec { + self + .context + .main_thread + .window_target + .available_monitors() + .map(|m| MonitorHandleWrapper(m).into()) + .collect() + } + #[cfg(target_os = "macos")] fn show(&self) -> tauri_runtime::Result<()> { send_user_message( @@ -1928,25 +1779,46 @@ impl RuntimeHandle for WryHandle { Message::Application(ApplicationMessage::Hide), ) } + + #[cfg(target_os = "android")] + fn find_class<'a>( + &self, + env: &mut jni::JNIEnv<'a>, + activity: &jni::objects::JObject<'_>, + name: impl Into, + ) -> std::result::Result, jni::errors::Error> { + find_class(env, activity, name.into()) + } + + #[cfg(target_os = "android")] + fn run_on_android_context(&self, f: F) + where + F: FnOnce(&mut jni::JNIEnv, &jni::objects::JObject, &jni::objects::JObject) + Send + 'static, + { + dispatch(f) + } } impl Wry { + fn init_with_builder( + mut event_loop_builder: EventLoopBuilder>, + #[allow(unused_variables)] args: RuntimeInitArgs, + ) -> Result { + #[cfg(windows)] + if let Some(hook) = args.msg_hook { + use wry::application::platform::windows::EventLoopBuilderExtWindows; + event_loop_builder.with_msg_hook(hook); + } + Self::init(event_loop_builder.build()) + } + fn init(event_loop: EventLoop>) -> Result { let main_thread_id = current_thread().id(); let web_context = WebContextStore::default(); - #[cfg(all(desktop, feature = "global-shortcut"))] - let global_shortcut_manager = Arc::new(Mutex::new(WryShortcutManager::new(&event_loop))); - - #[cfg(feature = "clipboard")] - let clipboard_manager = Arc::new(Mutex::new(Clipboard::new())); - let windows = Arc::new(RefCell::new(HashMap::default())); let webview_id_map = WebviewIdStore::default(); - #[cfg(all(desktop, feature = "system-tray"))] - let system_tray_manager = Default::default(); - let context = Context { webview_id_map, main_thread_id, @@ -1954,79 +1826,47 @@ impl Wry { main_thread: DispatcherMainThreadContext { window_target: event_loop.deref().clone(), web_context, - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager, - #[cfg(feature = "clipboard")] - clipboard_manager, windows, - #[cfg(all(desktop, feature = "system-tray"))] - system_tray_manager, }, - }; - - #[cfg(all(desktop, feature = "global-shortcut"))] - let global_shortcut_manager_handle = GlobalShortcutManagerHandle { - context: context.clone(), - shortcuts: Default::default(), - listeners: Default::default(), - }; - - #[cfg(feature = "clipboard")] - #[allow(clippy::redundant_clone)] - let clipboard_manager_handle = ClipboardManagerWrapper { - context: context.clone(), + plugins: Default::default(), }; Ok(Self { context, - - plugins: Default::default(), - - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager_handle, - - #[cfg(feature = "clipboard")] - clipboard_manager_handle, - event_loop, }) } - - pub fn plugin + 'static>(&mut self, plugin: P) { - self - .plugins - .push(Box::new(plugin.build(self.context.clone()))); - } } impl Runtime for Wry { type Dispatcher = WryDispatcher; type Handle = WryHandle; - #[cfg(all(desktop, feature = "global-shortcut"))] - type GlobalShortcutManager = GlobalShortcutManagerHandle; - - #[cfg(feature = "clipboard")] - type ClipboardManager = ClipboardManagerWrapper; - - #[cfg(all(desktop, feature = "system-tray"))] - type TrayHandler = SystemTrayHandle; - type EventLoopProxy = EventProxy; - fn new() -> Result { - let event_loop = EventLoop::>::with_user_event(); - Self::init(event_loop) + fn new(args: RuntimeInitArgs) -> Result { + Self::init_with_builder(EventLoopBuilder::>::with_user_event(), args) + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn new_any_thread(args: RuntimeInitArgs) -> Result { + use wry::application::platform::unix::EventLoopBuilderExtUnix; + let mut event_loop_builder = EventLoopBuilder::>::with_user_event(); + event_loop_builder.with_any_thread(true); + Self::init_with_builder(event_loop_builder, args) } - #[cfg(any(windows, target_os = "linux"))] - fn new_any_thread() -> Result { - #[cfg(target_os = "linux")] - use wry::application::platform::unix::EventLoopExtUnix; - #[cfg(windows)] - use wry::application::platform::windows::EventLoopExtWindows; - let event_loop = EventLoop::>::new_any_thread(); - Self::init(event_loop) + #[cfg(windows)] + fn new_any_thread(args: RuntimeInitArgs) -> Result { + use wry::application::platform::windows::EventLoopBuilderExtWindows; + let mut event_loop_builder = EventLoopBuilder::>::with_user_event(); + event_loop_builder.with_any_thread(true); + Self::init_with_builder(event_loop_builder, args) } fn create_proxy(&self) -> EventProxy { @@ -2039,20 +1879,12 @@ impl Runtime for Wry { } } - #[cfg(all(desktop, feature = "global-shortcut"))] - fn global_shortcut_manager(&self) -> Self::GlobalShortcutManager { - self.global_shortcut_manager_handle.clone() - } - - #[cfg(feature = "clipboard")] - fn clipboard_manager(&self) -> Self::ClipboardManager { - self.clipboard_manager_handle.clone() - } - - fn create_window(&self, pending: PendingWindow) -> Result> { + fn create_window( + &self, + pending: PendingWindow, + before_webview_creation: Option, + ) -> Result> { let label = pending.label.clone(); - let menu_ids = pending.menu_ids.clone(); - let js_event_listeners = pending.js_event_listeners.clone(); let window_id = rand::random(); let webview = create_webview( @@ -2061,6 +1893,7 @@ impl Runtime for Wry { &self.context.main_thread.web_context, self.context.clone(), pending, + before_webview_creation, )?; let dispatcher = WryDispatcher { @@ -2075,55 +1908,26 @@ impl Runtime for Wry { .borrow_mut() .insert(window_id, webview); - Ok(DetachedWindow { - label, - dispatcher, - menu_ids, - js_event_listeners, - }) + Ok(DetachedWindow { label, dispatcher }) } - #[cfg(all(desktop, feature = "system-tray"))] - fn system_tray(&self, mut system_tray: SystemTray) -> Result { - let id = system_tray.id; - let mut listeners = Vec::new(); - if let Some(l) = system_tray.on_event.take() { - listeners.push(Arc::new(l)); - } - let (tray, items) = create_tray(WryTrayId(id), system_tray, &self.event_loop)?; + fn primary_monitor(&self) -> Option { self .context .main_thread - .system_tray_manager - .trays - .lock() - .unwrap() - .insert( - id, - TrayContext { - tray: Arc::new(Mutex::new(Some(tray))), - listeners: Arc::new(Mutex::new(listeners)), - items: Arc::new(Mutex::new(items)), - }, - ); - - Ok(SystemTrayHandle { - context: self.context.clone(), - id, - proxy: self.event_loop.create_proxy(), - }) + .window_target + .primary_monitor() + .map(|m| MonitorHandleWrapper(m).into()) } - #[cfg(all(desktop, feature = "system-tray"))] - fn on_system_tray_event(&mut self, f: F) { + fn available_monitors(&self) -> Vec { self .context .main_thread - .system_tray_manager - .global_listeners - .lock() - .unwrap() - .push(Arc::new(Box::new(f))); + .window_target + .available_monitors() + .map(|m| MonitorHandleWrapper(m).into()) + .collect() } #[cfg(target_os = "macos")] @@ -2160,17 +1964,8 @@ impl Runtime for Wry { let windows = self.context.main_thread.windows.clone(); let webview_id_map = self.context.webview_id_map.clone(); let web_context = &self.context.main_thread.web_context; - let plugins = &mut self.plugins; - #[cfg(all(desktop, feature = "system-tray"))] - let system_tray_manager = self.context.main_thread.system_tray_manager.clone(); - - #[cfg(all(desktop, feature = "global-shortcut"))] - let global_shortcut_manager = self.context.main_thread.global_shortcut_manager.clone(); - #[cfg(all(desktop, feature = "global-shortcut"))] - let global_shortcut_manager_handle = self.global_shortcut_manager_handle.clone(); + let plugins = self.context.plugins.clone(); - #[cfg(feature = "clipboard")] - let clipboard_manager = self.context.main_thread.clipboard_manager.clone(); let mut iteration = RunIteration::default(); let proxy = self.event_loop.create_proxy(); @@ -2183,7 +1978,7 @@ impl Runtime for Wry { *control_flow = ControlFlow::Exit; } - for p in plugins.iter_mut() { + for p in plugins.lock().unwrap().iter_mut() { let prevent_default = p.on_event( &event, event_loop, @@ -2193,14 +1988,6 @@ impl Runtime for Wry { callback: &mut callback, webview_id_map: webview_id_map.clone(), windows: windows.clone(), - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager: global_shortcut_manager.clone(), - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager_handle: &global_shortcut_manager_handle, - #[cfg(feature = "clipboard")] - clipboard_manager: clipboard_manager.clone(), - #[cfg(all(desktop, feature = "system-tray"))] - system_tray_manager: system_tray_manager.clone(), }, web_context, ); @@ -2217,14 +2004,6 @@ impl Runtime for Wry { callback: &mut callback, windows: windows.clone(), webview_id_map: webview_id_map.clone(), - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager: global_shortcut_manager.clone(), - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager_handle: &global_shortcut_manager_handle, - #[cfg(feature = "clipboard")] - clipboard_manager: clipboard_manager.clone(), - #[cfg(all(desktop, feature = "system-tray"))] - system_tray_manager: system_tray_manager.clone(), }, web_context, ); @@ -2237,23 +2016,12 @@ impl Runtime for Wry { let windows = self.context.main_thread.windows.clone(); let webview_id_map = self.context.webview_id_map.clone(); let web_context = self.context.main_thread.web_context; - let mut plugins = self.plugins; - - #[cfg(all(desktop, feature = "system-tray"))] - let system_tray_manager = self.context.main_thread.system_tray_manager; - - #[cfg(all(desktop, feature = "global-shortcut"))] - let global_shortcut_manager = self.context.main_thread.global_shortcut_manager.clone(); - #[cfg(all(desktop, feature = "global-shortcut"))] - let global_shortcut_manager_handle = self.global_shortcut_manager_handle.clone(); - - #[cfg(feature = "clipboard")] - let clipboard_manager = self.context.main_thread.clipboard_manager.clone(); + let plugins = self.context.plugins.clone(); let proxy = self.event_loop.create_proxy(); self.event_loop.run(move |event, event_loop, control_flow| { - for p in &mut plugins { + for p in plugins.lock().unwrap().iter_mut() { let prevent_default = p.on_event( &event, event_loop, @@ -2263,14 +2031,6 @@ impl Runtime for Wry { callback: &mut callback, webview_id_map: webview_id_map.clone(), windows: windows.clone(), - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager: global_shortcut_manager.clone(), - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager_handle: &global_shortcut_manager_handle, - #[cfg(feature = "clipboard")] - clipboard_manager: clipboard_manager.clone(), - #[cfg(all(desktop, feature = "system-tray"))] - system_tray_manager: system_tray_manager.clone(), }, &web_context, ); @@ -2286,14 +2046,6 @@ impl Runtime for Wry { callback: &mut callback, webview_id_map: webview_id_map.clone(), windows: windows.clone(), - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager: global_shortcut_manager.clone(), - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager_handle: &global_shortcut_manager_handle, - #[cfg(feature = "clipboard")] - clipboard_manager: clipboard_manager.clone(), - #[cfg(all(desktop, feature = "system-tray"))] - system_tray_manager: system_tray_manager.clone(), }, &web_context, ); @@ -2305,25 +2057,11 @@ pub struct EventLoopIterationContext<'a, T: UserEvent> { pub callback: &'a mut (dyn FnMut(RunEvent) + 'static), pub webview_id_map: WebviewIdStore, pub windows: Arc>>, - #[cfg(all(desktop, feature = "global-shortcut"))] - pub global_shortcut_manager: Arc>, - #[cfg(all(desktop, feature = "global-shortcut"))] - pub global_shortcut_manager_handle: &'a GlobalShortcutManagerHandle, - #[cfg(feature = "clipboard")] - pub clipboard_manager: Arc>, - #[cfg(all(desktop, feature = "system-tray"))] - pub system_tray_manager: SystemTrayManager, } struct UserMessageContext { windows: Arc>>, webview_id_map: WebviewIdStore, - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager: Arc>, - #[cfg(feature = "clipboard")] - clipboard_manager: Arc>, - #[cfg(all(desktop, feature = "system-tray"))] - system_tray_manager: SystemTrayManager, } fn handle_user_message( @@ -2334,13 +2072,7 @@ fn handle_user_message( ) -> RunIteration { let UserMessageContext { webview_id_map, - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager, - #[cfg(feature = "clipboard")] - clipboard_manager, windows, - #[cfg(all(desktop, feature = "system-tray"))] - system_tray_manager, } = context; match message { Message::Task(task) => task(), @@ -2354,237 +2086,240 @@ fn handle_user_message( } }, Message::Window(id, window_message) => { - if let WindowMessage::UpdateMenuItem(item_id, update) = window_message { - if let Some(menu_items) = windows.borrow_mut().get_mut(&id).map(|w| &mut w.menu_items) { - if let Some(menu_items) = menu_items.as_mut() { - let item = menu_items.get_mut(&item_id).expect("menu item not found"); - match update { - MenuUpdate::SetEnabled(enabled) => item.set_enabled(enabled), - MenuUpdate::SetTitle(title) => item.set_title(&title), - MenuUpdate::SetSelected(selected) => item.set_selected(selected), - #[cfg(target_os = "macos")] - MenuUpdate::SetNativeImage(image) => { - item.set_native_image(NativeImageWrapper::from(image).0) + let w = windows + .borrow() + .get(&id) + .map(|w| (w.inner.clone(), w.window_event_listeners.clone())); + if let Some((Some(window), window_event_listeners)) = w { + match window_message { + WindowMessage::WithWebview(f) => { + if let WindowHandle::Webview { inner: w, .. } = &window { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + use wry::webview::WebviewExtUnix; + f(w.webview()); } - } - } - } - } else { - let w = windows.borrow().get(&id).map(|w| { - ( - w.inner.clone(), - w.window_event_listeners.clone(), - w.menu_event_listeners.clone(), - ) - }); - if let Some((Some(window), window_event_listeners, menu_event_listeners)) = w { - match window_message { - #[cfg(desktop)] - WindowMessage::WithWebview(f) => { - if let WindowHandle::Webview { inner: w, .. } = &window { - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - { - use wry::webview::WebviewExtUnix; - f(w.webview()); - } - #[cfg(target_os = "macos")] - { - use wry::webview::WebviewExtMacOS; - f(Webview { - webview: w.webview(), - manager: w.manager(), - ns_window: w.ns_window(), - }); - } - - #[cfg(windows)] - { - f(Webview { - controller: w.controller(), - }); - } - } - } - - WindowMessage::AddEventListener(id, listener) => { - window_event_listeners.lock().unwrap().insert(id, listener); - } - - WindowMessage::AddMenuEventListener(id, listener) => { - menu_event_listeners.lock().unwrap().insert(id, listener); - } - - #[cfg(any(debug_assertions, feature = "devtools"))] - WindowMessage::OpenDevTools => { - if let WindowHandle::Webview { inner: w, .. } = &window { - w.open_devtools(); + #[cfg(target_os = "macos")] + { + use wry::webview::WebviewExtMacOS; + f(Webview { + webview: w.webview(), + manager: w.manager(), + ns_window: w.ns_window(), + }); } - } - #[cfg(any(debug_assertions, feature = "devtools"))] - WindowMessage::CloseDevTools => { - if let WindowHandle::Webview { inner: w, .. } = &window { - w.close_devtools(); + #[cfg(target_os = "ios")] + { + use wry::{application::platform::ios::WindowExtIOS, webview::WebviewExtIOS}; + + f(Webview { + webview: w.webview(), + manager: w.manager(), + view_controller: w.window().ui_view_controller() as cocoa::base::id, + }); } - } - #[cfg(any(debug_assertions, feature = "devtools"))] - WindowMessage::IsDevToolsOpen(tx) => { - if let WindowHandle::Webview { inner: w, .. } = &window { - tx.send(w.is_devtools_open()).unwrap(); - } else { - tx.send(false).unwrap(); + #[cfg(windows)] + { + f(Webview { + controller: w.controller(), + }); } - } - // Getters - WindowMessage::Url(tx) => { - if let WindowHandle::Webview { inner: w, .. } = &window { - tx.send(w.url()).unwrap(); + #[cfg(target_os = "android")] + { + f(w.handle()) } } - WindowMessage::ScaleFactor(tx) => tx.send(window.scale_factor()).unwrap(), - WindowMessage::InnerPosition(tx) => tx - .send( - window - .inner_position() - .map(|p| PhysicalPositionWrapper(p).into()) - .map_err(|_| Error::FailedToSendMessage), - ) - .unwrap(), - WindowMessage::OuterPosition(tx) => tx - .send( - window - .outer_position() - .map(|p| PhysicalPositionWrapper(p).into()) - .map_err(|_| Error::FailedToSendMessage), - ) - .unwrap(), - WindowMessage::InnerSize(tx) => tx - .send(PhysicalSizeWrapper(window.inner_size()).into()) - .unwrap(), - WindowMessage::OuterSize(tx) => tx - .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::IsFocused(tx) => tx.send(window.is_focused()).unwrap(), - WindowMessage::IsDecorated(tx) => tx.send(window.is_decorated()).unwrap(), - WindowMessage::IsResizable(tx) => tx.send(window.is_resizable()).unwrap(), - WindowMessage::IsMaximizable(tx) => tx.send(window.is_maximizable()).unwrap(), - WindowMessage::IsMinimizable(tx) => tx.send(window.is_minimizable()).unwrap(), - WindowMessage::IsClosable(tx) => tx.send(window.is_closable()).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(), - WindowMessage::AvailableMonitors(tx) => { - tx.send(window.available_monitors().collect()).unwrap() - } - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] - WindowMessage::GtkWindow(tx) => { - tx.send(GtkWindow(window.gtk_window().clone())).unwrap() - } - WindowMessage::RawWindowHandle(tx) => tx - .send(RawWindowHandle(window.raw_window_handle())) - .unwrap(), - WindowMessage::Theme(tx) => { - tx.send(map_theme(&window.theme())).unwrap(); - } - // Setters - WindowMessage::Center => { - let _ = center_window(&window, window.inner_size()); - } - WindowMessage::RequestUserAttention(request_type) => { - window.request_user_attention(request_type.map(|r| r.0)); - } - WindowMessage::SetResizable(resizable) => window.set_resizable(resizable), - WindowMessage::SetMaximizable(maximizable) => window.set_maximizable(maximizable), - WindowMessage::SetMinimizable(minimizable) => window.set_minimizable(minimizable), - WindowMessage::SetClosable(closable) => window.set_closable(closable), - WindowMessage::SetTitle(title) => window.set_title(&title), - WindowMessage::Maximize => window.set_maximized(true), - WindowMessage::Unmaximize => window.set_maximized(false), - WindowMessage::Minimize => window.set_minimized(true), - WindowMessage::Unminimize => window.set_minimized(false), - WindowMessage::ShowMenu => window.show_menu(), - WindowMessage::HideMenu => window.hide_menu(), - WindowMessage::Show => window.set_visible(true), - WindowMessage::Hide => window.set_visible(false), - WindowMessage::Close => { - panic!("cannot handle `WindowMessage::Close` on the main thread") - } - 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); - } - WindowMessage::SetMinSize(size) => { - window.set_min_inner_size(size.map(|s| SizeWrapper::from(s).0)); - } - WindowMessage::SetMaxSize(size) => { - window.set_max_inner_size(size.map(|s| SizeWrapper::from(s).0)); - } - WindowMessage::SetPosition(position) => { - window.set_outer_position(PositionWrapper::from(position).0) - } - WindowMessage::SetFullscreen(fullscreen) => { - if fullscreen { - window.set_fullscreen(Some(Fullscreen::Borderless(None))) - } else { - window.set_fullscreen(None) - } - } - WindowMessage::SetFocus => { - window.set_focus(); - } - WindowMessage::SetIcon(icon) => { - window.set_window_icon(Some(icon)); - } - #[allow(unused_variables)] - WindowMessage::SetSkipTaskbar(skip) => { - #[cfg(any(windows, target_os = "linux"))] - window.set_skip_taskbar(skip); - } - WindowMessage::SetCursorGrab(grab) => { - let _ = window.set_cursor_grab(grab); - } - WindowMessage::SetCursorVisible(visible) => { - window.set_cursor_visible(visible); - } - WindowMessage::SetCursorIcon(icon) => { - window.set_cursor_icon(CursorIconWrapper::from(icon).0); + } + + WindowMessage::AddEventListener(id, listener) => { + window_event_listeners.lock().unwrap().insert(id, listener); + } + + #[cfg(any(debug_assertions, feature = "devtools"))] + WindowMessage::OpenDevTools => { + if let WindowHandle::Webview { inner: w, .. } = &window { + w.open_devtools(); } - WindowMessage::SetCursorPosition(position) => { - let _ = window.set_cursor_position(PositionWrapper::from(position).0); + } + #[cfg(any(debug_assertions, feature = "devtools"))] + WindowMessage::CloseDevTools => { + if let WindowHandle::Webview { inner: w, .. } = &window { + w.close_devtools(); } - WindowMessage::SetIgnoreCursorEvents(ignore) => { - let _ = window.set_ignore_cursor_events(ignore); + } + #[cfg(any(debug_assertions, feature = "devtools"))] + WindowMessage::IsDevToolsOpen(tx) => { + if let WindowHandle::Webview { inner: w, .. } = &window { + tx.send(w.is_devtools_open()).unwrap(); + } else { + tx.send(false).unwrap(); } - WindowMessage::DragWindow => { - let _ = window.drag_window(); + } + + // Getters + WindowMessage::Url(tx) => { + if let WindowHandle::Webview { inner: w, .. } = &window { + tx.send(w.url()).unwrap(); } - WindowMessage::UpdateMenuItem(_id, _update) => { - // already handled + } + WindowMessage::ScaleFactor(tx) => tx.send(window.scale_factor()).unwrap(), + WindowMessage::InnerPosition(tx) => tx + .send( + window + .inner_position() + .map(|p| PhysicalPositionWrapper(p).into()) + .map_err(|_| Error::FailedToSendMessage), + ) + .unwrap(), + WindowMessage::OuterPosition(tx) => tx + .send( + window + .outer_position() + .map(|p| PhysicalPositionWrapper(p).into()) + .map_err(|_| Error::FailedToSendMessage), + ) + .unwrap(), + WindowMessage::InnerSize(tx) => tx + .send(PhysicalSizeWrapper(window.inner_size()).into()) + .unwrap(), + WindowMessage::OuterSize(tx) => tx + .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::IsFocused(tx) => tx.send(window.is_focused()).unwrap(), + WindowMessage::IsDecorated(tx) => tx.send(window.is_decorated()).unwrap(), + WindowMessage::IsResizable(tx) => tx.send(window.is_resizable()).unwrap(), + WindowMessage::IsMaximizable(tx) => tx.send(window.is_maximizable()).unwrap(), + WindowMessage::IsMinimizable(tx) => tx.send(window.is_minimizable()).unwrap(), + WindowMessage::IsClosable(tx) => tx.send(window.is_closable()).unwrap(), + WindowMessage::IsVisible(tx) => tx.send(window.is_visible()).unwrap(), + WindowMessage::Title(tx) => tx.send(window.title()).unwrap(), + WindowMessage::CurrentMonitor(tx) => tx.send(window.current_monitor()).unwrap(), + WindowMessage::PrimaryMonitor(tx) => tx.send(window.primary_monitor()).unwrap(), + WindowMessage::AvailableMonitors(tx) => { + tx.send(window.available_monitors().collect()).unwrap() + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + WindowMessage::GtkWindow(tx) => tx.send(GtkWindow(window.gtk_window().clone())).unwrap(), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + WindowMessage::GtkBox(tx) => tx + .send(GtkBox(window.default_vbox().unwrap().clone())) + .unwrap(), + WindowMessage::RawWindowHandle(tx) => tx + .send(RawWindowHandle(window.raw_window_handle())) + .unwrap(), + WindowMessage::Theme(tx) => { + tx.send(map_theme(&window.theme())).unwrap(); + } + // Setters + WindowMessage::Center => { + let _ = center_window(&window, window.inner_size()); + } + WindowMessage::RequestUserAttention(request_type) => { + window.request_user_attention(request_type.map(|r| r.0)); + } + WindowMessage::SetResizable(resizable) => window.set_resizable(resizable), + WindowMessage::SetMaximizable(maximizable) => window.set_maximizable(maximizable), + WindowMessage::SetMinimizable(minimizable) => window.set_minimizable(minimizable), + WindowMessage::SetClosable(closable) => window.set_closable(closable), + WindowMessage::SetTitle(title) => window.set_title(&title), + WindowMessage::Navigate(url) => { + if let WindowHandle::Webview { inner: w, .. } = &window { + w.load_url(url.as_str()) } - WindowMessage::RequestRedraw => { - window.request_redraw(); + } + WindowMessage::Maximize => window.set_maximized(true), + WindowMessage::Unmaximize => window.set_maximized(false), + WindowMessage::Minimize => window.set_minimized(true), + WindowMessage::Unminimize => window.set_minimized(false), + WindowMessage::Show => window.set_visible(true), + WindowMessage::Hide => window.set_visible(false), + WindowMessage::Close => { + panic!("cannot handle `WindowMessage::Close` on the main thread") + } + WindowMessage::SetDecorations(decorations) => window.set_decorations(decorations), + WindowMessage::SetShadow(_enable) => { + #[cfg(windows)] + window.set_undecorated_shadow(_enable); + #[cfg(target_os = "macos")] + window.set_has_shadow(_enable); + } + WindowMessage::SetAlwaysOnTop(always_on_top) => window.set_always_on_top(always_on_top), + WindowMessage::SetVisibleOnAllWorkspaces(visible_on_all_workspaces) => { + window.set_visible_on_all_workspaces(visible_on_all_workspaces) + } + WindowMessage::SetContentProtected(protected) => window.set_content_protection(protected), + WindowMessage::SetSize(size) => { + window.set_inner_size(SizeWrapper::from(size).0); + } + WindowMessage::SetMinSize(size) => { + window.set_min_inner_size(size.map(|s| SizeWrapper::from(s).0)); + } + WindowMessage::SetMaxSize(size) => { + window.set_max_inner_size(size.map(|s| SizeWrapper::from(s).0)); + } + WindowMessage::SetPosition(position) => { + window.set_outer_position(PositionWrapper::from(position).0) + } + WindowMessage::SetFullscreen(fullscreen) => { + if fullscreen { + window.set_fullscreen(Some(Fullscreen::Borderless(None))) + } else { + window.set_fullscreen(None) } } + WindowMessage::SetFocus => { + window.set_focus(); + } + WindowMessage::SetIcon(icon) => { + window.set_window_icon(Some(icon)); + } + #[allow(unused_variables)] + WindowMessage::SetSkipTaskbar(skip) => { + #[cfg(any(windows, target_os = "linux"))] + window.set_skip_taskbar(skip); + } + WindowMessage::SetCursorGrab(grab) => { + let _ = window.set_cursor_grab(grab); + } + WindowMessage::SetCursorVisible(visible) => { + window.set_cursor_visible(visible); + } + WindowMessage::SetCursorIcon(icon) => { + window.set_cursor_icon(CursorIconWrapper::from(icon).0); + } + WindowMessage::SetCursorPosition(position) => { + let _ = window.set_cursor_position(PositionWrapper::from(position).0); + } + WindowMessage::SetIgnoreCursorEvents(ignore) => { + let _ = window.set_ignore_cursor_events(ignore); + } + WindowMessage::DragWindow => { + let _ = window.drag_window(); + } + WindowMessage::RequestRedraw => { + window.request_redraw(); + } } } } @@ -2627,9 +2362,7 @@ fn handle_user_message( WindowWrapper { label, inner: Some(WindowHandle::Window(w.clone())), - menu_items: Default::default(), window_event_listeners: Default::default(), - menu_event_listeners: Default::default(), }, ); sender.send(Ok(Arc::downgrade(&w))).unwrap(); @@ -2638,97 +2371,6 @@ fn handle_user_message( } } - #[cfg(all(desktop, feature = "system-tray"))] - Message::Tray(tray_id, tray_message) => { - let mut trays = system_tray_manager.trays.lock().unwrap(); - - if let TrayMessage::Create(mut tray, tx) = tray_message { - let mut listeners = Vec::new(); - if let Some(l) = tray.on_event.take() { - listeners.push(Arc::new(l)); - } - match create_tray(WryTrayId(tray_id), tray, event_loop) { - Ok((tray, items)) => { - trays.insert( - tray_id, - TrayContext { - tray: Arc::new(Mutex::new(Some(tray))), - listeners: Arc::new(Mutex::new(listeners)), - items: Arc::new(Mutex::new(items)), - }, - ); - - tx.send(Ok(())).unwrap(); - } - - Err(e) => { - tx.send(Err(e)).unwrap(); - } - } - } else if let Some(tray_context) = trays.get(&tray_id) { - match tray_message { - TrayMessage::UpdateItem(menu_id, update) => { - let mut tray = tray_context.items.as_ref().lock().unwrap(); - let item = tray.get_mut(&menu_id).expect("menu item not found"); - match update { - MenuUpdate::SetEnabled(enabled) => item.set_enabled(enabled), - MenuUpdate::SetTitle(title) => item.set_title(&title), - MenuUpdate::SetSelected(selected) => item.set_selected(selected), - #[cfg(target_os = "macos")] - MenuUpdate::SetNativeImage(image) => { - item.set_native_image(NativeImageWrapper::from(image).0) - } - } - } - TrayMessage::UpdateMenu(menu) => { - if let Some(tray) = &mut *tray_context.tray.lock().unwrap() { - let mut items = HashMap::new(); - tray.set_menu(&to_wry_context_menu(&mut items, menu)); - *tray_context.items.lock().unwrap() = items; - } - } - TrayMessage::UpdateIcon(icon) => { - if let Some(tray) = &mut *tray_context.tray.lock().unwrap() { - if let Ok(icon) = TrayIcon::try_from(icon) { - tray.set_icon(icon.0); - } - } - } - #[cfg(target_os = "macos")] - TrayMessage::UpdateIconAsTemplate(is_template) => { - if let Some(tray) = &mut *tray_context.tray.lock().unwrap() { - tray.set_icon_as_template(is_template); - } - } - #[cfg(target_os = "macos")] - TrayMessage::UpdateTitle(title) => { - if let Some(tray) = &mut *tray_context.tray.lock().unwrap() { - 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 - } - TrayMessage::Destroy(tx) => { - *tray_context.tray.lock().unwrap() = None; - tray_context.listeners.lock().unwrap().clear(); - tray_context.items.lock().unwrap().clear(); - tx.send(Ok(())).unwrap(); - } - } - } - } - #[cfg(all(desktop, feature = "global-shortcut"))] - Message::GlobalShortcut(message) => { - handle_global_shortcut_message(message, &global_shortcut_manager) - } - #[cfg(feature = "clipboard")] - Message::Clipboard(message) => handle_clipboard_message(message, &clipboard_manager), Message::UserEvent(_) => (), } @@ -2749,14 +2391,6 @@ fn handle_event_loop( callback, webview_id_map, windows, - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager, - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager_handle, - #[cfg(feature = "clipboard")] - clipboard_manager, - #[cfg(all(desktop, feature = "system-tray"))] - system_tray_manager, } = context; if *control_flow != ControlFlow::Exit { *control_flow = ControlFlow::Wait; @@ -2779,126 +2413,6 @@ fn handle_event_loop( callback(RunEvent::Exit); } - #[cfg(all(desktop, feature = "global-shortcut"))] - Event::GlobalShortcutEvent(accelerator_id) => { - for (id, handler) in &*global_shortcut_manager_handle.listeners.lock().unwrap() { - if accelerator_id == *id { - handler(); - } - } - } - Event::MenuEvent { - window_id, - menu_id, - origin: MenuType::MenuBar, - .. - } => { - #[allow(unused_mut)] - let mut window_id = window_id.unwrap(); // always Some on MenuBar event - - #[cfg(target_os = "macos")] - { - // safety: we're only checking to see if the window_id is 0 - // which is the value sent by macOS when the window is minimized (NSApplication::sharedApplication::mainWindow is null) - if window_id == unsafe { WindowId::dummy() } { - window_id = *webview_id_map.0.lock().unwrap().keys().next().unwrap(); - } - } - - let event = MenuEvent { - menu_item_id: menu_id.0, - }; - let window_menu_event_listeners = { - // on macOS the window id might be the inspector window if it is detached - let window_id = if let Some(window_id) = webview_id_map.get(&window_id) { - window_id - } else { - *webview_id_map.0.lock().unwrap().values().next().unwrap() - }; - windows - .borrow() - .get(&window_id) - .unwrap() - .menu_event_listeners - .clone() - }; - let listeners = window_menu_event_listeners.lock().unwrap(); - let handlers = listeners.values(); - for handler in handlers { - handler(&event); - } - } - #[cfg(all(desktop, feature = "system-tray"))] - Event::MenuEvent { - window_id: _, - menu_id, - origin: MenuType::ContextMenu, - .. - } => { - let event = SystemTrayEvent::MenuItemClick(menu_id.0); - - let trays = system_tray_manager.trays.lock().unwrap(); - let trays_iter = trays.iter(); - - let (mut listeners, mut tray_id) = (None, 0); - for (id, tray_context) in trays_iter { - let has_menu = { - let items = tray_context.items.lock().unwrap(); - items.contains_key(&menu_id.0) - }; - if has_menu { - listeners.replace(tray_context.listeners.lock().unwrap().clone()); - tray_id = *id; - break; - } - } - drop(trays); - if let Some(listeners) = listeners { - let handlers = listeners.iter(); - for handler in handlers { - handler(&event); - } - - let global_listeners = system_tray_manager.global_listeners.lock().unwrap(); - let global_listeners_iter = global_listeners.iter(); - for global_listener in global_listeners_iter { - global_listener(tray_id, &event); - } - } - } - #[cfg(all(desktop, feature = "system-tray"))] - Event::TrayEvent { - id, - bounds, - event, - position: _cursor_position, - .. - } => { - let (position, size) = ( - PhysicalPositionWrapper(bounds.position).into(), - PhysicalSizeWrapper(bounds.size).into(), - ); - let event = match event { - TrayEvent::RightClick => SystemTrayEvent::RightClick { position, size }, - TrayEvent::DoubleClick => SystemTrayEvent::DoubleClick { position, size }, - // default to left click - _ => SystemTrayEvent::LeftClick { position, size }, - }; - let trays = system_tray_manager.trays.lock().unwrap(); - if let Some(tray_context) = trays.get(&id.0) { - let listeners = tray_context.listeners.lock().unwrap(); - let iter = listeners.iter(); - for handler in iter { - handler(&event); - } - } - - let global_listeners = system_tray_manager.global_listeners.lock().unwrap(); - let global_listeners_iter = global_listeners.iter(); - for global_listener in global_listeners_iter { - global_listener(id.0, &event); - } - } Event::UserEvent(Message::Webview(id, WebviewMessage::WebviewEvent(event))) => { if let Some(event) = WindowEventWrapper::from(&event).0 { let windows = windows.borrow(); @@ -2917,6 +2431,7 @@ fn handle_event_loop( } } } + Event::WindowEvent { event, window_id, .. } => { @@ -2992,18 +2507,16 @@ fn handle_event_loop( message, UserMessageContext { webview_id_map, - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager, - #[cfg(feature = "clipboard")] - clipboard_manager, windows, - #[cfg(all(desktop, feature = "system-tray"))] - system_tray_manager, }, web_context, ); } }, + #[cfg(any(target_os = "macos", target_os = "ios"))] + Event::Opened { urls } => { + callback(RunEvent::Opened { urls }); + } _ => (), } @@ -3045,7 +2558,7 @@ fn on_close_requested<'a, T: UserEvent>( } fn on_window_close(window_id: WebviewId, windows: Arc>>) { - if let Some(mut window_wrapper) = windows.borrow_mut().get_mut(&window_id) { + if let Some(window_wrapper) = windows.borrow_mut().get_mut(&window_id) { window_wrapper.inner = None; } } @@ -3066,61 +2579,26 @@ pub fn center_window(window: &Window, window_size: WryPhysicalSize) -> Resu } } -fn to_wry_menu( - custom_menu_items: &mut HashMap, - menu: Menu, -) -> MenuBar { - let mut wry_menu = MenuBar::new(); - for item in menu.items { - match item { - MenuEntry::CustomItem(c) => { - let mut attributes = MenuItemAttributesWrapper::from(&c).0; - attributes = attributes.with_id(WryMenuId(c.id)); - #[allow(unused_mut)] - let mut item = wry_menu.add_item(attributes); - #[cfg(target_os = "macos")] - if let Some(native_image) = c.native_image { - item.set_native_image(NativeImageWrapper::from(native_image).0); - } - custom_menu_items.insert(c.id, item); - } - MenuEntry::NativeItem(i) => { - wry_menu.add_native_item(MenuItemWrapper::from(i).0); - } - MenuEntry::Submenu(submenu) => { - wry_menu.add_submenu( - &submenu.title, - submenu.enabled, - to_wry_menu(custom_menu_items, submenu.inner), - ); - } - } - } - wry_menu -} - -fn create_webview( +fn create_webview( window_id: WebviewId, event_loop: &EventLoopWindowTarget>, web_context_store: &WebContextStore, context: Context, pending: PendingWindow>, + before_webview_creation: Option, ) -> Result { #[allow(unused_mut)] let PendingWindow { webview_attributes, uri_scheme_protocols, mut window_builder, - ipc_handler, label, + ipc_handler, url, - menu_ids, - js_event_listeners, + #[cfg(target_os = "android")] + on_webview_created, .. } = pending; - let webview_id_map = context.webview_id_map.clone(); - #[cfg(windows)] - let proxy = context.proxy.clone(); let window_event_listeners = WindowEventListeners::default(); @@ -3133,6 +2611,8 @@ fn create_webview( #[cfg(windows)] let window_theme = window_builder.inner.window.preferred_theme; + #[cfg(windows)] + let proxy = context.proxy.clone(); #[cfg(target_os = "macos")] { @@ -3145,21 +2625,39 @@ fn create_webview( } let is_window_transparent = window_builder.inner.window.transparent; - let menu_items = if let Some(menu) = window_builder.menu { - let mut menu_items = HashMap::new(); - let menu = to_wry_menu(&mut menu_items, menu); - window_builder.inner = window_builder.inner.with_menu(menu); - Some(menu_items) - } else { - None - }; let window = window_builder.inner.build(event_loop).unwrap(); - webview_id_map.insert(window.id(), window_id); + context.webview_id_map.insert(window.id(), window_id); if window_builder.center { let _ = center_window(&window, window.inner_size()); } + + if let Some(handler) = before_webview_creation { + let raw = RawWindow { + #[cfg(windows)] + hwnd: window.hwnd(), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + gtk_window: window.gtk_window(), + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + default_vbox: window.default_vbox(), + _marker: &std::marker::PhantomData, + }; + handler(raw); + } + let mut webview_builder = WebViewBuilder::new(window) .map_err(|e| Error::CreateWebview(Box::new(e)))? .with_url(&url) @@ -3172,7 +2670,9 @@ fn create_webview( } 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) + Url::parse(&url) + .map(|url| navigation_handler(&url)) + .unwrap_or(true) }); } if let Some(user_agent) = webview_attributes.user_agent { @@ -3180,28 +2680,25 @@ fn create_webview( } #[cfg(windows)] - if let Some(additional_browser_args) = webview_attributes.additional_browser_args { - webview_builder = webview_builder.with_additional_browser_args(&additional_browser_args); - } + { + 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(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, - label.clone(), - menu_ids, - js_event_listeners, - handler, - )); + webview_builder = + webview_builder.with_ipc_handler(create_ipc_handler(context, label.clone(), handler)); } + for (scheme, protocol) in uri_scheme_protocols { webview_builder = webview_builder.with_custom_protocol(scheme, move |wry_request| { protocol(&HttpRequestWrapper::from(wry_request).0) @@ -3243,11 +2740,28 @@ fn create_webview( webview_builder.webview.clipboard = true; } + if webview_attributes.incognito { + webview_builder.webview.incognito = true; + } + #[cfg(any(debug_assertions, feature = "devtools"))] { webview_builder = webview_builder.with_devtools(true); } + #[cfg(target_os = "android")] + { + if let Some(on_webview_created) = on_webview_created { + webview_builder = webview_builder.on_webview_created(move |ctx| { + on_webview_created(tauri_runtime::window::CreationContext { + env: ctx.env, + activity: ctx.activity, + webview: ctx.webview, + }) + }); + } + } + let webview = webview_builder .with_web_context(web_context) .build() @@ -3261,7 +2775,7 @@ fn create_webview( unsafe { controller.add_GotFocus( &FocusChangedEventHandler::create(Box::new(move |_, _| { - let _ = proxy_.send_event(Message::Webview( + let _ = proxy.send_event(Message::Webview( window_id, WebviewMessage::WebviewEvent(WebviewEvent::Focused(true)), )); @@ -3274,7 +2788,7 @@ fn create_webview( unsafe { controller.add_LostFocus( &FocusChangedEventHandler::create(Box::new(move |_, _| { - let _ = proxy.send_event(Message::Webview( + let _ = proxy_.send_event(Message::Webview( window_id, WebviewMessage::WebviewEvent(WebviewEvent::Focused(false)), )); @@ -3297,9 +2811,7 @@ fn create_webview( web_context_key }, }), - menu_items, window_event_listeners, - menu_event_listeners: Default::default(), }) } @@ -3307,8 +2819,6 @@ fn create_webview( fn create_ipc_handler( context: Context, label: String, - menu_ids: Arc>>, - js_event_listeners: Arc>>>, handler: WebviewIpcHandler>, ) -> Box { Box::new(move |window, request| { @@ -3320,8 +2830,6 @@ fn create_ipc_handler( context: context.clone(), }, label: label.clone(), - menu_ids: menu_ids.clone(), - js_event_listeners: js_event_listeners.clone(), }, request, ); diff --git a/core/tauri-runtime-wry/src/system_tray.rs b/core/tauri-runtime-wry/src/system_tray.rs deleted file mode 100644 index 594e13cc490..00000000000 --- a/core/tauri-runtime-wry/src/system_tray.rs +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -pub use tauri_runtime::{ - menu::{ - Menu, MenuEntry, MenuItem, MenuUpdate, Submenu, SystemTrayMenu, SystemTrayMenuEntry, - SystemTrayMenuItem, TrayHandle, - }, - Icon, SystemTrayEvent, -}; -use wry::application::event_loop::EventLoopWindowTarget; -pub use wry::application::{ - event::TrayEvent, - event_loop::EventLoopProxy, - menu::{ - ContextMenu as WryContextMenu, CustomMenuItem as WryCustomMenuItem, MenuItem as WryMenuItem, - }, - system_tray::Icon as WryTrayIcon, - TrayId as WryTrayId, -}; - -#[cfg(target_os = "macos")] -pub use wry::application::platform::macos::{ - CustomMenuItemExtMacOS, SystemTrayBuilderExtMacOS, SystemTrayExtMacOS, -}; - -use wry::application::system_tray::{SystemTray as WrySystemTray, SystemTrayBuilder}; - -use crate::{send_user_message, Context, Error, Message, Result, TrayId, TrayMessage}; - -use tauri_runtime::{menu::MenuHash, SystemTray, UserEvent}; - -use std::{ - collections::HashMap, - fmt, - sync::{Arc, Mutex}, -}; - -pub type GlobalSystemTrayEventHandler = Box; -pub type GlobalSystemTrayEventListeners = Arc>>>; - -pub type SystemTrayEventHandler = Box; -pub type SystemTrayEventListeners = Arc>>>; -pub type SystemTrayItems = Arc>>; - -#[derive(Clone, Default)] -pub struct TrayContext { - pub tray: Arc>>, - pub listeners: SystemTrayEventListeners, - pub items: SystemTrayItems, -} - -impl fmt::Debug for TrayContext { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("TrayContext") - .field("items", &self.items) - .finish() - } -} - -#[derive(Clone, Default)] -pub struct SystemTrayManager { - pub trays: Arc>>, - pub global_listeners: GlobalSystemTrayEventListeners, -} - -impl fmt::Debug for SystemTrayManager { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SystemTrayManager") - .field("trays", &self.trays) - .finish() - } -} - -/// Wrapper around a [`wry::application::system_tray::Icon`] that can be created from an [`WindowIcon`]. -pub struct TrayIcon(pub(crate) WryTrayIcon); - -impl TryFrom for TrayIcon { - type Error = Error; - fn try_from(icon: Icon) -> std::result::Result { - WryTrayIcon::from_rgba(icon.rgba, icon.width, icon.height) - .map(Self) - .map_err(crate::icon_err) - } -} - -pub fn create_tray( - id: WryTrayId, - system_tray: SystemTray, - event_loop: &EventLoopWindowTarget, -) -> crate::Result<(WrySystemTray, HashMap)> { - let icon = TrayIcon::try_from(system_tray.icon.expect("tray icon not set"))?; - - let mut items = HashMap::new(); - - #[allow(unused_mut)] - let mut builder = SystemTrayBuilder::new( - icon.0, - system_tray - .menu - .map(|menu| to_wry_context_menu(&mut items, menu)), - ) - .with_id(id); - - #[cfg(target_os = "macos")] - { - builder = builder - .with_icon_as_template(system_tray.icon_as_template) - .with_menu_on_left_click(system_tray.menu_on_left_click); - - if let Some(title) = system_tray.title { - builder = builder.with_title(&title); - } - } - - 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)))?; - - Ok((tray, items)) -} - -#[derive(Debug, Clone)] -pub struct SystemTrayHandle { - pub(crate) context: Context, - pub(crate) id: TrayId, - pub(crate) proxy: EventLoopProxy>, -} - -impl TrayHandle for SystemTrayHandle { - fn set_icon(&self, icon: Icon) -> Result<()> { - self - .proxy - .send_event(Message::Tray(self.id, TrayMessage::UpdateIcon(icon))) - .map_err(|_| Error::FailedToSendMessage) - } - - fn set_menu(&self, menu: SystemTrayMenu) -> Result<()> { - self - .proxy - .send_event(Message::Tray(self.id, TrayMessage::UpdateMenu(menu))) - .map_err(|_| Error::FailedToSendMessage) - } - - fn update_item(&self, id: u16, update: MenuUpdate) -> Result<()> { - self - .proxy - .send_event(Message::Tray(self.id, TrayMessage::UpdateItem(id, update))) - .map_err(|_| Error::FailedToSendMessage) - } - - #[cfg(target_os = "macos")] - fn set_icon_as_template(&self, is_template: bool) -> tauri_runtime::Result<()> { - self - .proxy - .send_event(Message::Tray( - self.id, - TrayMessage::UpdateIconAsTemplate(is_template), - )) - .map_err(|_| Error::FailedToSendMessage) - } - - #[cfg(target_os = "macos")] - fn set_title(&self, title: &str) -> tauri_runtime::Result<()> { - self - .proxy - .send_event(Message::Tray( - self.id, - TrayMessage::UpdateTitle(title.to_owned()), - )) - .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( - &self.context, - Message::Tray(self.id, TrayMessage::Destroy(tx)), - )?; - rx.recv().unwrap()?; - Ok(()) - } -} - -impl From for crate::MenuItemWrapper { - fn from(item: SystemTrayMenuItem) -> Self { - match item { - SystemTrayMenuItem::Separator => Self(WryMenuItem::Separator), - _ => unimplemented!(), - } - } -} - -pub fn to_wry_context_menu( - custom_menu_items: &mut HashMap, - menu: SystemTrayMenu, -) -> WryContextMenu { - let mut tray_menu = WryContextMenu::new(); - for item in menu.items { - match item { - SystemTrayMenuEntry::CustomItem(c) => { - #[allow(unused_mut)] - let mut item = tray_menu.add_item(crate::MenuItemAttributesWrapper::from(&c).0); - #[cfg(target_os = "macos")] - if let Some(native_image) = c.native_image { - item.set_native_image(crate::NativeImageWrapper::from(native_image).0); - } - custom_menu_items.insert(c.id, item); - } - SystemTrayMenuEntry::NativeItem(i) => { - tray_menu.add_native_item(crate::MenuItemWrapper::from(i).0); - } - SystemTrayMenuEntry::Submenu(submenu) => { - tray_menu.add_submenu( - &submenu.title, - submenu.enabled, - to_wry_context_menu(custom_menu_items, submenu.inner), - ); - } - } - } - tray_menu -} diff --git a/core/tauri-runtime-wry/src/webview.rs b/core/tauri-runtime-wry/src/webview.rs index 834b6a4d19e..283c32fb5c3 100644 --- a/core/tauri-runtime-wry/src/webview.rs +++ b/core/tauri-runtime-wry/src/webview.rs @@ -26,6 +26,17 @@ mod imp { } } +#[cfg(target_os = "ios")] +mod imp { + use cocoa::base::id; + + pub struct Webview { + pub webview: id, + pub manager: id, + pub view_controller: id, + } +} + #[cfg(windows)] mod imp { use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller; @@ -34,4 +45,10 @@ mod imp { } } +#[cfg(target_os = "android")] +mod imp { + use wry::webview::JniHandle; + pub type Webview = JniHandle; +} + pub use imp::*; diff --git a/core/tauri-runtime/CHANGELOG.md b/core/tauri-runtime/CHANGELOG.md index 2a411df2be5..182aacc9145 100644 --- a/core/tauri-runtime/CHANGELOG.md +++ b/core/tauri-runtime/CHANGELOG.md @@ -1,5 +1,75 @@ # Changelog +## \[1.0.0-alpha.0] + +### New Features + +- [`4db363a0`](https://www.github.com/tauri-apps/tauri/commit/4db363a03c182349f8491f46ced258d84723b11f)([#6589](https://www.github.com/tauri-apps/tauri/pull/6589)) Added `visible_on_all_workspaces` configuration option to `WindowBuilder`, `Window`, and `WindowConfig`. +- [`84c41597`](https://www.github.com/tauri-apps/tauri/commit/84c4159754b2e59244211ed9e1fc702d851a0562)([#6394](https://www.github.com/tauri-apps/tauri/pull/6394)) Added `primary_monitor` and `available_monitors` to `Runtime` and `RuntimeHandle`. +- [`2a000e15`](https://www.github.com/tauri-apps/tauri/commit/2a000e150d02dff28c8b20ad097b29e209160045)([#7235](https://www.github.com/tauri-apps/tauri/pull/7235)) Added `navigate` function to `Dispatch` trait. +- [`3b98141a`](https://www.github.com/tauri-apps/tauri/commit/3b98141aa26f74c641a4090874247b97079bd58a)([#3736](https://www.github.com/tauri-apps/tauri/pull/3736)) Added the `Opened` variant to `RunEvent`. + +### Dependencies + +- Upgraded to `tauri-utils@2.0.0-alpha.7` + +### Breaking Changes + +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) `Dispatch::create_window`, `Runtime::create_window` and `RuntimeHandle::create_window` has been changed to accept a 3rd parameter which is a closure that takes `RawWindow` and to be executed right after the window is created and before the webview is added to the window. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) System tray and menu related APIs and structs have all been removed and are now implemented in tauri outside of the runtime-space. +- [`3a2c3e74`](https://www.github.com/tauri-apps/tauri/commit/3a2c3e74710bef9a14932dce74c351cca6215429)([#7306](https://www.github.com/tauri-apps/tauri/pull/7306)) The `PendingWindow#navigation_handler` closure now receives a `&Url` argument instead of `Url`. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) `Runtime::new` and `Runtime::new_any_thread` now accept a `RuntimeInitArgs`. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) Removed `system-tray` feature flag + +## \[0.13.0-alpha.6] + +### New Features + +- [`e0f0dce2`](https://www.github.com/tauri-apps/tauri/commit/e0f0dce220730e2822fc202463aedf0166145de7)([#6442](https://www.github.com/tauri-apps/tauri/pull/6442)) Added the `window_effects` option when creating a window and `Window::set_effects` to change it at runtime. + +## \[0.13.0-alpha.5] + +- [`39f1b04f`](https://www.github.com/tauri-apps/tauri/commit/39f1b04f7be4966488484829cd54c8ce72a04200)([#6943](https://www.github.com/tauri-apps/tauri/pull/6943)) Moved the `event` JS APIs to a plugin. +- [`3188f376`](https://www.github.com/tauri-apps/tauri/commit/3188f3764978c6d1452ee31d5a91469691e95094)([#6883](https://www.github.com/tauri-apps/tauri/pull/6883)) Bump the MSRV to 1.65. +- [`cebd7526`](https://www.github.com/tauri-apps/tauri/commit/cebd75261ac71b98976314a450cb292eeeec1515)([#6728](https://www.github.com/tauri-apps/tauri/pull/6728)) Moved the `clipboard` feature to its own plugin in the plugins-workspace repository. +- [`3f17ee82`](https://www.github.com/tauri-apps/tauri/commit/3f17ee82f6ff21108806edb7b00500b8512b8dc7)([#6737](https://www.github.com/tauri-apps/tauri/pull/6737)) Moved the `global-shortcut` feature to its own plugin in the plugins-workspace repository. + +## \[0.13.0-alpha.4] + +- Added `android` configuration object under `tauri > bundle`. + - Bumped due to a bump in tauri-utils. + - [db4c9dc6](https://www.github.com/tauri-apps/tauri/commit/db4c9dc655e07ee2184fe04571f500f7910890cd) feat(core): add option to configure Android's minimum SDK version ([#6651](https://www.github.com/tauri-apps/tauri/pull/6651)) on 2023-04-07 + +## \[0.13.0-alpha.3] + +- Pull changes from Tauri 1.3 release. + - [](https://www.github.com/tauri-apps/tauri/commit/undefined) on undefined + +## \[0.13.0-alpha.2] + +- Add `find_class`, `run_on_android_context` on `RuntimeHandle`. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 +- Added the `shadow` option when creating a window and `Window::set_shadow`. + - [a81750d7](https://www.github.com/tauri-apps/tauri/commit/a81750d779bc72f0fdb7de90b7fbddfd8049b328) feat(core): add shadow APIs ([#6206](https://www.github.com/tauri-apps/tauri/pull/6206)) on 2023-02-08 +- Implemented `with_webview` on Android and iOS. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 + +## \[0.13.0-alpha.1] + +- Update gtk to 0.16. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Bump the MSRV to 1.64. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 + +## \[0.13.0-alpha.0] + +- Parse `android` and `ios` Tauri configuration files. + - Bumped due to a bump in tauri-utils. + - [b3a3afc7](https://www.github.com/tauri-apps/tauri/commit/b3a3afc7de8de4021d73559288f5192732a706cf) feat(core): detect android and ios platform configuration files ([#4997](https://www.github.com/tauri-apps/tauri/pull/4997)) on 2022-08-22 +- First mobile alpha release! + - Bumped due to a bump in tauri-utils. + - [fa3a1098](https://www.github.com/tauri-apps/tauri/commit/fa3a10988a03aed1b66fb17d893b1a9adb90f7cd) feat(ci): prepare 2.0.0-alpha.0 ([#5786](https://www.github.com/tauri-apps/tauri/pull/5786)) on 2022-12-08 + ## \[0.14.0] ### New Features diff --git a/core/tauri-runtime/Cargo.toml b/core/tauri-runtime/Cargo.toml index 1c255a94ae7..0185ca771ca 100644 --- a/core/tauri-runtime/Cargo.toml +++ b/core/tauri-runtime/Cargo.toml @@ -1,16 +1,16 @@ [package] name = "tauri-runtime" -version = "0.14.0" -authors = [ "Tauri Programme within The Commons Conservancy" ] -categories = [ "gui", "web-programming" ] -license = "Apache-2.0 OR MIT" -homepage = "https://tauri.app" -repository = "https://github.com/tauri-apps/tauri" +version = "1.0.0-alpha.0" description = "Runtime for Tauri applications" -edition = "2021" -rust-version = "1.60" exclude = [ "CHANGELOG.md", "/target" ] readme = "README.md" +authors = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +categories = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } [package.metadata.docs.rs] all-features = true @@ -26,7 +26,7 @@ targets = [ serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" thiserror = "1.0" -tauri-utils = { version = "1.4.0", path = "../tauri-utils" } +tauri-utils = { version = "2.0.0-alpha.7", path = "../tauri-utils" } uuid = { version = "1", features = [ "v4" ] } http = "0.2.4" http-range = "0.1.4" @@ -34,19 +34,19 @@ raw-window-handle = "0.5" rand = "0.8" url = { version = "2" } -[target."cfg(windows)".dependencies] -webview2-com = "0.19.1" - - [target."cfg(windows)".dependencies.windows] - version = "0.39.0" - features = [ "Win32_Foundation" ] +[target."cfg(windows)".dependencies.windows] +version = "0.48" +features = [ "Win32_Foundation" ] [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] -gtk = { version = "0.15", features = [ "v3_20" ] } +gtk = { version = "0.16", features = [ "v3_24" ] } + +[target."cfg(target_os = \"android\")".dependencies] +jni = "0.21" + +[target."cfg(target_os = \"macos\")".dependencies] +url = "2" [features] devtools = [ ] -system-tray = [ ] macos-private-api = [ ] -global-shortcut = [ ] -clipboard = [ ] diff --git a/core/tauri-runtime/src/http/response.rs b/core/tauri-runtime/src/http/response.rs index 6c427a0ffed..a004d4dc0d5 100644 --- a/core/tauri-runtime/src/http/response.rs +++ b/core/tauri-runtime/src/http/response.rs @@ -7,7 +7,7 @@ use super::{ status::StatusCode, version::Version, }; -use std::fmt; +use std::{borrow::Cow, fmt}; type Result = core::result::Result>; @@ -33,7 +33,7 @@ type Result = core::result::Result>; /// pub struct Response { head: ResponseParts, - body: Vec, + body: Cow<'static, [u8]>, } /// Component parts of an HTTP `Response` @@ -67,7 +67,7 @@ pub struct Builder { impl Response { /// Creates a new blank `Response` with the body #[inline] - pub fn new(body: Vec) -> Response { + pub fn new(body: Cow<'static, [u8]>) -> Response { Response { head: ResponseParts::new(), body, @@ -81,7 +81,7 @@ impl Response { /// This API is used internally. It may have breaking changes in the future. #[inline] #[doc(hidden)] - pub fn into_parts(self) -> (ResponseParts, Vec) { + pub fn into_parts(self) -> (ResponseParts, Cow<'static, [u8]>) { (self.head, self.body) } @@ -129,13 +129,13 @@ impl Response { /// Returns a mutable reference to the associated HTTP body. #[inline] - pub fn body_mut(&mut self) -> &mut Vec { + pub fn body_mut(&mut self) -> &mut Cow<'static, [u8]> { &mut self.body } /// Returns a reference to the associated HTTP body. #[inline] - pub fn body(&self) -> &Vec { + pub fn body(&self) -> &Cow<'static, [u8]> { &self.body } } @@ -143,7 +143,7 @@ impl Response { impl Default for Response { #[inline] fn default() -> Response { - Response::new(Vec::new()) + Response::new(Default::default()) } } @@ -280,8 +280,11 @@ impl Builder { /// .body(Vec::new()) /// .unwrap(); /// ``` - pub fn body(self, body: Vec) -> Result { - self.inner.map(move |head| Response { head, body }) + pub fn body(self, body: impl Into>) -> Result { + self.inner.map(move |head| Response { + head, + body: body.into(), + }) } // private diff --git a/core/tauri-runtime/src/lib.rs b/core/tauri-runtime/src/lib.rs index 0671fa25378..25c743b8ae3 100644 --- a/core/tauri-runtime/src/lib.rs +++ b/core/tauri-runtime/src/lib.rs @@ -2,8 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! [![](https://github.com/tauri-apps/tauri/raw/dev/.github/splash.png)](https://tauri.app) +//! //! Internal runtime between Tauri and the underlying webview runtime. +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] #![cfg_attr(doc_cfg, feature(doc_cfg))] use raw_window_handle::RawDisplayHandle; @@ -14,8 +20,6 @@ use url::Url; use uuid::Uuid; pub mod http; -/// Create window and system tray menus. -pub mod menu; /// Types useful for interacting with a user's monitors. pub mod monitor; pub mod webview; @@ -25,7 +29,7 @@ use monitor::Monitor; use webview::WindowBuilder; use window::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, - CursorIcon, DetachedWindow, PendingWindow, WindowEvent, + CursorIcon, DetachedWindow, PendingWindow, RawWindow, WindowEvent, }; use crate::http::{ @@ -35,156 +39,6 @@ use crate::http::{ InvalidUri, }; -#[cfg(all(desktop, feature = "system-tray"))] -use std::fmt; - -pub type TrayId = u16; -pub type TrayEventHandler = dyn Fn(&SystemTrayEvent) + Send + 'static; - -#[cfg(all(desktop, feature = "system-tray"))] -#[non_exhaustive] -pub struct SystemTray { - pub id: TrayId, - pub icon: Option, - pub menu: Option, - #[cfg(target_os = "macos")] - pub icon_as_template: bool, - #[cfg(target_os = "macos")] - pub menu_on_left_click: bool, - #[cfg(target_os = "macos")] - pub title: Option, - pub on_event: Option>, - pub tooltip: Option, -} - -#[cfg(all(desktop, feature = "system-tray"))] -impl fmt::Debug for SystemTray { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut d = f.debug_struct("SystemTray"); - d.field("id", &self.id) - .field("icon", &self.icon) - .field("menu", &self.menu); - #[cfg(target_os = "macos")] - { - d.field("icon_as_template", &self.icon_as_template) - .field("menu_on_left_click", &self.menu_on_left_click) - .field("title", &self.title); - } - d.finish() - } -} - -#[cfg(all(desktop, feature = "system-tray"))] -impl Clone for SystemTray { - fn clone(&self) -> Self { - Self { - id: self.id, - icon: self.icon.clone(), - menu: self.menu.clone(), - on_event: None, - #[cfg(target_os = "macos")] - icon_as_template: self.icon_as_template, - #[cfg(target_os = "macos")] - menu_on_left_click: self.menu_on_left_click, - #[cfg(target_os = "macos")] - title: self.title.clone(), - tooltip: self.tooltip.clone(), - } - } -} - -#[cfg(all(desktop, feature = "system-tray"))] -impl Default for SystemTray { - fn default() -> Self { - Self { - id: rand::random(), - icon: None, - menu: None, - #[cfg(target_os = "macos")] - icon_as_template: false, - #[cfg(target_os = "macos")] - menu_on_left_click: false, - #[cfg(target_os = "macos")] - title: None, - on_event: None, - tooltip: None, - } - } -} - -#[cfg(all(desktop, feature = "system-tray"))] -impl SystemTray { - /// Creates a new system tray that only renders an icon. - pub fn new() -> Self { - Default::default() - } - - pub fn menu(&self) -> Option<&menu::SystemTrayMenu> { - self.menu.as_ref() - } - - /// Sets the tray id. - #[must_use] - pub fn with_id(mut self, id: TrayId) -> Self { - self.id = id; - self - } - - /// Sets the tray icon. - #[must_use] - pub fn with_icon(mut self, icon: Icon) -> Self { - self.icon.replace(icon); - self - } - - /// Sets the tray icon as template. - #[cfg(target_os = "macos")] - #[must_use] - pub fn with_icon_as_template(mut self, is_template: bool) -> Self { - self.icon_as_template = is_template; - self - } - - /// Sets whether the menu should appear when the tray receives a left click. Defaults to `true`. - #[cfg(target_os = "macos")] - #[must_use] - pub fn with_menu_on_left_click(mut self, menu_on_left_click: bool) -> Self { - self.menu_on_left_click = menu_on_left_click; - self - } - - #[cfg(target_os = "macos")] - #[must_use] - pub fn with_title(mut self, title: &str) -> Self { - self.title = Some(title.to_owned()); - 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 { - self.menu.replace(menu); - self - } - - #[must_use] - pub fn on_event(mut self, f: F) -> Self { - self.on_event.replace(Box::new(f)); - self - } -} - /// Type of user attention requested on a window. #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(tag = "type")] @@ -237,21 +91,12 @@ pub enum Error { /// Failed to serialize/deserialize. #[error("JSON error: {0}")] Json(#[from] serde_json::Error), - /// Encountered an error creating the app system tray. - #[cfg(all(desktop, feature = "system-tray"))] - #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] - #[error("error encountered during tray setup: {0}")] - SystemTray(Box), /// Failed to load window icon. #[error("invalid icon: {0}")] InvalidIcon(Box), /// Failed to get monitor on window operation. #[error("failed to get monitor")] FailedToGetMonitor, - /// Global shortcut error. - #[cfg(all(desktop, feature = "global-shortcut"))] - #[error(transparent)] - GlobalShortcut(Box), #[error("Invalid header name: {0}")] InvalidHeaderName(#[from] InvalidHeaderName), #[error("Invalid header value: {0}")] @@ -288,6 +133,7 @@ pub trait UserEvent: Debug + Clone + Send + 'static {} impl UserEvent for T {} /// Event triggered on the event loop run. +#[derive(Debug)] #[non_exhaustive] pub enum RunEvent { /// Event loop is exiting. @@ -311,6 +157,9 @@ pub enum RunEvent { /// /// This event is useful as a place to put your code that should be run after all state-changing events have been handled and you want to do stuff (updating state, performing calculations, etc) that happens as the “main body” of your event loop. MainEventsCleared, + /// Emitted when the user wants to open the specified resource with the app. + #[cfg(any(target_os = "macos", target_os = "ios"))] + Opened { urls: Vec }, /// A custom event defined by the user. UserEvent(T), } @@ -322,24 +171,6 @@ pub enum ExitRequestedEventAction { Prevent, } -/// A system tray event. -#[derive(Debug)] -pub enum SystemTrayEvent { - MenuItemClick(u16), - LeftClick { - position: PhysicalPosition, - size: PhysicalSize, - }, - RightClick { - position: PhysicalPosition, - size: PhysicalSize, - }, - DoubleClick { - position: PhysicalPosition, - size: PhysicalSize, - }, -} - /// Metadata for a runtime event loop iteration on `run_iteration`. #[derive(Debug, Clone, Default)] pub struct RunIteration { @@ -367,24 +198,20 @@ pub trait RuntimeHandle: Debug + Clone + Send + Sync + Sized + 'st fn create_proxy(&self) -> >::EventLoopProxy; /// Create a new webview window. - fn create_window( + fn create_window( &self, pending: PendingWindow, + before_webview_creation: Option, ) -> Result>; /// Run a task on the main thread. fn run_on_main_thread(&self, f: F) -> Result<()>; - /// Adds an icon to the system tray with the specified menu items. - #[cfg(all(desktop, feature = "system-tray"))] - #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "system-tray"))))] - fn system_tray( - &self, - system_tray: SystemTray, - ) -> Result<>::TrayHandler>; - fn raw_display_handle(&self) -> RawDisplayHandle; + fn primary_monitor(&self) -> Option; + fn available_monitors(&self) -> Vec; + /// Shows the application, but does not automatically focus it. #[cfg(target_os = "macos")] #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))] @@ -394,62 +221,51 @@ pub trait RuntimeHandle: Debug + Clone + Send + Sync + Sized + 'st #[cfg(target_os = "macos")] #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))] fn hide(&self) -> Result<()>; -} - -/// A global shortcut manager. -#[cfg(all(desktop, feature = "global-shortcut"))] -pub trait GlobalShortcutManager: Debug + Clone + Send + Sync { - /// Whether the application has registered the given `accelerator`. - fn is_registered(&self, accelerator: &str) -> Result; - - /// Register a global shortcut of `accelerator`. - fn register(&mut self, accelerator: &str, handler: F) -> Result<()>; - - /// Unregister all accelerators registered by the manager instance. - fn unregister_all(&mut self) -> Result<()>; - /// Unregister the provided `accelerator`. - fn unregister(&mut self, accelerator: &str) -> Result<()>; -} + /// Finds an Android class in the project scope. + #[cfg(target_os = "android")] + fn find_class<'a>( + &self, + env: &mut jni::JNIEnv<'a>, + activity: &jni::objects::JObject<'_>, + name: impl Into, + ) -> std::result::Result, jni::errors::Error>; -/// Clipboard manager. -#[cfg(feature = "clipboard")] -pub trait ClipboardManager: Debug + Clone + Send + Sync { - /// Writes the text into the clipboard as plain text. - fn write_text>(&mut self, text: T) -> Result<()>; - /// Read the content in the clipboard as plain text. - fn read_text(&self) -> Result>; + /// Dispatch a closure to run on the Android context. + /// + /// The closure takes the JNI env, the Android activity instance and the possibly null webview. + #[cfg(target_os = "android")] + fn run_on_android_context(&self, f: F) + where + F: FnOnce(&mut jni::JNIEnv, &jni::objects::JObject, &jni::objects::JObject) + Send + 'static; } pub trait EventLoopProxy: Debug + Clone + Send + Sync { fn send_event(&self, event: T) -> Result<()>; } +#[derive(Default)] +pub struct RuntimeInitArgs { + #[cfg(windows)] + pub msg_hook: Option bool + 'static>>, +} + /// The webview runtime interface. pub trait Runtime: Debug + Sized + 'static { /// The message dispatcher. type Dispatcher: Dispatch; /// The runtime handle type. type Handle: RuntimeHandle; - /// The global shortcut manager type. - #[cfg(all(desktop, feature = "global-shortcut"))] - type GlobalShortcutManager: GlobalShortcutManager; - /// The clipboard manager type. - #[cfg(feature = "clipboard")] - type ClipboardManager: ClipboardManager; - /// The tray handler type. - #[cfg(all(desktop, feature = "system-tray"))] - type TrayHandler: menu::TrayHandle; /// The proxy type. type EventLoopProxy: EventLoopProxy; /// Creates a new webview runtime. Must be used on the main thread. - fn new() -> Result; + fn new(args: RuntimeInitArgs) -> Result; /// Creates a new webview runtime on any thread. #[cfg(any(windows, target_os = "linux"))] #[cfg_attr(doc_cfg, doc(cfg(any(windows, target_os = "linux"))))] - fn new_any_thread() -> Result; + fn new_any_thread(args: RuntimeInitArgs) -> Result; /// Creates an `EventLoopProxy` that can be used to dispatch user events to the main event loop. fn create_proxy(&self) -> Self::EventLoopProxy; @@ -457,26 +273,15 @@ pub trait Runtime: Debug + Sized + 'static { /// Gets a runtime handle. fn handle(&self) -> Self::Handle; - /// Gets the global shortcut manager. - #[cfg(all(desktop, feature = "global-shortcut"))] - fn global_shortcut_manager(&self) -> Self::GlobalShortcutManager; - - /// Gets the clipboard manager. - #[cfg(feature = "clipboard")] - fn clipboard_manager(&self) -> Self::ClipboardManager; - /// Create a new webview window. - fn create_window(&self, pending: PendingWindow) -> Result>; - - /// Adds the icon to the system tray with the specified menu items. - #[cfg(all(desktop, feature = "system-tray"))] - #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] - fn system_tray(&self, system_tray: SystemTray) -> Result; + fn create_window( + &self, + pending: PendingWindow, + before_webview_creation: Option, + ) -> Result>; - /// Registers a system tray event handler. - #[cfg(all(desktop, feature = "system-tray"))] - #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] - fn on_system_tray_event(&mut self, f: F); + fn primary_monitor(&self) -> Option; + fn available_monitors(&self) -> Vec; /// Sets the activation policy for the application. It is set to `NSApplicationActivationPolicyRegular` by default. #[cfg(target_os = "macos")] @@ -528,8 +333,8 @@ pub trait Dispatch: Debug + Clone + Send + Sync + Sized + 'static /// Registers a window event handler. fn on_window_event(&self, f: F) -> Uuid; - /// Registers a window event handler. - fn on_menu_event(&self, f: F) -> Uuid; + /// Runs a closure with the platform webview object as argument. + fn with_webview) + Send + 'static>(&self, f: F) -> Result<()>; /// Open the web inspector which is usually called devtools. #[cfg(any(debug_assertions, feature = "devtools"))] @@ -611,9 +416,6 @@ pub trait Dispatch: Debug + Clone + Send + Sync + Sized + 'static /// Gets the window's current title. fn title(&self) -> Result; - /// Gets the window menu current visibility state. - fn is_menu_visible(&self) -> Result; - /// Returns the monitor on which the window currently resides. /// /// Returns None if current monitor can't be detected. @@ -637,6 +439,16 @@ pub trait Dispatch: Debug + Clone + Send + Sync + Sized + 'static ))] fn gtk_window(&self) -> Result; + /// Returns the vertical [`gtk::Box`] that is added by default as the sole child of this window. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn default_vbox(&self) -> Result; + fn raw_window_handle(&self) -> Result; /// Returns the current window theme. @@ -656,9 +468,10 @@ pub trait Dispatch: Debug + Clone + Send + Sync + Sized + 'static fn request_user_attention(&self, request_type: Option) -> Result<()>; /// Create a new webview window. - fn create_window( + fn create_window( &mut self, pending: PendingWindow, + before_webview_creation: Option, ) -> Result>; /// Updates the window resizable flag. @@ -691,6 +504,9 @@ pub trait Dispatch: Debug + Clone + Send + Sync + Sized + 'static /// Updates the window title. fn set_title>(&self, title: S) -> Result<()>; + /// Naviagte to the given URL. + fn navigate(&self, url: Url) -> Result<()>; + /// Maximizes the window. fn maximize(&self) -> Result<()>; @@ -703,12 +519,6 @@ pub trait Dispatch: Debug + Clone + Send + Sync + Sized + 'static /// Unminimizes the window. fn unminimize(&self) -> Result<()>; - /// Shows the window menu. - fn show_menu(&self) -> Result<()>; - - /// Hides the window menu. - fn hide_menu(&self) -> Result<()>; - /// Shows the window. fn show(&self) -> Result<()>; @@ -718,12 +528,18 @@ pub trait Dispatch: Debug + Clone + Send + Sync + Sized + 'static /// Closes the window. fn close(&self) -> Result<()>; - /// Updates the hasDecorations flag. + /// Updates the decorations flag. fn set_decorations(&self, decorations: bool) -> Result<()>; + /// Updates the shadow flag. + fn set_shadow(&self, enable: bool) -> Result<()>; + /// Updates the window alwaysOnTop flag. fn set_always_on_top(&self, always_on_top: bool) -> Result<()>; + /// Updates the window visibleOnAllWorkspaces flag. + fn set_visible_on_all_workspaces(&self, visible_on_all_workspaces: bool) -> Result<()>; + /// Prevents the window contents from being captured by other apps. fn set_content_protected(&self, protected: bool) -> Result<()>; @@ -776,7 +592,4 @@ pub trait Dispatch: Debug + Clone + Send + Sync + Sized + 'static /// Executes javascript on the window this [`Dispatch`] represents. fn eval_script>(&self, script: S) -> Result<()>; - - /// Applies the specified `update` to the menu item associated with the given `id`. - fn update_menu_item(&self, id: u16, update: menu::MenuUpdate) -> Result<()>; } diff --git a/core/tauri-runtime/src/menu.rs b/core/tauri-runtime/src/menu.rs deleted file mode 100644 index bc65aa890b0..00000000000 --- a/core/tauri-runtime/src/menu.rs +++ /dev/null @@ -1,747 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use std::{ - collections::hash_map::DefaultHasher, - fmt, - hash::{Hash, Hasher}, -}; - -pub type MenuHash = u16; -pub type MenuId = String; -pub type MenuIdRef<'a> = &'a str; - -/// Named images defined by the system. -#[cfg(target_os = "macos")] -#[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))] -#[derive(Debug, Clone)] -pub enum NativeImage { - /// An add item template image. - Add, - /// Advanced preferences toolbar icon for the preferences window. - Advanced, - /// A Bluetooth template image. - Bluetooth, - /// Bookmarks image suitable for a template. - Bookmarks, - /// A caution image. - Caution, - /// A color panel toolbar icon. - ColorPanel, - /// A column view mode template image. - ColumnView, - /// A computer icon. - Computer, - /// An enter full-screen mode template image. - EnterFullScreen, - /// Permissions for all users. - Everyone, - /// An exit full-screen mode template image. - ExitFullScreen, - /// A cover flow view mode template image. - FlowView, - /// A folder image. - Folder, - /// A burnable folder icon. - FolderBurnable, - /// A smart folder icon. - FolderSmart, - /// A link template image. - FollowLinkFreestanding, - /// A font panel toolbar icon. - FontPanel, - /// A `go back` template image. - GoLeft, - /// A `go forward` template image. - GoRight, - /// Home image suitable for a template. - Home, - /// An iChat Theater template image. - IChatTheater, - /// An icon view mode template image. - IconView, - /// An information toolbar icon. - Info, - /// A template image used to denote invalid data. - InvalidDataFreestanding, - /// A generic left-facing triangle template image. - LeftFacingTriangle, - /// A list view mode template image. - ListView, - /// A locked padlock template image. - LockLocked, - /// An unlocked padlock template image. - LockUnlocked, - /// A horizontal dash, for use in menus. - MenuMixedState, - /// A check mark template image, for use in menus. - MenuOnState, - /// A MobileMe icon. - MobileMe, - /// A drag image for multiple items. - MultipleDocuments, - /// A network icon. - Network, - /// A path button template image. - Path, - /// General preferences toolbar icon for the preferences window. - PreferencesGeneral, - /// A Quick Look template image. - QuickLook, - /// A refresh template image. - RefreshFreestanding, - /// A refresh template image. - Refresh, - /// A remove item template image. - Remove, - /// A reveal contents template image. - RevealFreestanding, - /// A generic right-facing triangle template image. - RightFacingTriangle, - /// A share view template image. - Share, - /// A slideshow template image. - Slideshow, - /// A badge for a `smart` item. - SmartBadge, - /// Small green indicator, similar to iChat’s available image. - StatusAvailable, - /// Small clear indicator. - StatusNone, - /// Small yellow indicator, similar to iChat’s idle image. - StatusPartiallyAvailable, - /// Small red indicator, similar to iChat’s unavailable image. - StatusUnavailable, - /// A stop progress template image. - StopProgressFreestanding, - /// A stop progress button template image. - StopProgress, - - /// An image of the empty trash can. - TrashEmpty, - /// An image of the full trash can. - TrashFull, - /// Permissions for a single user. - User, - /// User account toolbar icon for the preferences window. - UserAccounts, - /// Permissions for a group of users. - UserGroup, - /// Permissions for guests. - UserGuest, -} - -#[derive(Debug, Clone)] -pub enum MenuUpdate { - /// Modifies the enabled state of the menu item. - SetEnabled(bool), - /// Modifies the title (label) of the menu item. - SetTitle(String), - /// Modifies the selected state of the menu item. - SetSelected(bool), - /// Update native image. - #[cfg(target_os = "macos")] - #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))] - SetNativeImage(NativeImage), -} - -pub trait TrayHandle: fmt::Debug + Clone + Send + Sync { - fn set_icon(&self, icon: crate::Icon) -> crate::Result<()>; - fn set_menu(&self, menu: crate::menu::SystemTrayMenu) -> crate::Result<()>; - fn update_item(&self, id: u16, update: MenuUpdate) -> crate::Result<()>; - #[cfg(target_os = "macos")] - 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<()>; -} - -/// A window menu. -#[derive(Debug, Default, Clone)] -#[non_exhaustive] -pub struct Menu { - pub items: Vec, -} - -#[derive(Debug, Clone)] -#[non_exhaustive] -pub struct Submenu { - pub title: String, - pub enabled: bool, - pub inner: Menu, -} - -impl Submenu { - /// Creates a new submenu with the given title and menu items. - pub fn new>(title: S, menu: Menu) -> Self { - Self { - title: title.into(), - enabled: true, - inner: menu, - } - } -} - -impl Menu { - /// Creates a new window menu. - pub fn new() -> Self { - Default::default() - } - - /// Creates a menu filled with default menu items and submenus. - /// - /// ## Platform-specific: - /// - /// - **Windows**: - /// - File - /// - CloseWindow - /// - Quit - /// - Edit - /// - Cut - /// - Copy - /// - Paste - /// - Window - /// - Minimize - /// - CloseWindow - /// - /// - **Linux**: - /// - File - /// - CloseWindow - /// - Quit - /// - Window - /// - Minimize - /// - CloseWindow - /// - /// - **macOS**: - /// - App - /// - About - /// - Separator - /// - Services - /// - Separator - /// - Hide - /// - HideOthers - /// - ShowAll - /// - Separator - /// - Quit - /// - File - /// - CloseWindow - /// - Edit - /// - Undo - /// - Redo - /// - Separator - /// - Cut - /// - Copy - /// - Paste - /// - SelectAll - /// - View - /// - EnterFullScreen - /// - Window - /// - Minimize - /// - Zoom - /// - Separator - /// - CloseWindow - pub fn os_default(#[allow(unused)] app_name: &str) -> Self { - let mut menu = Menu::new(); - #[cfg(target_os = "macos")] - { - menu = menu.add_submenu(Submenu::new( - app_name, - Menu::new() - .add_native_item(MenuItem::About( - app_name.to_string(), - AboutMetadata::default(), - )) - .add_native_item(MenuItem::Separator) - .add_native_item(MenuItem::Services) - .add_native_item(MenuItem::Separator) - .add_native_item(MenuItem::Hide) - .add_native_item(MenuItem::HideOthers) - .add_native_item(MenuItem::ShowAll) - .add_native_item(MenuItem::Separator) - .add_native_item(MenuItem::Quit), - )); - } - - let mut file_menu = Menu::new(); - file_menu = file_menu.add_native_item(MenuItem::CloseWindow); - #[cfg(not(target_os = "macos"))] - { - file_menu = file_menu.add_native_item(MenuItem::Quit); - } - menu = menu.add_submenu(Submenu::new("File", file_menu)); - - #[cfg(not(target_os = "linux"))] - let mut edit_menu = Menu::new(); - #[cfg(target_os = "macos")] - { - edit_menu = edit_menu.add_native_item(MenuItem::Undo); - edit_menu = edit_menu.add_native_item(MenuItem::Redo); - edit_menu = edit_menu.add_native_item(MenuItem::Separator); - } - #[cfg(not(target_os = "linux"))] - { - edit_menu = edit_menu.add_native_item(MenuItem::Cut); - edit_menu = edit_menu.add_native_item(MenuItem::Copy); - edit_menu = edit_menu.add_native_item(MenuItem::Paste); - } - #[cfg(target_os = "macos")] - { - edit_menu = edit_menu.add_native_item(MenuItem::SelectAll); - } - #[cfg(not(target_os = "linux"))] - { - menu = menu.add_submenu(Submenu::new("Edit", edit_menu)); - } - #[cfg(target_os = "macos")] - { - menu = menu.add_submenu(Submenu::new( - "View", - Menu::new().add_native_item(MenuItem::EnterFullScreen), - )); - } - - let mut window_menu = Menu::new(); - window_menu = window_menu.add_native_item(MenuItem::Minimize); - #[cfg(target_os = "macos")] - { - window_menu = window_menu.add_native_item(MenuItem::Zoom); - window_menu = window_menu.add_native_item(MenuItem::Separator); - } - window_menu = window_menu.add_native_item(MenuItem::CloseWindow); - menu = menu.add_submenu(Submenu::new("Window", window_menu)); - - menu - } - - /// Creates a new window menu with the given items. - /// - /// # Examples - /// ``` - /// # use tauri_runtime::menu::{Menu, MenuItem, CustomMenuItem, Submenu}; - /// Menu::with_items([ - /// MenuItem::SelectAll.into(), - /// #[cfg(target_os = "macos")] - /// MenuItem::Redo.into(), - /// CustomMenuItem::new("toggle", "Toggle visibility").into(), - /// Submenu::new("View", Menu::new()).into(), - /// ]); - /// ``` - pub fn with_items>(items: I) -> Self { - Self { - items: items.into_iter().collect(), - } - } - - /// Adds the custom menu item to the menu. - #[must_use] - pub fn add_item(mut self, item: CustomMenuItem) -> Self { - self.items.push(MenuEntry::CustomItem(item)); - self - } - - /// Adds a native item to the menu. - #[must_use] - pub fn add_native_item(mut self, item: MenuItem) -> Self { - self.items.push(MenuEntry::NativeItem(item)); - self - } - - /// Adds an entry with submenu. - #[must_use] - pub fn add_submenu(mut self, submenu: Submenu) -> Self { - self.items.push(MenuEntry::Submenu(submenu)); - self - } -} - -/// A custom menu item. -#[derive(Debug, Clone)] -#[non_exhaustive] -pub struct CustomMenuItem { - pub id: MenuHash, - pub id_str: MenuId, - pub title: String, - pub keyboard_accelerator: Option, - pub enabled: bool, - pub selected: bool, - #[cfg(target_os = "macos")] - pub native_image: Option, -} - -impl CustomMenuItem { - /// Create new custom menu item. - pub fn new, T: Into>(id: I, title: T) -> Self { - let id_str = id.into(); - Self { - id: Self::hash(&id_str), - id_str, - title: title.into(), - keyboard_accelerator: None, - enabled: true, - selected: false, - #[cfg(target_os = "macos")] - native_image: None, - } - } - - /// Assign a keyboard shortcut to the menu action. - #[must_use] - pub fn accelerator>(mut self, accelerator: T) -> Self { - self.keyboard_accelerator.replace(accelerator.into()); - self - } - - #[cfg(target_os = "macos")] - #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))] - #[must_use] - /// A native image do render on the menu item. - pub fn native_image(mut self, image: NativeImage) -> Self { - self.native_image.replace(image); - self - } - - /// Mark the item as disabled. - #[must_use] - pub fn disabled(mut self) -> Self { - self.enabled = false; - self - } - - /// Mark the item as selected. - #[must_use] - pub fn selected(mut self) -> Self { - self.selected = true; - self - } - - fn hash(id: &str) -> MenuHash { - let mut hasher = DefaultHasher::new(); - id.hash(&mut hasher); - hasher.finish() as MenuHash - } -} - -/// A system tray menu. -#[derive(Debug, Default, Clone)] -#[non_exhaustive] -pub struct SystemTrayMenu { - pub items: Vec, -} - -#[derive(Debug, Clone)] -#[non_exhaustive] -pub struct SystemTraySubmenu { - pub title: String, - pub enabled: bool, - pub inner: SystemTrayMenu, -} - -impl SystemTraySubmenu { - /// Creates a new submenu with the given title and menu items. - pub fn new>(title: S, menu: SystemTrayMenu) -> Self { - Self { - title: title.into(), - enabled: true, - inner: menu, - } - } -} - -impl SystemTrayMenu { - /// Creates a new system tray menu. - pub fn new() -> Self { - Default::default() - } - - /// Adds the custom menu item to the system tray menu. - #[must_use] - pub fn add_item(mut self, item: CustomMenuItem) -> Self { - self.items.push(SystemTrayMenuEntry::CustomItem(item)); - self - } - - /// Adds a native item to the system tray menu. - #[must_use] - pub fn add_native_item(mut self, item: SystemTrayMenuItem) -> Self { - self.items.push(SystemTrayMenuEntry::NativeItem(item)); - self - } - - /// Adds an entry with submenu. - #[must_use] - pub fn add_submenu(mut self, submenu: SystemTraySubmenu) -> Self { - self.items.push(SystemTrayMenuEntry::Submenu(submenu)); - self - } -} - -/// An entry on the system tray menu. -#[derive(Debug, Clone)] -pub enum SystemTrayMenuEntry { - /// A custom item. - CustomItem(CustomMenuItem), - /// A native item. - NativeItem(SystemTrayMenuItem), - /// An entry with submenu. - Submenu(SystemTraySubmenu), -} - -/// System tray menu item. -#[derive(Debug, Clone)] -#[non_exhaustive] -pub enum SystemTrayMenuItem { - /// A separator. - Separator, -} - -/// An entry on the system tray menu. -#[derive(Debug, Clone)] -pub enum MenuEntry { - /// A custom item. - CustomItem(CustomMenuItem), - /// A native item. - NativeItem(MenuItem), - /// An entry with submenu. - Submenu(Submenu), -} - -impl From for MenuEntry { - fn from(item: CustomMenuItem) -> Self { - Self::CustomItem(item) - } -} - -impl From for MenuEntry { - fn from(item: MenuItem) -> Self { - Self::NativeItem(item) - } -} - -impl From for MenuEntry { - fn from(submenu: Submenu) -> Self { - Self::Submenu(submenu) - } -} - -/// Application metadata for the [`MenuItem::About`] action. -/// -/// ## Platform-specific -/// -/// - **Windows / macOS / Android / iOS:** The metadata is ignored on these platforms. -#[derive(Debug, Clone, Default)] -#[non_exhaustive] -pub struct AboutMetadata { - /// The application name. - pub version: Option, - /// The authors of the application. - pub authors: Option>, - /// Application comments. - pub comments: Option, - /// The copyright of the application. - pub copyright: Option, - /// The license of the application. - pub license: Option, - /// The application website. - pub website: Option, - /// The website label. - pub website_label: Option, -} - -impl AboutMetadata { - /// Creates the default metadata for the [`MenuItem::About`] action, which is just empty. - pub fn new() -> Self { - Default::default() - } - - /// Defines the application version. - pub fn version(mut self, version: impl Into) -> Self { - self.version.replace(version.into()); - self - } - - /// Defines the application authors. - pub fn authors(mut self, authors: Vec) -> Self { - self.authors.replace(authors); - self - } - - /// Defines the application comments. - pub fn comments(mut self, comments: impl Into) -> Self { - self.comments.replace(comments.into()); - self - } - - /// Defines the application copyright. - pub fn copyright(mut self, copyright: impl Into) -> Self { - self.copyright.replace(copyright.into()); - self - } - - /// Defines the application license. - pub fn license(mut self, license: impl Into) -> Self { - self.license.replace(license.into()); - self - } - - /// Defines the application's website link. - pub fn website(mut self, website: impl Into) -> Self { - self.website.replace(website.into()); - self - } - - /// Defines the application's website link name. - pub fn website_label(mut self, website_label: impl Into) -> Self { - self.website_label.replace(website_label.into()); - self - } -} - -/// A menu item, bound to a pre-defined action or `Custom` emit an event. Note that status bar only -/// supports `Custom` menu item variants. And on the menu bar, some platforms might not support some -/// of the variants. Unsupported variant will be no-op on such platform. -#[derive(Debug, Clone)] -#[non_exhaustive] -pub enum MenuItem { - /// Shows a standard "About" item. - /// - /// The first value is the application name, and the second is its metadata. - /// - /// ## Platform-specific - /// - /// - **Windows / Android / iOS:** Unsupported - /// - **Linux:** The metadata is only applied on Linux - /// - About(String, AboutMetadata), - - /// A standard "hide the app" menu item. - /// - /// ## Platform-specific - /// - /// - **Android / iOS:** Unsupported - /// - Hide, - - /// A standard "Services" menu item. - /// - /// ## Platform-specific - /// - /// - **Windows / Linux / Android / iOS:** Unsupported - /// - Services, - - /// A "hide all other windows" menu item. - /// - /// ## Platform-specific - /// - /// - **Windows / Linux / Android / iOS:** Unsupported - /// - HideOthers, - - /// A menu item to show all the windows for this app. - /// - /// ## Platform-specific - /// - /// - **Windows / Linux / Android / iOS:** Unsupported - /// - ShowAll, - - /// Close the current window. - /// - /// ## Platform-specific - /// - /// - **Android / iOS:** Unsupported - /// - CloseWindow, - - /// A "quit this app" menu icon. - /// - /// ## Platform-specific - /// - /// - **Android / iOS:** Unsupported - /// - Quit, - - /// A menu item for enabling copying (often text) from responders. - /// - /// ## Platform-specific - /// - /// - **Android / iOS / Linux:** Unsupported - /// - Copy, - - /// A menu item for enabling cutting (often text) from responders. - /// - /// ## Platform-specific - /// - /// - **Android / iOS / Linux:** Unsupported - /// - Cut, - - /// An "undo" menu item; particularly useful for supporting the cut/copy/paste/undo lifecycle - /// of events. - /// - /// ## Platform-specific - /// - /// - **Windows / Linux / Android / iOS:** Unsupported - /// - Undo, - - /// An "redo" menu item; particularly useful for supporting the cut/copy/paste/undo lifecycle - /// of events. - /// - /// ## Platform-specific - /// - /// - **Windows / Linux / Android / iOS:** Unsupported - /// - Redo, - - /// A menu item for selecting all (often text) from responders. - /// - /// ## Platform-specific - /// - /// - **Windows / Android / iOS / Linux:** Unsupported - /// - SelectAll, - - /// A menu item for pasting (often text) into responders. - /// - /// ## Platform-specific - /// - /// - **Android / iOS / Linux:** Unsupported - /// - Paste, - - /// A standard "enter full screen" item. - /// - /// ## Platform-specific - /// - /// - **Windows / Linux / Android / iOS:** Unsupported - /// - EnterFullScreen, - - /// An item for minimizing the window with the standard system controls. - /// - /// ## Platform-specific - /// - /// - **Android / iOS:** Unsupported - /// - Minimize, - - /// An item for instructing the app to zoom - /// - /// ## Platform-specific - /// - /// - **Windows / Linux / Android / iOS:** Unsupported - /// - Zoom, - - /// Represents a Separator - /// - /// ## Platform-specific - /// - /// - **Android / iOS:** Unsupported - /// - Separator, -} diff --git a/core/tauri-runtime/src/webview.rs b/core/tauri-runtime/src/webview.rs index 0161cc296e6..79814a4f807 100644 --- a/core/tauri-runtime/src/webview.rs +++ b/core/tauri-runtime/src/webview.rs @@ -4,12 +4,12 @@ //! Items specific to the [`Runtime`](crate::Runtime)'s webview. -use crate::{menu::Menu, window::DetachedWindow, Icon}; +use crate::{window::DetachedWindow, Icon}; #[cfg(target_os = "macos")] use tauri_utils::TitleBarStyle; use tauri_utils::{ - config::{WindowConfig, WindowUrl}, + config::{WindowConfig, WindowEffectsConfig, WindowUrl}, Theme, }; @@ -29,11 +29,14 @@ pub struct WebviewAttributes { pub clipboard: bool, pub accept_first_mouse: bool, pub additional_browser_args: Option, + pub window_effects: Option, + pub incognito: bool, } impl From<&WindowConfig> for WebviewAttributes { fn from(config: &WindowConfig) -> Self { let mut builder = Self::new(config.url.clone()); + builder = builder.incognito(config.incognito); builder = builder.accept_first_mouse(config.accept_first_mouse); if !config.file_drop_enabled { builder = builder.disable_file_drop_handler(); @@ -44,10 +47,12 @@ impl From<&WindowConfig> for WebviewAttributes { if let Some(additional_browser_args) = &config.additional_browser_args { builder = builder.additional_browser_args(additional_browser_args); } + if let Some(effects) = &config.window_effects { + builder = builder.window_effects(effects.clone()); + } builder } } - impl WebviewAttributes { /// Initializes the default attributes for a webview. pub fn new(url: WindowUrl) -> Self { @@ -60,6 +65,8 @@ impl WebviewAttributes { clipboard: false, accept_first_mouse: false, additional_browser_args: None, + window_effects: None, + incognito: false, } } @@ -114,6 +121,20 @@ impl WebviewAttributes { self.additional_browser_args = Some(additional_args.to_string()); self } + + /// Sets window effects + #[must_use] + pub fn window_effects(mut self, effects: WindowEffectsConfig) -> Self { + self.window_effects = Some(effects); + self + } + + /// Enable or disable incognito mode for the WebView. + #[must_use] + pub fn incognito(mut self, incognito: bool) -> Self { + self.incognito = incognito; + self + } } /// Do **NOT** implement this trait except for use in a custom [`Runtime`](crate::Runtime). @@ -132,10 +153,6 @@ pub trait WindowBuilder: WindowBuilderBase { /// Initializes a new webview builder from a [`WindowConfig`] fn with_config(config: WindowConfig) -> Self; - /// Sets the menu for the window. - #[must_use] - fn menu(self, menu: Menu) -> Self; - /// Show window in the center of the screen. #[must_use] fn center(self) -> Self; @@ -227,6 +244,10 @@ pub trait WindowBuilder: WindowBuilderBase { #[must_use] fn always_on_top(self, always_on_top: bool) -> Self; + /// Whether the window should be visible on all workspaces or virtual desktops. + #[must_use] + fn visible_on_all_workspaces(self, visible_on_all_workspaces: bool) -> Self; + /// Prevents the window contents from being captured by other apps. #[must_use] fn content_protected(self, protected: bool) -> Self; @@ -238,6 +259,18 @@ pub trait WindowBuilder: WindowBuilderBase { #[must_use] fn skip_taskbar(self, skip: bool) -> Self; + /// Sets whether or not the window has shadow. + /// + /// ## Platform-specific + /// + /// - **Windows:** + /// - `false` has no effect on decorated window, shadows are always ON. + /// - `true` will make ndecorated window have a 1px white border, + /// and on Windows 11, it will have a rounded corners. + /// - **Linux:** Unsupported. + #[must_use] + fn shadow(self, enable: bool) -> Self; + /// Sets a parent to the window to be created. /// /// A child window has the WS_CHILD style and is confined to the client area of its parent window. @@ -293,9 +326,6 @@ pub trait WindowBuilder: WindowBuilderBase { /// Whether the icon was set or not. fn has_icon(&self) -> bool; - - /// Gets the window menu. - fn get_menu(&self) -> Option<&Menu>; } /// IPC handler. diff --git a/core/tauri-runtime/src/window.rs b/core/tauri-runtime/src/window.rs index 7c154cdea58..5f06b6219ee 100644 --- a/core/tauri-runtime/src/window.rs +++ b/core/tauri-runtime/src/window.rs @@ -6,26 +6,31 @@ use crate::{ http::{Request as HttpRequest, Response as HttpResponse}, - menu::{Menu, MenuEntry, MenuHash, MenuId}, webview::{WebviewAttributes, WebviewIpcHandler}, Dispatch, Runtime, UserEvent, WindowBuilder, }; -use serde::{Deserialize, Deserializer, Serialize}; + +use serde::{Deserialize, Deserializer}; use tauri_utils::{config::WindowConfig, Theme}; use url::Url; use std::{ - collections::{HashMap, HashSet}, + collections::HashMap, hash::{Hash, Hasher}, + marker::PhantomData, path::PathBuf, - sync::{mpsc::Sender, Arc, Mutex}, + sync::mpsc::Sender, }; +use self::dpi::PhysicalPosition; + type UriSchemeProtocol = dyn Fn(&HttpRequest) -> Result> + Send + Sync + 'static; type WebResourceRequestHandler = dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync; +type NavigationHandler = dyn Fn(&Url) -> bool + Send; + /// UI scaling utilities. pub mod dpi; @@ -73,37 +78,27 @@ pub enum WindowEvent { #[non_exhaustive] pub enum FileDropEvent { /// The file(s) have been dragged onto the window, but have not been dropped yet. - Hovered(Vec), + Hovered { + paths: Vec, + /// The position of the mouse cursor. + position: PhysicalPosition, + }, /// The file(s) have been dropped onto the window. - Dropped(Vec), + Dropped { + paths: Vec, + /// The position of the mouse cursor. + position: PhysicalPosition, + }, /// The file drop was aborted. Cancelled, } -/// A menu event. -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct MenuEvent { - pub menu_item_id: u16, -} - -fn get_menu_ids(map: &mut HashMap, menu: &Menu) { - for item in &menu.items { - match item { - MenuEntry::CustomItem(c) => { - map.insert(c.id, c.id_str.clone()); - } - MenuEntry::Submenu(s) => get_menu_ids(map, &s.inner), - _ => {} - } - } -} - /// Describes the appearance of the mouse cursor. #[non_exhaustive] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] pub enum CursorIcon { /// The platform-dependent default cursor. + #[default] Default, /// A simple crosshair. Crosshair, @@ -205,10 +200,11 @@ impl<'de> Deserialize<'de> for CursorIcon { } } -impl Default for CursorIcon { - fn default() -> Self { - CursorIcon::Default - } +#[cfg(target_os = "android")] +pub struct CreationContext<'a, 'b> { + pub env: &'a mut jni::JNIEnv<'b>, + pub activity: &'a jni::objects::JObject<'b>, + pub webview: &'a jni::objects::JObject<'b>, } /// A webview window that has yet to be built. @@ -227,19 +223,18 @@ pub struct PendingWindow> { /// How to handle IPC calls on the webview window. pub ipc_handler: Option>, - /// Maps runtime id to a string menu id. - pub menu_ids: Arc>>, - - /// A HashMap mapping JS event names with associated listener ids. - pub js_event_listeners: Arc>>>, - /// A handler to decide if incoming url is allowed to navigate. - pub navigation_handler: Option bool + Send>>, - - pub web_resource_request_handler: Option>, + pub navigation_handler: Option>, /// The resolved URL to load on the webview. pub url: String, + + #[cfg(target_os = "android")] + #[allow(clippy::type_complexity)] + pub on_webview_created: + Option) -> Result<(), jni::errors::Error> + Send>>, + + pub web_resource_request_handler: Option>, } pub fn is_label_valid(label: &str) -> bool { @@ -262,10 +257,6 @@ impl> PendingWindow { webview_attributes: WebviewAttributes, label: impl Into, ) -> crate::Result { - let mut menu_ids = HashMap::new(); - if let Some(menu) = window_builder.get_menu() { - get_menu_ids(&mut menu_ids, menu); - } let label = label.into(); if !is_label_valid(&label) { Err(crate::Error::InvalidWindowLabel) @@ -276,11 +267,11 @@ impl> PendingWindow { uri_scheme_protocols: Default::default(), label, ipc_handler: None, - menu_ids: Arc::new(Mutex::new(menu_ids)), - js_event_listeners: Default::default(), navigation_handler: Default::default(), - web_resource_request_handler: Default::default(), url: "tauri://localhost".to_string(), + #[cfg(target_os = "android")] + on_webview_created: None, + web_resource_request_handler: Default::default(), }) } } @@ -293,10 +284,7 @@ impl> PendingWindow { ) -> crate::Result { let window_builder = <>::WindowBuilder>::with_config(window_config); - let mut menu_ids = HashMap::new(); - if let Some(menu) = window_builder.get_menu() { - get_menu_ids(&mut menu_ids, menu); - } + let label = label.into(); if !is_label_valid(&label) { Err(crate::Error::InvalidWindowLabel) @@ -307,24 +295,15 @@ impl> PendingWindow { uri_scheme_protocols: Default::default(), label, ipc_handler: None, - menu_ids: Arc::new(Mutex::new(menu_ids)), - js_event_listeners: Default::default(), navigation_handler: Default::default(), - web_resource_request_handler: Default::default(), url: "tauri://localhost".to_string(), + #[cfg(target_os = "android")] + on_webview_created: None, + web_resource_request_handler: Default::default(), }) } } - #[must_use] - pub fn set_menu(mut self, menu: Menu) -> Self { - let mut menu_ids = HashMap::new(); - get_menu_ids(&mut menu_ids, &menu); - *self.menu_ids.lock().unwrap() = menu_ids; - self.window_builder = self.window_builder.menu(menu); - self - } - pub fn register_uri_scheme_protocol< N: Into, H: Fn(&HttpRequest) -> Result> + Send + Sync + 'static, @@ -338,15 +317,17 @@ impl> PendingWindow { .uri_scheme_protocols .insert(uri_scheme, Box::new(move |data| (protocol)(data))); } -} -/// Key for a JS event listener. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct JsEventListenerKey { - /// The associated window label. - pub window_label: Option, - /// The event name. - pub event: String, + #[cfg(target_os = "android")] + pub fn on_webview_created< + F: Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send + 'static, + >( + mut self, + f: F, + ) -> Self { + self.on_webview_created.replace(Box::new(f)); + self + } } /// A webview window that is not yet managed by Tauri. @@ -357,12 +338,6 @@ pub struct DetachedWindow> { /// The [`Dispatch`](crate::Dispatch) associated with the window. pub dispatcher: R::Dispatcher, - - /// Maps runtime id to a string menu id. - pub menu_ids: Arc>>, - - /// A HashMap mapping JS event names with associated listener ids. - pub js_event_listeners: Arc>>>, } impl> Clone for DetachedWindow { @@ -370,8 +345,6 @@ impl> Clone for DetachedWindow { Self { label: self.label.clone(), dispatcher: self.dispatcher.clone(), - menu_ids: self.menu_ids.clone(), - js_event_listeners: self.js_event_listeners.clone(), } } } @@ -390,3 +363,28 @@ impl> PartialEq for DetachedWindow { self.label.eq(&other.label) } } + +/// A raw window type that contains fields to access +/// the HWND on Windows, gtk::ApplicationWindow on Linux and +/// NSView on macOS. +pub struct RawWindow<'a> { + #[cfg(windows)] + pub hwnd: isize, + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub gtk_window: &'a gtk::ApplicationWindow, + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub default_vbox: Option<&'a gtk::Box>, + pub _marker: &'a PhantomData<()>, +} diff --git a/core/tauri-utils/CHANGELOG.md b/core/tauri-utils/CHANGELOG.md index 835f3ed3679..e385391b3b2 100644 --- a/core/tauri-utils/CHANGELOG.md +++ b/core/tauri-utils/CHANGELOG.md @@ -1,5 +1,72 @@ # Changelog +## \[2.0.0-alpha.7] + +### New Features + +- [`4db363a0`](https://www.github.com/tauri-apps/tauri/commit/4db363a03c182349f8491f46ced258d84723b11f)([#6589](https://www.github.com/tauri-apps/tauri/pull/6589)) Added `visible_on_all_workspaces` configuration option to `WindowBuilder`, `Window`, and `WindowConfig`. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) Add option to specify a tooltip text for the tray icon in the config. +- [`74b1f4fc`](https://www.github.com/tauri-apps/tauri/commit/74b1f4fc6625d5b4f9b86f70e4eebd6551c61809)([#7384](https://www.github.com/tauri-apps/tauri/pull/7384)) Add `WindowEffect::MicaDark` and `WindowEffect::MicaLight` +- [`3b98141a`](https://www.github.com/tauri-apps/tauri/commit/3b98141aa26f74c641a4090874247b97079bd58a)([#3736](https://www.github.com/tauri-apps/tauri/pull/3736)) Add a configuration object for file associations under `BundleConfig`. + +### Enhancements + +- [`fbeb5b91`](https://www.github.com/tauri-apps/tauri/commit/fbeb5b9185baeda19e865228179e3e44c165f1d9)([#7170](https://www.github.com/tauri-apps/tauri/pull/7170)) Use custom protocols on the IPC implementation to enhance performance. + +### Security fixes + +- [`43c6285e`](https://www.github.com/tauri-apps/tauri/commit/43c6285e9006fb84066461d57fe09ea8db76d636)([#7359](https://www.github.com/tauri-apps/tauri/pull/7359)) Changed HTML implementation from unmaintained `kuchiki` to `kuchikiki`. + +### Breaking Changes + +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) `systemTray` config option has been renamed to `trayIcon`. + +## \[2.0.0-alpha.6] + +### New Features + +- [`e0f0dce2`](https://www.github.com/tauri-apps/tauri/commit/e0f0dce220730e2822fc202463aedf0166145de7)([#6442](https://www.github.com/tauri-apps/tauri/pull/6442)) Added the `window_effects` option to the window configuration. + +## \[2.0.0-alpha.5] + +- [`9a79dc08`](https://www.github.com/tauri-apps/tauri/commit/9a79dc085870e0c1a5df13481ff271b8c6cc3b78)([#6947](https://www.github.com/tauri-apps/tauri/pull/6947)) Remove `enable_tauri_api` from the IPC scope. +- [`09376af5`](https://www.github.com/tauri-apps/tauri/commit/09376af59424cc27803fa2820d2ac0d4cdc90a6d)([#6704](https://www.github.com/tauri-apps/tauri/pull/6704)) Moved the `cli` feature to its own plugin in the plugins-workspace repository. +- [`e1e85dc2`](https://www.github.com/tauri-apps/tauri/commit/e1e85dc2a5f656fc37867e278cae8042037740ac)([#6925](https://www.github.com/tauri-apps/tauri/pull/6925)) Moved the `protocol` scope configuration to the `asset_protocol` field in `SecurityConfig`. +- [`e1e85dc2`](https://www.github.com/tauri-apps/tauri/commit/e1e85dc2a5f656fc37867e278cae8042037740ac)([#6925](https://www.github.com/tauri-apps/tauri/pull/6925)) Moved the updater configuration to the `BundleConfig`. +- [`b072daa3`](https://www.github.com/tauri-apps/tauri/commit/b072daa3bd3e38b808466666619ddb885052c5b2)([#6919](https://www.github.com/tauri-apps/tauri/pull/6919)) Moved the `updater` feature to its own plugin in the plugins-workspace repository. +- [`3188f376`](https://www.github.com/tauri-apps/tauri/commit/3188f3764978c6d1452ee31d5a91469691e95094)([#6883](https://www.github.com/tauri-apps/tauri/pull/6883)) Bump the MSRV to 1.65. +- [`e1e85dc2`](https://www.github.com/tauri-apps/tauri/commit/e1e85dc2a5f656fc37867e278cae8042037740ac)([#6925](https://www.github.com/tauri-apps/tauri/pull/6925)) Removed the allowlist configuration. +- [`2d5378bf`](https://www.github.com/tauri-apps/tauri/commit/2d5378bfc1ba817ee2f331b41738a90e5997e5e8)([#6717](https://www.github.com/tauri-apps/tauri/pull/6717)) Remove the updater's dialog option. + +## \[2.0.0-alpha.4] + +- Added `android` configuration object under `tauri > bundle`. + - [db4c9dc6](https://www.github.com/tauri-apps/tauri/commit/db4c9dc655e07ee2184fe04571f500f7910890cd) feat(core): add option to configure Android's minimum SDK version ([#6651](https://www.github.com/tauri-apps/tauri/pull/6651)) on 2023-04-07 + +## \[2.0.0-alpha.3] + +- Pull changes from Tauri 1.3 release. + - [](https://www.github.com/tauri-apps/tauri/commit/undefined) on undefined + +## \[2.0.0-alpha.2] + +- Added the `shadow` option to the window configuration and `set_shadow` option to the `window` allow list. + - [a81750d7](https://www.github.com/tauri-apps/tauri/commit/a81750d779bc72f0fdb7de90b7fbddfd8049b328) feat(core): add shadow APIs ([#6206](https://www.github.com/tauri-apps/tauri/pull/6206)) on 2023-02-08 + +## \[2.0.0-alpha.1] + +- Bump the MSRV to 1.64. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Added `crate_name` field on `PackageInfo`. + - [630a7f4b](https://www.github.com/tauri-apps/tauri/commit/630a7f4b18cef169bfd48673609306fec434e397) refactor: remove mobile log initialization, ref [#6049](https://www.github.com/tauri-apps/tauri/pull/6049) ([#6081](https://www.github.com/tauri-apps/tauri/pull/6081)) on 2023-01-17 + +## \[2.0.0-alpha.0] + +- Parse `android` and `ios` Tauri configuration files. + - [b3a3afc7](https://www.github.com/tauri-apps/tauri/commit/b3a3afc7de8de4021d73559288f5192732a706cf) feat(core): detect android and ios platform configuration files ([#4997](https://www.github.com/tauri-apps/tauri/pull/4997)) on 2022-08-22 +- First mobile alpha release! + - [fa3a1098](https://www.github.com/tauri-apps/tauri/commit/fa3a10988a03aed1b66fb17d893b1a9adb90f7cd) feat(ci): prepare 2.0.0-alpha.0 ([#5786](https://www.github.com/tauri-apps/tauri/pull/5786)) on 2022-12-08 + ## \[1.4.0] ### New Features diff --git a/core/tauri-utils/Cargo.toml b/core/tauri-utils/Cargo.toml index 86076c606f2..7da87f5c339 100644 --- a/core/tauri-utils/Cargo.toml +++ b/core/tauri-utils/Cargo.toml @@ -1,15 +1,16 @@ [package] name = "tauri-utils" -version = "1.4.0" -authors = [ "Tauri Programme within The Commons Conservancy" ] -license = "Apache-2.0 OR MIT" -homepage = "https://tauri.app" -repository = "https://github.com/tauri-apps/tauri" +version = "2.0.0-alpha.7" description = "Utilities for Tauri" -edition = "2021" -rust-version = "1.60" exclude = [ "CHANGELOG.md", "/target" ] readme = "README.md" +authors = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +categories = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } [dependencies] serde = { version = "1", features = [ "derive" ] } @@ -18,8 +19,8 @@ thiserror = "1" phf = { version = "0.10", features = [ "macros" ] } brotli = { version = "3", optional = true, default-features = false, features = [ "std" ] } url = { version = "2", features = [ "serde" ] } -kuchiki = "0.8" -html5ever = "0.25" +html5ever = "0.26" +kuchiki = { package = "kuchikiki", version = "0.8" } proc-macro2 = { version = "1", optional = true } quote = { version = "1", optional = true } schemars = { version = "0.8", features = [ "url" ], optional = true } @@ -42,7 +43,7 @@ dunce = "1" heck = "0.4" [target."cfg(windows)".dependencies.windows] -version = "0.39.0" +version = "0.48.0" features = [ "implement", "Win32_Foundation", diff --git a/core/tauri-utils/src/config.rs b/core/tauri-utils/src/config.rs index 6dbe8f7279c..4cc7aeac1fe 100644 --- a/core/tauri-utils/src/config.rs +++ b/core/tauri-utils/src/config.rs @@ -34,7 +34,7 @@ use std::{ /// Items to help with parsing content into a [`Config`]. pub mod parse; -use crate::TitleBarStyle; +use crate::{TitleBarStyle, WindowEffect, WindowEffectState}; pub use self::parse::parse; @@ -438,6 +438,21 @@ pub struct WixConfig { pub dialog_image_path: Option, } +/// Compression algorithms used in the NSIS installer. +/// +/// See +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub enum NsisCompression { + /// ZLIB uses the deflate algorithm, it is a quick and simple method. With the default compression level it uses about 300 KB of memory. + Zlib, + /// BZIP2 usually gives better compression ratios than ZLIB, but it is a bit slower and uses more memory. With the default compression level it uses about 4 MB of memory. + Bzip2, + /// LZMA (default) is a new compression method that gives very good compression ratios. The decompression speed is high (10-20 MB/s on a 2 GHz CPU), the compression speed is lower. The memory size that will be used for decompression is the dictionary size plus a few KBs, the default is 8 MB. + Lzma, +} + /// Configuration for the Installer bundle using NSIS. #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] @@ -480,6 +495,10 @@ pub struct NsisConfig { /// By default the OS language is selected, with a fallback to the first language in the `languages` array. #[serde(default, alias = "display-language-selector")] pub display_language_selector: bool, + /// Set the compression algorithm used to compress files in the installer. + /// + /// See + pub compression: Option, } /// Install Modes for the NSIS installer. @@ -624,6 +643,137 @@ impl Default for WindowsConfig { } } +/// macOS-only. Corresponds to CFBundleTypeRole +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +pub enum BundleTypeRole { + /// CFBundleTypeRole.Editor. Files can be read and edited. + #[default] + Editor, + /// CFBundleTypeRole.Viewer. Files can be read. + Viewer, + /// CFBundleTypeRole.Shell + Shell, + /// CFBundleTypeRole.QLGenerator + QLGenerator, + /// CFBundleTypeRole.None + None, +} + +impl Display for BundleTypeRole { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Editor => write!(f, "Editor"), + Self::Viewer => write!(f, "Viewer"), + Self::Shell => write!(f, "Shell"), + Self::QLGenerator => write!(f, "QLGenerator"), + Self::None => write!(f, "None"), + } + } +} + +/// An extension for a [`FileAssociation`]. +/// +/// A leading `.` is automatically stripped. +#[derive(Debug, PartialEq, Eq, Clone, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +pub struct AssociationExt(pub String); + +impl fmt::Display for AssociationExt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl<'d> serde::Deserialize<'d> for AssociationExt { + fn deserialize>(deserializer: D) -> Result { + let ext = String::deserialize(deserializer)?; + if let Some(ext) = ext.strip_prefix('.') { + Ok(AssociationExt(ext.into())) + } else { + Ok(AssociationExt(ext)) + } + } +} + +/// File association +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct FileAssociation { + /// File extensions to associate with this app. e.g. 'png' + pub ext: Vec, + /// The name. Maps to `CFBundleTypeName` on macOS. Default to ext[0] + pub name: Option, + /// The association description. Windows-only. It is displayed on the `Type` column on Windows Explorer. + pub description: Option, + /// The app’s role with respect to the type. Maps to `CFBundleTypeRole` on macOS. + #[serde(default)] + pub role: BundleTypeRole, + /// The mime-type e.g. 'image/png' or 'text/plain'. Linux-only. + #[serde(alias = "mime-type")] + pub mime_type: Option, +} + +/// The Updater configuration object. +/// +/// See more: https://tauri.app/v1/api/config#updaterconfig +#[skip_serializing_none] +#[derive(Debug, PartialEq, Eq, Clone, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct UpdaterConfig { + /// Whether the updater is active or not. + #[serde(default)] + pub active: bool, + /// Signature public key. + #[serde(default)] // use default just so the schema doesn't flag it as required + pub pubkey: String, + /// The Windows configuration for the updater. + #[serde(default)] + pub windows: UpdaterWindowsConfig, +} + +impl<'de> Deserialize<'de> for UpdaterConfig { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct InnerUpdaterConfig { + #[serde(default)] + active: bool, + pubkey: Option, + #[serde(default)] + windows: UpdaterWindowsConfig, + } + + let config = InnerUpdaterConfig::deserialize(deserializer)?; + + if config.active && config.pubkey.is_none() { + return Err(DeError::custom( + "The updater `pubkey` configuration is required.", + )); + } + + Ok(UpdaterConfig { + active: config.active, + pubkey: config.pubkey.unwrap_or_default(), + windows: config.windows, + }) + } +} + +impl Default for UpdaterConfig { + fn default() -> Self { + Self { + active: false, + pubkey: "".into(), + windows: Default::default(), + } + } +} + /// Configuration for tauri-bundler. /// /// See more: https://tauri.app/v1/api/config#bundleconfig @@ -661,6 +811,8 @@ pub struct BundleConfig { /// Should be one of the following: /// Business, DeveloperTool, Education, Entertainment, Finance, Game, ActionGame, AdventureGame, ArcadeGame, BoardGame, CardGame, CasinoGame, DiceGame, EducationalGame, FamilyGame, KidsGame, MusicGame, PuzzleGame, RacingGame, RolePlayingGame, SimulationGame, SportsGame, StrategyGame, TriviaGame, WordGame, GraphicsAndDesign, HealthcareAndFitness, Lifestyle, Medical, Music, News, Photography, Productivity, Reference, SocialNetworking, Sports, Travel, Utility, Video, Weather. pub category: Option, + /// File associations to application. + pub file_associations: Option>, /// A short description of your application. #[serde(alias = "short-description")] pub short_description: Option, @@ -692,191 +844,39 @@ pub struct BundleConfig { /// Configuration for the Windows bundle. #[serde(default)] pub windows: WindowsConfig, + /// iOS configuration. + #[serde(rename = "iOS", default)] + pub ios: IosConfig, + /// Android configuration. + #[serde(default)] + pub android: AndroidConfig, + /// The updater configuration. + #[serde(default)] + pub updater: UpdaterConfig, } -/// A CLI argument definition. -#[skip_serializing_none] -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +/// a tuple struct of RGBA colors. Each value has minimum of 0 and maximum of 255. +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)] #[cfg_attr(feature = "schema", derive(JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CliArg { - /// The short version of the argument, without the preceding -. - /// - /// NOTE: Any leading `-` characters will be stripped, and only the first non-character will be used as the short version. - pub short: Option, - /// The unique argument name - pub name: String, - /// The argument description which will be shown on the help information. - /// Typically, this is a short (one line) description of the arg. - pub description: Option, - /// The argument long description which will be shown on the help information. - /// Typically this a more detailed (multi-line) message that describes the argument. - #[serde(alias = "long-description")] - pub long_description: Option, - /// Specifies that the argument takes a value at run time. - /// - /// NOTE: values for arguments may be specified in any of the following methods - /// - Using a space such as -o value or --option value - /// - Using an equals and no space such as -o=value or --option=value - /// - Use a short and no space such as -ovalue - #[serde(default, alias = "takes-value")] - pub takes_value: bool, - /// Specifies that the argument may have an unknown number of multiple values. Without any other settings, this argument may appear only once. - /// - /// For example, --opt val1 val2 is allowed, but --opt val1 val2 --opt val3 is not. - /// - /// NOTE: Setting this requires `takes_value` to be set to true. - #[serde(default)] - pub multiple: bool, - /// Specifies that the argument may appear more than once. - /// For flags, this results in the number of occurrences of the flag being recorded. For example -ddd or -d -d -d would count as three occurrences. - /// For options or arguments that take a value, this does not affect how many values they can accept. (i.e. only one at a time is allowed) - /// - /// For example, --opt val1 --opt val2 is allowed, but --opt val1 val2 is not. - #[serde(default, alias = "multiple-occurrences")] - pub multiple_occurrences: bool, - /// Specifies how many values are required to satisfy this argument. For example, if you had a - /// `-f ` argument where you wanted exactly 3 'files' you would set - /// `number_of_values = 3`, and this argument wouldn't be satisfied unless the user provided - /// 3 and only 3 values. - /// - /// **NOTE:** Does *not* require `multiple_occurrences = true` to be set. Setting - /// `multiple_occurrences = true` would allow `-f -f ` where - /// as *not* setting it would only allow one occurrence of this argument. - /// - /// **NOTE:** implicitly sets `takes_value = true` and `multiple_values = true`. - #[serde(alias = "number-of-values")] - pub number_of_values: Option, - /// Specifies a list of possible values for this argument. - /// At runtime, the CLI verifies that only one of the specified values was used, or fails with an error message. - #[serde(alias = "possible-values")] - pub possible_values: Option>, - /// Specifies the minimum number of values for this argument. - /// For example, if you had a -f `` argument where you wanted at least 2 'files', - /// you would set `minValues: 2`, and this argument would be satisfied if the user provided, 2 or more values. - #[serde(alias = "min-values")] - pub min_values: Option, - /// Specifies the maximum number of values are for this argument. - /// For example, if you had a -f `` argument where you wanted up to 3 'files', - /// you would set .max_values(3), and this argument would be satisfied if the user provided, 1, 2, or 3 values. - #[serde(alias = "max-values")] - pub max_values: Option, - /// Sets whether or not the argument is required by default. - /// - /// - Required by default means it is required, when no other conflicting rules have been evaluated - /// - Conflicting rules take precedence over being required. - #[serde(default)] - pub required: bool, - /// Sets an arg that override this arg's required setting - /// i.e. this arg will be required unless this other argument is present. - #[serde(alias = "required-unless-present")] - pub required_unless_present: Option, - /// Sets args that override this arg's required setting - /// i.e. this arg will be required unless all these other arguments are present. - #[serde(alias = "required-unless-present-all")] - pub required_unless_present_all: Option>, - /// Sets args that override this arg's required setting - /// i.e. this arg will be required unless at least one of these other arguments are present. - #[serde(alias = "required-unless-present-any")] - pub required_unless_present_any: Option>, - /// Sets a conflicting argument by name - /// i.e. when using this argument, the following argument can't be present and vice versa. - #[serde(alias = "conflicts-with")] - pub conflicts_with: Option, - /// The same as conflictsWith but allows specifying multiple two-way conflicts per argument. - #[serde(alias = "conflicts-with-all")] - pub conflicts_with_all: Option>, - /// Tets an argument by name that is required when this one is present - /// i.e. when using this argument, the following argument must be present. - pub requires: Option, - /// Sts multiple arguments by names that are required when this one is present - /// i.e. when using this argument, the following arguments must be present. - #[serde(alias = "requires-all")] - pub requires_all: Option>, - /// Allows a conditional requirement with the signature [arg, value] - /// the requirement will only become valid if `arg`'s value equals `${value}`. - #[serde(alias = "requires-if")] - pub requires_if: Option>, - /// Allows specifying that an argument is required conditionally with the signature [arg, value] - /// the requirement will only become valid if the `arg`'s value equals `${value}`. - #[serde(alias = "requires-if-eq")] - pub required_if_eq: Option>, - /// Requires that options use the --option=val syntax - /// i.e. an equals between the option and associated value. - #[serde(alias = "requires-equals")] - pub require_equals: Option, - /// The positional argument index, starting at 1. - /// - /// The index refers to position according to other positional argument. - /// It does not define position in the argument list as a whole. When utilized with multiple=true, - /// only the last positional argument may be defined as multiple (i.e. the one with the highest index). - #[cfg_attr(feature = "schema", validate(range(min = 1)))] - pub index: Option, -} +pub struct Color(pub u8, pub u8, pub u8, pub u8); -/// describes a CLI configuration -/// -/// See more: https://tauri.app/v1/api/config#cliconfig +/// The window effects configuration object #[skip_serializing_none] -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default)] #[cfg_attr(feature = "schema", derive(JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct CliConfig { - /// Command description which will be shown on the help information. - pub description: Option, - /// Command long description which will be shown on the help information. - #[serde(alias = "long-description")] - pub long_description: Option, - /// Adds additional help information to be displayed in addition to auto-generated help. - /// This information is displayed before the auto-generated help information. - /// This is often used for header information. - #[serde(alias = "before-help")] - pub before_help: Option, - /// Adds additional help information to be displayed in addition to auto-generated help. - /// This information is displayed after the auto-generated help information. - /// This is often used to describe how to use the arguments, or caveats to be noted. - #[serde(alias = "after-help")] - pub after_help: Option, - /// List of arguments for the command - pub args: Option>, - /// List of subcommands of this command - pub subcommands: Option>, -} - -impl CliConfig { - /// List of arguments for the command - pub fn args(&self) -> Option<&Vec> { - self.args.as_ref() - } - - /// List of subcommands of this command - pub fn subcommands(&self) -> Option<&HashMap> { - self.subcommands.as_ref() - } - - /// Command description which will be shown on the help information. - pub fn description(&self) -> Option<&String> { - self.description.as_ref() - } - - /// Command long description which will be shown on the help information. - pub fn long_description(&self) -> Option<&String> { - self.description.as_ref() - } - - /// Adds additional help information to be displayed in addition to auto-generated help. - /// This information is displayed before the auto-generated help information. - /// This is often used for header information. - pub fn before_help(&self) -> Option<&String> { - self.before_help.as_ref() - } - - /// Adds additional help information to be displayed in addition to auto-generated help. - /// This information is displayed after the auto-generated help information. - /// This is often used to describe how to use the arguments, or caveats to be noted. - pub fn after_help(&self) -> Option<&String> { - self.after_help.as_ref() - } +pub struct WindowEffectsConfig { + /// List of Window effects to apply to the Window. + /// Conflicting effects will apply the first one and ignore the rest. + pub effects: Vec, + /// Window effect state **macOS Only** + pub state: Option, + /// Window effect corner radius **macOS Only** + pub radius: Option, + /// Window effect color. Affects [`WindowEffect::Blur`] and [`WindowEffect::Acrylic`] only + /// on Windows 10 v1903+. Doesn't have any effect on Windows 7 or Windows 11. + pub color: Option, } /// The window configuration object. @@ -981,6 +981,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, + /// Whether the window should be visible on all workspaces or virtual desktops. + #[serde(default, alias = "all-workspaces")] + pub visible_on_all_workspaces: bool, /// Prevents the window contents from being captured by other apps. #[serde(default, alias = "content-protected")] pub content_protected: bool, @@ -1010,6 +1013,34 @@ pub struct WindowConfig { /// 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, + /// Whether or not the window has shadow. + /// + /// ## Platform-specific + /// + /// - **Windows:** + /// - `false` has no effect on decorated window, shadow are always ON. + /// - `true` will make ndecorated window have a 1px white border, + /// and on Windows 11, it will have a rounded corners. + /// - **Linux:** Unsupported. + #[serde(default = "default_true")] + pub shadow: bool, + /// Window effects. + /// + /// Requires the window to be transparent. + /// + /// ## Platform-specific: + /// + /// - **Windows**: If using decorations or shadows, you may want to try this workaround https://github.com/tauri-apps/tao/issues/72#issuecomment-975607891 + /// - **Linux**: Unsupported + #[serde(default, alias = "window-effects")] + pub window_effects: Option, + /// Whether or not the webview should be launched in incognito mode. + /// + /// ## Platform-specific: + /// + /// - **Android**: Unsupported. + #[serde(default)] + pub incognito: bool, } impl Default for WindowConfig { @@ -1040,6 +1071,7 @@ impl Default for WindowConfig { visible: true, decorations: true, always_on_top: false, + visible_on_all_workspaces: false, content_protected: false, skip_taskbar: false, theme: None, @@ -1048,6 +1080,9 @@ impl Default for WindowConfig { accept_first_mouse: false, tabbing_identifier: None, additional_browser_args: None, + shadow: true, + window_effects: None, + incognito: false, } } } @@ -1225,81 +1260,9 @@ pub struct RemoteDomainAccessScope { /// The list of plugins that are allowed in this scope. #[serde(default)] pub plugins: Vec, - /// Enables access to the Tauri API. - #[serde(default, rename = "enableTauriAPI", alias = "enable-tauri-api")] - pub enable_tauri_api: bool, -} - -/// Security configuration. -/// -/// See more: https://tauri.app/v1/api/config#securityconfig -#[skip_serializing_none] -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct SecurityConfig { - /// The Content Security Policy that will be injected on all HTML files on the built application. - /// If [`dev_csp`](#SecurityConfig.devCsp) is not specified, this value is also injected on dev. - /// - /// This is a really important part of the configuration since it helps you ensure your WebView is secured. - /// See . - pub csp: Option, - /// The Content Security Policy that will be injected on all HTML files on development. - /// - /// This is a really important part of the configuration since it helps you ensure your WebView is secured. - /// See . - #[serde(alias = "dev-csp")] - pub dev_csp: Option, - /// Freeze the `Object.prototype` when using the custom protocol. - #[serde(default, alias = "freeze-prototype")] - pub freeze_prototype: bool, - /// Disables the Tauri-injected CSP sources. - /// - /// At compile time, Tauri parses all the frontend assets and changes the Content-Security-Policy - /// to only allow loading of your own scripts and styles by injecting nonce and hash sources. - /// This stricts your CSP, which may introduce issues when using along with other flexing sources. - /// - /// This configuration option allows both a boolean and a list of strings as value. - /// A boolean instructs Tauri to disable the injection for all CSP injections, - /// and a list of strings indicates the CSP directives that Tauri cannot inject. - /// - /// **WARNING:** Only disable this if you know what you are doing and have properly configured the CSP. - /// Your application might be vulnerable to XSS attacks without this Tauri protection. - #[serde(default, alias = "dangerous-disable-asset-csp-modification")] - pub dangerous_disable_asset_csp_modification: DisabledCspModificationKind, - /// Allow external domains to send command to Tauri. - /// - /// By default, external domains do not have access to `window.__TAURI__`, which means they cannot - /// communicate with the commands defined in Rust. This prevents attacks where an externally - /// loaded malicious or compromised sites could start executing commands on the user's device. - /// - /// This configuration allows a set of external domains to have access to the Tauri commands. - /// When you configure a domain to be allowed to access the IPC, all subpaths are allowed. Subdomains are not allowed. - /// - /// **WARNING:** Only use this option if you either have internal checks against malicious - /// external sites or you can trust the allowed external sites. You application might be - /// vulnerable to dangerous Tauri command related attacks otherwise. - #[serde(default, alias = "dangerous-remote-domain-ipc-access")] - pub dangerous_remote_domain_ipc_access: Vec, -} - -/// Defines an allowlist type. -pub trait Allowlist { - /// Returns all features associated with the allowlist struct. - fn all_features() -> Vec<&'static str>; - /// Returns the tauri features enabled on this allowlist. - fn to_features(&self) -> Vec<&'static str>; -} - -macro_rules! check_feature { - ($self:ident, $features:ident, $flag:ident, $feature_name: expr) => { - if $self.$flag { - $features.push($feature_name) - } - }; } -/// Filesystem scope definition. +/// Protocol scope definition. /// It is a list of glob patterns that restrict the API access from the webview. /// /// Each pattern can start with a variable that resolves to a system base directory. @@ -1310,7 +1273,7 @@ macro_rules! check_feature { #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(untagged)] #[cfg_attr(feature = "schema", derive(JsonSchema))] -pub enum FsAllowlistScope { +pub enum FsScope { /// A list of paths that are allowed by this scope. AllowedPaths(Vec), /// A complete scope configuration. @@ -1336,13 +1299,13 @@ pub enum FsAllowlistScope { }, } -impl Default for FsAllowlistScope { +impl Default for FsScope { fn default() -> Self { Self::AllowedPaths(Vec::new()) } } -impl FsAllowlistScope { +impl FsScope { /// The list of allowed paths. pub fn allowed_paths(&self) -> &Vec { match self { @@ -1360,1019 +1323,75 @@ impl FsAllowlistScope { } } -/// Allowlist for the file system APIs. +/// Config for the asset custom protocol. /// -/// See more: https://tauri.app/v1/api/config#fsallowlistconfig +/// See more: https://tauri.app/v1/api/config#assetprotocolconfig #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct FsAllowlistConfig { - /// The access scope for the filesystem APIs. - #[serde(default)] - pub scope: FsAllowlistScope, - /// Use this flag to enable all file system API features. +pub struct AssetProtocolConfig { + /// The access scope for the asset protocol. #[serde(default)] - pub all: bool, - /// Read file from local filesystem. - #[serde(default, alias = "read-file")] - pub read_file: bool, - /// Write file to local filesystem. - #[serde(default, alias = "write-file")] - pub write_file: bool, - /// Read directory from local filesystem. - #[serde(default, alias = "read-dir")] - pub read_dir: bool, - /// Copy file from local filesystem. - #[serde(default, alias = "copy-file")] - pub copy_file: bool, - /// Create directory from local filesystem. - #[serde(default, alias = "create-dir")] - pub create_dir: bool, - /// Remove directory from local filesystem. - #[serde(default, alias = "remove-dir")] - pub remove_dir: bool, - /// Remove file from local filesystem. - #[serde(default, alias = "remove-file")] - pub remove_file: bool, - /// Rename file from local filesystem. - #[serde(default, alias = "rename-file")] - pub rename_file: bool, - /// Check if path exists on the local filesystem. + pub scope: FsScope, + /// Enables the asset protocol. #[serde(default)] - pub exists: bool, -} - -impl Allowlist for FsAllowlistConfig { - fn all_features() -> Vec<&'static str> { - let allowlist = Self { - scope: Default::default(), - all: false, - read_file: true, - write_file: true, - read_dir: true, - copy_file: true, - create_dir: true, - remove_dir: true, - remove_file: true, - rename_file: true, - exists: true, - }; - let mut features = allowlist.to_features(); - features.push("fs-all"); - features - } - - fn to_features(&self) -> Vec<&'static str> { - if self.all { - vec!["fs-all"] - } else { - let mut features = Vec::new(); - check_feature!(self, features, read_file, "fs-read-file"); - check_feature!(self, features, write_file, "fs-write-file"); - check_feature!(self, features, read_dir, "fs-read-dir"); - check_feature!(self, features, copy_file, "fs-copy-file"); - check_feature!(self, features, create_dir, "fs-create-dir"); - check_feature!(self, features, remove_dir, "fs-remove-dir"); - check_feature!(self, features, remove_file, "fs-remove-file"); - check_feature!(self, features, rename_file, "fs-rename-file"); - check_feature!(self, features, exists, "fs-exists"); - features - } - } + pub enable: bool, } -/// Allowlist for the window APIs. +/// Security configuration. /// -/// See more: https://tauri.app/v1/api/config#windowallowlistconfig +/// See more: https://tauri.app/v1/api/config#securityconfig +#[skip_serializing_none] #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct WindowAllowlistConfig { - /// Use this flag to enable all window API features. - #[serde(default)] - pub all: bool, - /// Allows dynamic window creation. - #[serde(default)] - pub create: bool, - /// Allows centering the window. - #[serde(default)] - pub center: bool, - /// Allows requesting user attention on the window. - #[serde(default, alias = "request-user-attention")] - pub request_user_attention: bool, - /// Allows setting the resizable flag of the window. - #[serde(default, alias = "set-resizable")] - pub set_resizable: bool, - /// Allows setting whether the window's native maximize button is enabled or not. - #[serde(default, alias = "set-maximizable")] - pub set_maximizable: bool, - /// Allows setting whether the window's native minimize button is enabled or not. - #[serde(default, alias = "set-minimizable")] - pub set_minimizable: bool, - /// Allows setting whether the window's native close button is enabled or not. - #[serde(default, alias = "set-closable")] - pub set_closable: bool, - /// Allows changing the window title. - #[serde(default, alias = "set-title")] - pub set_title: bool, - /// Allows maximizing the window. - #[serde(default)] - pub maximize: bool, - /// Allows unmaximizing the window. - #[serde(default)] - pub unmaximize: bool, - /// Allows minimizing the window. - #[serde(default)] - pub minimize: bool, - /// Allows unminimizing the window. - #[serde(default)] - pub unminimize: bool, - /// Allows showing the window. - #[serde(default)] - pub show: bool, - /// Allows hiding the window. - #[serde(default)] - pub hide: bool, - /// Allows closing the window. - #[serde(default)] - pub close: bool, - /// Allows setting the decorations flag of the window. - #[serde(default, alias = "set-decorations")] - pub set_decorations: bool, - /// 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, - /// Allows setting the window minimum size. - #[serde(default, alias = "set-min-size")] - pub set_min_size: bool, - /// Allows setting the window maximum size. - #[serde(default, alias = "set-max-size")] - pub set_max_size: bool, - /// Allows changing the position of the window. - #[serde(default, alias = "set-position")] - pub set_position: bool, - /// Allows setting the fullscreen flag of the window. - #[serde(default, alias = "set-fullscreen")] - pub set_fullscreen: bool, - /// Allows focusing the window. - #[serde(default, alias = "set-focus")] - pub set_focus: bool, - /// Allows changing the window icon. - #[serde(default, alias = "set-icon")] - pub set_icon: bool, - /// Allows setting the skip_taskbar flag of the window. - #[serde(default, alias = "set-skip-taskbar")] - pub set_skip_taskbar: bool, - /// Allows grabbing the cursor. - #[serde(default, alias = "set-cursor-grab")] - pub set_cursor_grab: bool, - /// Allows setting the cursor visibility. - #[serde(default, alias = "set-cursor-visible")] - pub set_cursor_visible: bool, - /// Allows changing the cursor icon. - #[serde(default, alias = "set-cursor-icon")] - pub set_cursor_icon: bool, - /// Allows setting the cursor position. - #[serde(default, alias = "set-cursor-position")] - pub set_cursor_position: bool, - /// Allows ignoring cursor events. - #[serde(default, alias = "set-ignore-cursor-events")] - pub set_ignore_cursor_events: bool, - /// Allows start dragging on the window. - #[serde(default, alias = "start-dragging")] - pub start_dragging: bool, - /// Allows opening the system dialog to print the window content. - #[serde(default)] - pub print: bool, -} - -impl Allowlist for WindowAllowlistConfig { - fn all_features() -> Vec<&'static str> { - let allowlist = Self { - all: false, - create: true, - center: true, - request_user_attention: true, - set_resizable: true, - set_maximizable: true, - set_minimizable: true, - set_closable: true, - set_title: true, - maximize: true, - unmaximize: true, - minimize: true, - unminimize: true, - show: true, - hide: true, - 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, - set_position: true, - set_fullscreen: true, - set_focus: true, - set_icon: true, - set_skip_taskbar: true, - set_cursor_grab: true, - set_cursor_visible: true, - set_cursor_icon: true, - set_cursor_position: true, - set_ignore_cursor_events: true, - start_dragging: true, - print: true, - }; - let mut features = allowlist.to_features(); - features.push("window-all"); - features - } - - fn to_features(&self) -> Vec<&'static str> { - if self.all { - vec!["window-all"] - } else { - let mut features = Vec::new(); - check_feature!(self, features, create, "window-create"); - check_feature!(self, features, center, "window-center"); - check_feature!( - self, - features, - request_user_attention, - "window-request-user-attention" - ); - check_feature!(self, features, set_resizable, "window-set-resizable"); - check_feature!(self, features, set_maximizable, "window-set-maximizable"); - check_feature!(self, features, set_minimizable, "window-set-minimizable"); - check_feature!(self, features, set_closable, "window-set-closable"); - check_feature!(self, features, set_title, "window-set-title"); - check_feature!(self, features, maximize, "window-maximize"); - check_feature!(self, features, unmaximize, "window-unmaximize"); - check_feature!(self, features, minimize, "window-minimize"); - check_feature!(self, features, unminimize, "window-unminimize"); - check_feature!(self, features, show, "window-show"); - check_feature!(self, features, hide, "window-hide"); - check_feature!(self, features, close, "window-close"); - check_feature!(self, features, set_decorations, "window-set-decorations"); - check_feature!( - self, - features, - 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"); - check_feature!(self, features, set_position, "window-set-position"); - check_feature!(self, features, set_fullscreen, "window-set-fullscreen"); - check_feature!(self, features, set_focus, "window-set-focus"); - check_feature!(self, features, set_icon, "window-set-icon"); - check_feature!(self, features, set_skip_taskbar, "window-set-skip-taskbar"); - check_feature!(self, features, set_cursor_grab, "window-set-cursor-grab"); - check_feature!( - self, - features, - set_cursor_visible, - "window-set-cursor-visible" - ); - check_feature!(self, features, set_cursor_icon, "window-set-cursor-icon"); - check_feature!( - self, - features, - set_cursor_position, - "window-set-cursor-position" - ); - check_feature!( - self, - features, - set_ignore_cursor_events, - "window-set-ignore-cursor-events" - ); - check_feature!(self, features, start_dragging, "window-start-dragging"); - check_feature!(self, features, print, "window-print"); - features - } - } -} - -/// A command allowed to be executed by the webview API. -#[derive(Debug, PartialEq, Eq, Clone, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -pub struct ShellAllowedCommand { - /// The name for this allowed shell command configuration. +pub struct SecurityConfig { + /// The Content Security Policy that will be injected on all HTML files on the built application. + /// If [`dev_csp`](#SecurityConfig.devCsp) is not specified, this value is also injected on dev. /// - /// This name will be used inside of the webview API to call this command along with - /// any specified arguments. - pub name: String, - - /// The command name. - /// It can start with a variable that resolves to a system base directory. - /// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, - /// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, - /// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, - /// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`. - #[serde(rename = "cmd", default)] // use default just so the schema doesn't flag it as required - pub command: PathBuf, - - /// The allowed arguments for the command execution. - #[serde(default)] - pub args: ShellAllowedArgs, - - /// If this command is a sidecar command. - #[serde(default)] - pub sidecar: bool, -} - -impl<'de> Deserialize<'de> for ShellAllowedCommand { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - struct InnerShellAllowedCommand { - name: String, - #[serde(rename = "cmd")] - command: Option, - #[serde(default)] - args: ShellAllowedArgs, - #[serde(default)] - sidecar: bool, - } - - let config = InnerShellAllowedCommand::deserialize(deserializer)?; - - if !config.sidecar && config.command.is_none() { - return Err(DeError::custom( - "The shell scope `command` value is required.", - )); - } - - Ok(ShellAllowedCommand { - name: config.name, - command: config.command.unwrap_or_default(), - args: config.args, - sidecar: config.sidecar, - }) - } -} - -/// A set of command arguments allowed to be executed by the webview API. -/// -/// A value of `true` will allow any arguments to be passed to the command. `false` will disable all -/// arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to -/// be passed to the attached command configuration. -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(untagged, deny_unknown_fields)] -#[non_exhaustive] -pub enum ShellAllowedArgs { - /// Use a simple boolean to allow all or disable all arguments to this command configuration. - Flag(bool), - - /// A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration. - List(Vec), -} - -impl Default for ShellAllowedArgs { - fn default() -> Self { - Self::Flag(false) - } -} - -/// A command argument allowed to be executed by the webview API. -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(untagged, deny_unknown_fields)] -#[non_exhaustive] -pub enum ShellAllowedArg { - /// A non-configurable argument that is passed to the command in the order it was specified. - Fixed(String), - - /// A variable that is set while calling the command from the webview API. + /// This is a really important part of the configuration since it helps you ensure your WebView is secured. + /// See . + pub csp: Option, + /// The Content Security Policy that will be injected on all HTML files on development. /// - Var { - /// [regex] validator to require passed values to conform to an expected input. - /// - /// This will require the argument value passed to this variable to match the `validator` regex - /// before it will be executed. - /// - /// [regex]: https://docs.rs/regex/latest/regex/#syntax - validator: String, - }, -} - -/// Shell scope definition. -/// It is a list of command names and associated CLI arguments that restrict the API access from the webview. -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -pub struct ShellAllowlistScope(pub Vec); - -/// Defines the `shell > open` api scope. -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(untagged, deny_unknown_fields)] -#[non_exhaustive] -pub enum ShellAllowlistOpen { - /// If the shell open API should be enabled. + /// This is a really important part of the configuration since it helps you ensure your WebView is secured. + /// See . + #[serde(alias = "dev-csp")] + pub dev_csp: Option, + /// Freeze the `Object.prototype` when using the custom protocol. + #[serde(default, alias = "freeze-prototype")] + pub freeze_prototype: bool, + /// Disables the Tauri-injected CSP sources. /// - /// 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. + /// At compile time, Tauri parses all the frontend assets and changes the Content-Security-Policy + /// to only allow loading of your own scripts and styles by injecting nonce and hash sources. + /// This stricts your CSP, which may introduce issues when using along with other flexing sources. /// - /// If using a custom regex to support a non-http(s) schema, care should be used to prevent values - /// that allow flag-like strings to pass validation. e.g. `--enable-debugging`, `-i`, `/R`. - Validate(String), -} - -impl Default for ShellAllowlistOpen { - fn default() -> Self { - Self::Flag(false) - } -} - -/// Allowlist for the shell APIs. -/// -/// See more: https://tauri.app/v1/api/config#shellallowlistconfig -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ShellAllowlistConfig { - /// Access scope for the binary execution APIs. - /// Sidecars are automatically enabled. - #[serde(default)] - pub scope: ShellAllowlistScope, - /// Use this flag to enable all shell API features. - #[serde(default)] - pub all: bool, - /// Enable binary execution. - #[serde(default)] - pub execute: bool, - /// Enable sidecar execution, allowing the JavaScript layer to spawn a sidecar command, - /// an executable that is shipped with the application. - /// For more information see . - #[serde(default)] - pub sidecar: bool, - /// Open URL with the user's default application. - #[serde(default)] - pub open: ShellAllowlistOpen, -} - -impl Allowlist for ShellAllowlistConfig { - fn all_features() -> Vec<&'static str> { - let allowlist = Self { - scope: Default::default(), - all: false, - execute: true, - sidecar: true, - open: ShellAllowlistOpen::Flag(true), - }; - let mut features = allowlist.to_features(); - features.push("shell-all"); - features - } - - fn to_features(&self) -> Vec<&'static str> { - if self.all { - vec!["shell-all"] - } else { - let mut features = Vec::new(); - check_feature!(self, features, execute, "shell-execute"); - check_feature!(self, features, sidecar, "shell-sidecar"); - - if !matches!(self.open, ShellAllowlistOpen::Flag(false)) { - features.push("shell-open") - } - - features - } - } -} - -/// Allowlist for the dialog APIs. -/// -/// See more: https://tauri.app/v1/api/config#dialogallowlistconfig -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct DialogAllowlistConfig { - /// Use this flag to enable all dialog API features. - #[serde(default)] - pub all: bool, - /// Allows the API to open a dialog window to pick files. - #[serde(default)] - pub open: bool, - /// Allows the API to open a dialog window to pick where to save files. - #[serde(default)] - pub save: bool, - /// Allows the API to show a message dialog window. - #[serde(default)] - pub message: bool, - /// Allows the API to show a dialog window with Yes/No buttons. - #[serde(default)] - pub ask: bool, - /// Allows the API to show a dialog window with Ok/Cancel buttons. - #[serde(default)] - pub confirm: bool, -} - -impl Allowlist for DialogAllowlistConfig { - fn all_features() -> Vec<&'static str> { - let allowlist = Self { - all: false, - open: true, - save: true, - message: true, - ask: true, - confirm: true, - }; - let mut features = allowlist.to_features(); - features.push("dialog-all"); - features - } - - fn to_features(&self) -> Vec<&'static str> { - if self.all { - vec!["dialog-all"] - } else { - let mut features = Vec::new(); - check_feature!(self, features, open, "dialog-open"); - check_feature!(self, features, save, "dialog-save"); - check_feature!(self, features, message, "dialog-message"); - check_feature!(self, features, ask, "dialog-ask"); - check_feature!(self, features, confirm, "dialog-confirm"); - features - } - } -} - -/// HTTP API scope definition. -/// It is a list of URLs that can be accessed by the webview when using the HTTP APIs. -/// The scoped URL is matched against the request URL using a glob pattern. -/// -/// Examples: -/// - "https://*": allows all HTTPS urls -/// - "https://*.github.com/tauri-apps/tauri": allows any subdomain of "github.com" with the "tauri-apps/api" path -/// - "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/" -#[allow(rustdoc::bare_urls)] -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -// TODO: in v2, parse into a String or a custom type that perserves the -// glob string because Url type will add a trailing slash -#[cfg_attr(feature = "schema", derive(JsonSchema))] -pub struct HttpAllowlistScope(pub Vec); - -/// Allowlist for the HTTP APIs. -/// -/// See more: https://tauri.app/v1/api/config#httpallowlistconfig -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct HttpAllowlistConfig { - /// The access scope for the HTTP APIs. - #[serde(default)] - pub scope: HttpAllowlistScope, - /// Use this flag to enable all HTTP API features. - #[serde(default)] - pub all: bool, - /// Allows making HTTP requests. - #[serde(default)] - pub request: bool, -} - -impl Allowlist for HttpAllowlistConfig { - fn all_features() -> Vec<&'static str> { - let allowlist = Self { - scope: Default::default(), - all: false, - request: true, - }; - let mut features = allowlist.to_features(); - features.push("http-all"); - features - } - - fn to_features(&self) -> Vec<&'static str> { - if self.all { - vec!["http-all"] - } else { - let mut features = Vec::new(); - check_feature!(self, features, request, "http-request"); - features - } - } -} - -/// Allowlist for the notification APIs. -/// -/// See more: https://tauri.app/v1/api/config#notificationallowlistconfig -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct NotificationAllowlistConfig { - /// Use this flag to enable all notification API features. - #[serde(default)] - pub all: bool, -} - -impl Allowlist for NotificationAllowlistConfig { - fn all_features() -> Vec<&'static str> { - let allowlist = Self { all: false }; - let mut features = allowlist.to_features(); - features.push("notification-all"); - features - } - - fn to_features(&self) -> Vec<&'static str> { - if self.all { - vec!["notification-all"] - } else { - vec![] - } - } -} - -/// Allowlist for the global shortcut APIs. -/// -/// See more: https://tauri.app/v1/api/config#globalshortcutallowlistconfig -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GlobalShortcutAllowlistConfig { - /// Use this flag to enable all global shortcut API features. - #[serde(default)] - pub all: bool, -} - -impl Allowlist for GlobalShortcutAllowlistConfig { - fn all_features() -> Vec<&'static str> { - let allowlist = Self { all: false }; - let mut features = allowlist.to_features(); - features.push("global-shortcut-all"); - features - } - - fn to_features(&self) -> Vec<&'static str> { - if self.all { - vec!["global-shortcut-all"] - } else { - vec![] - } - } -} - -/// Allowlist for the OS APIs. -/// -/// See more: https://tauri.app/v1/api/config#osallowlistconfig -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct OsAllowlistConfig { - /// Use this flag to enable all OS API features. - #[serde(default)] - pub all: bool, -} - -impl Allowlist for OsAllowlistConfig { - fn all_features() -> Vec<&'static str> { - let allowlist = Self { all: false }; - let mut features = allowlist.to_features(); - features.push("os-all"); - features - } - - fn to_features(&self) -> Vec<&'static str> { - if self.all { - vec!["os-all"] - } else { - vec![] - } - } -} - -/// Allowlist for the path APIs. -/// -/// See more: https://tauri.app/v1/api/config#pathallowlistconfig -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct PathAllowlistConfig { - /// Use this flag to enable all path API features. - #[serde(default)] - pub all: bool, -} - -impl Allowlist for PathAllowlistConfig { - fn all_features() -> Vec<&'static str> { - let allowlist = Self { all: false }; - let mut features = allowlist.to_features(); - features.push("path-all"); - features - } - - fn to_features(&self) -> Vec<&'static str> { - if self.all { - vec!["path-all"] - } else { - vec![] - } - } -} - -/// Allowlist for the custom protocols. -/// -/// See more: https://tauri.app/v1/api/config#protocolallowlistconfig -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ProtocolAllowlistConfig { - /// The access scope for the asset protocol. - #[serde(default, alias = "asset-scope")] - pub asset_scope: FsAllowlistScope, - /// Use this flag to enable all custom protocols. - #[serde(default)] - pub all: bool, - /// Enables the asset protocol. - #[serde(default)] - pub asset: bool, -} - -impl Allowlist for ProtocolAllowlistConfig { - fn all_features() -> Vec<&'static str> { - let allowlist = Self { - asset_scope: Default::default(), - all: false, - asset: true, - }; - let mut features = allowlist.to_features(); - features.push("protocol-all"); - features - } - - fn to_features(&self) -> Vec<&'static str> { - if self.all { - vec!["protocol-all"] - } else { - let mut features = Vec::new(); - check_feature!(self, features, asset, "protocol-asset"); - features - } - } -} - -/// Allowlist for the process APIs. -/// -/// See more: https://tauri.app/v1/api/config#processallowlistconfig -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ProcessAllowlistConfig { - /// Use this flag to enable all process APIs. - #[serde(default)] - pub all: bool, - /// Enables the relaunch API. - #[serde(default)] - pub relaunch: bool, - /// Dangerous option that allows macOS to relaunch even if the binary contains a symlink. + /// This configuration option allows both a boolean and a list of strings as value. + /// A boolean instructs Tauri to disable the injection for all CSP injections, + /// and a list of strings indicates the CSP directives that Tauri cannot inject. /// - /// This is due to macOS having less symlink protection. Highly recommended to not set this flag - /// unless you have a very specific reason too, and understand the implications of it. - #[serde( - default, - alias = "relaunchDangerousAllowSymlinkMacOS", - alias = "relaunch-dangerous-allow-symlink-macos" - )] - pub relaunch_dangerous_allow_symlink_macos: bool, - /// Enables the exit API. - #[serde(default)] - pub exit: bool, -} - -impl Allowlist for ProcessAllowlistConfig { - fn all_features() -> Vec<&'static str> { - let allowlist = Self { - all: false, - relaunch: true, - relaunch_dangerous_allow_symlink_macos: false, - exit: true, - }; - let mut features = allowlist.to_features(); - features.push("process-all"); - features - } - - fn to_features(&self) -> Vec<&'static str> { - if self.all { - vec!["process-all"] - } else { - let mut features = Vec::new(); - check_feature!(self, features, relaunch, "process-relaunch"); - check_feature!( - self, - features, - relaunch_dangerous_allow_symlink_macos, - "process-relaunch-dangerous-allow-symlink-macos" - ); - check_feature!(self, features, exit, "process-exit"); - features - } - } -} - -/// Allowlist for the clipboard APIs. -/// -/// See more: https://tauri.app/v1/api/config#clipboardallowlistconfig -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ClipboardAllowlistConfig { - /// Use this flag to enable all clipboard APIs. - #[serde(default)] - pub all: bool, - /// Enables the clipboard's `writeText` API. - #[serde(default, alias = "writeText")] - pub write_text: bool, - /// Enables the clipboard's `readText` API. - #[serde(default, alias = "readText")] - pub read_text: bool, -} - -impl Allowlist for ClipboardAllowlistConfig { - fn all_features() -> Vec<&'static str> { - let allowlist = Self { - all: false, - write_text: true, - read_text: true, - }; - let mut features = allowlist.to_features(); - features.push("clipboard-all"); - features - } - - fn to_features(&self) -> Vec<&'static str> { - if self.all { - vec!["clipboard-all"] - } else { - let mut features = Vec::new(); - check_feature!(self, features, write_text, "clipboard-write-text"); - check_feature!(self, features, read_text, "clipboard-read-text"); - features - } - } -} - -/// Allowlist for the app APIs. -/// -/// See more: https://tauri.app/v1/api/config#appallowlistconfig -#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct AppAllowlistConfig { - /// Use this flag to enable all app APIs. - #[serde(default)] - pub all: bool, - /// Enables the app's `show` API. - #[serde(default)] - pub show: bool, - /// Enables the app's `hide` API. - #[serde(default)] - pub hide: bool, -} - -impl Allowlist for AppAllowlistConfig { - fn all_features() -> Vec<&'static str> { - let allowlist = Self { - all: false, - show: true, - hide: true, - }; - let mut features = allowlist.to_features(); - features.push("app-all"); - features - } - - fn to_features(&self) -> Vec<&'static str> { - if self.all { - vec!["app-all"] - } else { - let mut features = Vec::new(); - check_feature!(self, features, show, "app-show"); - check_feature!(self, features, hide, "app-hide"); - features - } - } -} - -/// 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)] -pub struct AllowlistConfig { - /// Use this flag to enable all API features. - #[serde(default)] - pub all: bool, - /// File system API allowlist. - #[serde(default)] - pub fs: FsAllowlistConfig, - /// Window API allowlist. - #[serde(default)] - pub window: WindowAllowlistConfig, - /// Shell API allowlist. - #[serde(default)] - pub shell: ShellAllowlistConfig, - /// Dialog API allowlist. - #[serde(default)] - pub dialog: DialogAllowlistConfig, - /// HTTP API allowlist. - #[serde(default)] - pub http: HttpAllowlistConfig, - /// Notification API allowlist. - #[serde(default)] - pub notification: NotificationAllowlistConfig, - /// Global shortcut API allowlist. - #[serde(default, alias = "global-shortcut")] - pub global_shortcut: GlobalShortcutAllowlistConfig, - /// OS allowlist. - #[serde(default)] - pub os: OsAllowlistConfig, - /// Path API allowlist. - #[serde(default)] - pub path: PathAllowlistConfig, - /// Custom protocol allowlist. - #[serde(default)] - pub protocol: ProtocolAllowlistConfig, - /// Process API allowlist. - #[serde(default)] - pub process: ProcessAllowlistConfig, - /// Clipboard APIs allowlist. - #[serde(default)] - pub clipboard: ClipboardAllowlistConfig, - /// App APIs allowlist. - #[serde(default)] - pub app: AppAllowlistConfig, -} - -impl Allowlist for AllowlistConfig { - fn all_features() -> Vec<&'static str> { - let mut features = vec!["api-all"]; - features.extend(FsAllowlistConfig::all_features()); - features.extend(WindowAllowlistConfig::all_features()); - features.extend(ShellAllowlistConfig::all_features()); - features.extend(DialogAllowlistConfig::all_features()); - features.extend(HttpAllowlistConfig::all_features()); - features.extend(NotificationAllowlistConfig::all_features()); - features.extend(GlobalShortcutAllowlistConfig::all_features()); - features.extend(OsAllowlistConfig::all_features()); - features.extend(PathAllowlistConfig::all_features()); - features.extend(ProtocolAllowlistConfig::all_features()); - features.extend(ProcessAllowlistConfig::all_features()); - features.extend(ClipboardAllowlistConfig::all_features()); - features.extend(AppAllowlistConfig::all_features()); - features - } - - fn to_features(&self) -> Vec<&'static str> { - if self.all { - vec!["api-all"] - } else { - let mut features = Vec::new(); - features.extend(self.fs.to_features()); - features.extend(self.window.to_features()); - features.extend(self.shell.to_features()); - features.extend(self.dialog.to_features()); - features.extend(self.http.to_features()); - features.extend(self.notification.to_features()); - features.extend(self.global_shortcut.to_features()); - features.extend(self.os.to_features()); - features.extend(self.path.to_features()); - features.extend(self.protocol.to_features()); - features.extend(self.process.to_features()); - features.extend(self.clipboard.to_features()); - features.extend(self.app.to_features()); - features - } - } + /// **WARNING:** Only disable this if you know what you are doing and have properly configured the CSP. + /// Your application might be vulnerable to XSS attacks without this Tauri protection. + #[serde(default, alias = "dangerous-disable-asset-csp-modification")] + pub dangerous_disable_asset_csp_modification: DisabledCspModificationKind, + /// Allow external domains to send command to Tauri. + /// + /// By default, external domains do not have access to `window.__TAURI__`, which means they cannot + /// communicate with the commands defined in Rust. This prevents attacks where an externally + /// loaded malicious or compromised sites could start executing commands on the user's device. + /// + /// This configuration allows a set of external domains to have access to the Tauri commands. + /// When you configure a domain to be allowed to access the IPC, all subpaths are allowed. Subdomains are not allowed. + /// + /// **WARNING:** Only use this option if you either have internal checks against malicious + /// external sites or you can trust the allowed external sites. You application might be + /// vulnerable to dangerous Tauri command related attacks otherwise. + #[serde(default, alias = "dangerous-remote-domain-ipc-access")] + pub dangerous_remote_domain_ipc_access: Vec, + /// Custom protocol config. + #[serde(default, alias = "asset-protocol")] + pub asset_protocol: AssetProtocolConfig, } /// The application pattern. @@ -2410,23 +1429,15 @@ pub struct TauriConfig { /// The windows configuration. #[serde(default)] pub windows: Vec, - /// The CLI configuration. - pub cli: Option, /// The bundler configuration. #[serde(default)] pub bundle: BundleConfig, - /// The allowlist configuration. - #[serde(default)] - pub allowlist: AllowlistConfig, /// Security configuration. #[serde(default)] pub security: SecurityConfig, - /// The updater configuration. - #[serde(default)] - pub updater: UpdaterConfig, - /// Configuration for app system tray. - #[serde(alias = "system-tray")] - pub system_tray: Option, + /// Configuration for app tray icon. + #[serde(alias = "tray-icon")] + pub tray_icon: Option, /// MacOS private API configuration. Enables the transparent background API and sets the `fullScreenEnabled` preference to `true`. #[serde(rename = "macOSPrivateApi", alias = "macos-private-api", default)] pub macos_private_api: bool, @@ -2435,28 +1446,19 @@ pub struct TauriConfig { impl TauriConfig { /// Returns all Cargo features. pub fn all_features() -> Vec<&'static str> { - let mut features = AllowlistConfig::all_features(); - features.extend(vec![ - "cli", - "updater", - "system-tray", + vec![ + "tray-icon", "macos-private-api", "isolation", - ]); - features + "protocol-asset", + ] } /// Returns the enabled Cargo features. pub fn features(&self) -> Vec<&str> { - let mut features = self.allowlist.to_features(); - if self.cli.is_some() { - features.push("cli"); - } - if self.updater.active { - features.push("updater"); - } - if self.system_tray.is_some() { - features.push("system-tray"); + let mut features = Vec::new(); + if self.tray_icon.is_some() { + features.push("tray-icon"); } if self.macos_private_api { features.push("macos-private-api"); @@ -2464,43 +1466,14 @@ impl TauriConfig { if let PatternKind::Isolation { .. } = self.pattern { features.push("isolation"); } + if self.security.asset_protocol.enable { + features.push("protocol-asset"); + } features.sort_unstable(); features } } -/// A URL to an updater server. -/// -/// The URL must use the `https` scheme on production. -#[skip_serializing_none] -#[derive(Debug, PartialEq, Eq, Clone, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -pub struct UpdaterEndpoint(pub Url); - -impl std::fmt::Display for UpdaterEndpoint { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl<'de> Deserialize<'de> for UpdaterEndpoint { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let url = Url::deserialize(deserializer)?; - #[cfg(all(not(debug_assertions), not(feature = "schema")))] - { - if url.scheme() != "https" { - return Err(serde::de::Error::custom( - "The configured updater endpoint must use the `https` protocol.", - )); - } - } - Ok(Self(url)) - } -} - /// Install modes for the Windows update. #[derive(Debug, PartialEq, Eq, Clone)] #[cfg_attr(feature = "schema", derive(JsonSchema))] @@ -2591,104 +1564,20 @@ 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, /// The installation mode for the update on Windows. Defaults to `passive`. #[serde(default, alias = "install-mode")] pub install_mode: WindowsUpdateInstallMode, } -/// The Updater configuration object. -/// -/// See more: https://tauri.app/v1/api/config#updaterconfig -#[skip_serializing_none] -#[derive(Debug, PartialEq, Eq, Clone, Serialize)] -#[cfg_attr(feature = "schema", derive(JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct UpdaterConfig { - /// Whether the updater is active or not. - #[serde(default)] - pub active: bool, - /// Display built-in dialog or use event system if disabled. - #[serde(default = "default_true")] - pub dialog: bool, - /// The updater endpoints. TLS is enforced on production. - /// - /// The updater URL can contain the following variables: - /// - {{current_version}}: The version of the app that is requesting the update - /// - {{target}}: The operating system name (one of `linux`, `windows` or `darwin`). - /// - {{arch}}: The architecture of the machine (one of `x86_64`, `i686`, `aarch64` or `armv7`). - /// - /// # Examples - /// - "https://my.cdn.com/latest.json": a raw JSON endpoint that returns the latest version and download links for each platform. - /// - "https://updates.app.dev/{{target}}?version={{current_version}}&arch={{arch}}": a dedicated API with positional and query string arguments. - #[allow(rustdoc::bare_urls)] - pub endpoints: Option>, - /// Signature public key. - #[serde(default)] // use default just so the schema doesn't flag it as required - pub pubkey: String, - /// The Windows configuration for the updater. - #[serde(default)] - pub windows: UpdaterWindowsConfig, -} - -impl<'de> Deserialize<'de> for UpdaterConfig { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - struct InnerUpdaterConfig { - #[serde(default)] - active: bool, - #[serde(default = "default_true")] - dialog: bool, - endpoints: Option>, - pubkey: Option, - #[serde(default)] - windows: UpdaterWindowsConfig, - } - - let config = InnerUpdaterConfig::deserialize(deserializer)?; - - if config.active && config.pubkey.is_none() { - return Err(DeError::custom( - "The updater `pubkey` configuration is required.", - )); - } - - Ok(UpdaterConfig { - active: config.active, - dialog: config.dialog, - endpoints: config.endpoints, - pubkey: config.pubkey.unwrap_or_default(), - windows: config.windows, - }) - } -} - -impl Default for UpdaterConfig { - fn default() -> Self { - Self { - active: false, - dialog: true, - endpoints: None, - pubkey: "".into(), - windows: Default::default(), - } - } -} - -/// Configuration for application system tray icon. +/// Configuration for application tray icon. /// -/// See more: https://tauri.app/v1/api/config#systemtrayconfig +/// See more: https://tauri.app/v1/api/config#trayiconconfig #[skip_serializing_none] #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct SystemTrayConfig { - /// Path to the default icon to use on the system tray. +pub struct TrayIconConfig { + /// Path to the default icon to use for the tray icon. #[serde(alias = "icon-path")] pub icon_path: PathBuf, /// A Boolean value that determines whether the image represents a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) image on macOS. @@ -2699,6 +1588,44 @@ pub struct SystemTrayConfig { pub menu_on_left_click: bool, /// Title for MacOS tray pub title: Option, + /// Tray icon tooltip on Windows and macOS + pub tooltip: Option, +} + +/// General configuration for the iOS target. +#[skip_serializing_none] +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct IosConfig { + /// The development team. This value is required for iOS development because code signing is enforced. + /// The `TAURI_APPLE_DEVELOPMENT_TEAM` environment variable can be set to overwrite it. + #[serde(alias = "development-team")] + pub development_team: Option, +} + +/// General configuration for the iOS target. +#[skip_serializing_none] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct AndroidConfig { + /// The minimum API level required for the application to run. + /// The Android system will prevent the user from installing the application if the system's API level is lower than the value specified. + #[serde(alias = "min-sdk-version", default = "default_min_sdk_version")] + pub min_sdk_version: u32, +} + +impl Default for AndroidConfig { + fn default() -> Self { + Self { + min_sdk_version: default_min_sdk_version(), + } + } +} + +fn default_min_sdk_version() -> u32 { + 24 } /// Defines the URL or assets to embed in the application. @@ -2840,7 +1767,7 @@ fn default_dist_dir() -> AppUrl { struct PackageVersion(String); impl<'d> serde::Deserialize<'d> for PackageVersion { - fn deserialize>(deserializer: D) -> Result { + fn deserialize>(deserializer: D) -> Result { struct PackageVersionVisitor; impl<'d> Visitor<'d> for PackageVersionVisitor { @@ -2932,8 +1859,7 @@ impl PackageConfig { /// The Tauri configuration object. /// It is read from a file where you can define your frontend assets, -/// configure the bundler, enable the app updater, define a system tray, -/// enable APIs via the allowlist and more. +/// configure the bundler and define a tray icon. /// /// The configuration file is generated by the /// [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in @@ -2953,8 +1879,8 @@ impl PackageConfig { /// /// In addition to the default configuration file, Tauri can /// read a platform-specific configuration from `tauri.linux.conf.json`, -/// `tauri.windows.conf.json`, and `tauri.macos.conf.json` -/// (or `Tauri.linux.toml`, `Tauri.windows.toml` and `Tauri.macos.toml` if the `Tauri.toml` format is used), +/// `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json` +/// (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used), /// which gets merged with the main configuration object. /// /// ## Configuration Structure @@ -2979,16 +1905,10 @@ impl PackageConfig { /// "version": "0.1.0" /// }, /// "tauri": { -/// "allowlist": { -/// "all": true -/// }, /// "bundle": {}, /// "security": { /// "csp": null /// }, -/// "updater": { -/// "active": false -/// }, /// "windows": [ /// { /// "fullscreen": false, @@ -3203,7 +2123,7 @@ mod build { ::tauri::utils::config::$struct { $($field: #$field),+ } - }); + }) }; } @@ -3235,6 +2155,23 @@ mod build { } } + impl ToTokens for Color { + fn to_tokens(&self, tokens: &mut TokenStream) { + let Color(r, g, b, a) = self; + tokens.append_all(quote! {::tauri::utils::Color(#r,#g,#b,#a)}); + } + } + impl ToTokens for WindowEffectsConfig { + fn to_tokens(&self, tokens: &mut TokenStream) { + let effects = vec_lit(self.effects.clone(), |d| d); + let state = opt_lit(self.state.as_ref()); + let radius = opt_lit(self.radius.as_ref()); + let color = opt_lit(self.color.as_ref()); + + literal_struct!(tokens, WindowEffectsConfig, effects, state, radius, color) + } + } + impl ToTokens for crate::TitleBarStyle { fn to_tokens(&self, tokens: &mut TokenStream) { let prefix = quote! { ::tauri::utils::TitleBarStyle }; @@ -3247,6 +2184,53 @@ mod build { } } + impl ToTokens for crate::WindowEffect { + fn to_tokens(&self, tokens: &mut TokenStream) { + let prefix = quote! { ::tauri::utils::WindowEffect }; + + #[allow(deprecated)] + tokens.append_all(match self { + WindowEffect::AppearanceBased => quote! { #prefix::AppearanceBased}, + WindowEffect::Light => quote! { #prefix::Light}, + WindowEffect::Dark => quote! { #prefix::Dark}, + WindowEffect::MediumLight => quote! { #prefix::MediumLight}, + WindowEffect::UltraDark => quote! { #prefix::UltraDark}, + WindowEffect::Titlebar => quote! { #prefix::Titlebar}, + WindowEffect::Selection => quote! { #prefix::Selection}, + WindowEffect::Menu => quote! { #prefix::Menu}, + WindowEffect::Popover => quote! { #prefix::Popover}, + WindowEffect::Sidebar => quote! { #prefix::Sidebar}, + WindowEffect::HeaderView => quote! { #prefix::HeaderView}, + WindowEffect::Sheet => quote! { #prefix::Sheet}, + WindowEffect::WindowBackground => quote! { #prefix::WindowBackground}, + WindowEffect::HudWindow => quote! { #prefix::HudWindow}, + WindowEffect::FullScreenUI => quote! { #prefix::FullScreenUI}, + WindowEffect::Tooltip => quote! { #prefix::Tooltip}, + WindowEffect::ContentBackground => quote! { #prefix::ContentBackground}, + WindowEffect::UnderWindowBackground => quote! { #prefix::UnderWindowBackground}, + WindowEffect::UnderPageBackground => quote! { #prefix::UnderPageBackground}, + WindowEffect::Mica => quote! { #prefix::Mica}, + WindowEffect::MicaDark => quote! { #prefix::MicaDark}, + WindowEffect::MicaLight => quote! { #prefix::MicaLight}, + WindowEffect::Blur => quote! { #prefix::Blur}, + WindowEffect::Acrylic => quote! { #prefix::Acrylic}, + }) + } + } + + impl ToTokens for crate::WindowEffectState { + fn to_tokens(&self, tokens: &mut TokenStream) { + let prefix = quote! { ::tauri::utils::WindowEffectState }; + + #[allow(deprecated)] + tokens.append_all(match self { + WindowEffectState::Active => quote! { #prefix::Active}, + WindowEffectState::FollowsWindowActiveState => quote! { #prefix::FollowsWindowActiveState}, + WindowEffectState::Inactive => quote! { #prefix::Inactive}, + }) + } + } + impl ToTokens for WindowConfig { fn to_tokens(&self, tokens: &mut TokenStream) { let label = str_lit(&self.label); @@ -3274,6 +2258,7 @@ mod build { let visible = self.visible; let decorations = self.decorations; let always_on_top = self.always_on_top; + let visible_on_all_workspaces = self.visible_on_all_workspaces; let content_protected = self.content_protected; let skip_taskbar = self.skip_taskbar; let theme = opt_lit(self.theme.as_ref()); @@ -3282,6 +2267,9 @@ mod build { let accept_first_mouse = self.accept_first_mouse; let tabbing_identifier = opt_str_lit(self.tabbing_identifier.as_ref()); let additional_browser_args = opt_str_lit(self.additional_browser_args.as_ref()); + let shadow = self.shadow; + let window_effects = opt_lit(self.window_effects.as_ref()); + let incognito = self.incognito; literal_struct!( tokens, @@ -3311,6 +2299,7 @@ mod build { visible, decorations, always_on_top, + visible_on_all_workspaces, content_protected, skip_taskbar, theme, @@ -3318,104 +2307,10 @@ mod build { hidden_title, accept_first_mouse, tabbing_identifier, - additional_browser_args - ); - } - } - - impl ToTokens for CliArg { - fn to_tokens(&self, tokens: &mut TokenStream) { - let short = opt_lit(self.short.as_ref()); - let name = str_lit(&self.name); - let description = opt_str_lit(self.description.as_ref()); - let long_description = opt_str_lit(self.long_description.as_ref()); - let takes_value = self.takes_value; - let multiple = self.multiple; - let multiple_occurrences = self.multiple_occurrences; - let number_of_values = opt_lit(self.number_of_values.as_ref()); - let possible_values = opt_vec_str_lit(self.possible_values.as_ref()); - let min_values = opt_lit(self.min_values.as_ref()); - let max_values = opt_lit(self.max_values.as_ref()); - let required = self.required; - let required_unless_present = opt_str_lit(self.required_unless_present.as_ref()); - let required_unless_present_all = opt_vec_str_lit(self.required_unless_present_all.as_ref()); - let required_unless_present_any = opt_vec_str_lit(self.required_unless_present_any.as_ref()); - let conflicts_with = opt_str_lit(self.conflicts_with.as_ref()); - let conflicts_with_all = opt_vec_str_lit(self.conflicts_with_all.as_ref()); - let requires = opt_str_lit(self.requires.as_ref()); - let requires_all = opt_vec_str_lit(self.requires_all.as_ref()); - let requires_if = opt_vec_str_lit(self.requires_if.as_ref()); - let required_if_eq = opt_vec_str_lit(self.required_if_eq.as_ref()); - let require_equals = opt_lit(self.require_equals.as_ref()); - let index = opt_lit(self.index.as_ref()); - - literal_struct!( - tokens, - CliArg, - short, - name, - description, - long_description, - takes_value, - multiple, - multiple_occurrences, - number_of_values, - possible_values, - min_values, - max_values, - required, - required_unless_present, - required_unless_present_all, - required_unless_present_any, - conflicts_with, - conflicts_with_all, - requires, - requires_all, - requires_if, - required_if_eq, - require_equals, - index - ); - } - } - - impl ToTokens for CliConfig { - fn to_tokens(&self, tokens: &mut TokenStream) { - let description = opt_str_lit(self.description.as_ref()); - let long_description = opt_str_lit(self.long_description.as_ref()); - let before_help = opt_str_lit(self.before_help.as_ref()); - let after_help = opt_str_lit(self.after_help.as_ref()); - let args = { - let args = self.args.as_ref().map(|args| { - let arg = args.iter().map(|a| quote! { #a }); - quote! { vec![#(#arg),*] } - }); - opt_lit(args.as_ref()) - }; - let subcommands = opt_lit( - self - .subcommands - .as_ref() - .map(|map| { - map_lit( - quote! { ::std::collections::HashMap }, - map, - str_lit, - identity, - ) - }) - .as_ref(), - ); - - literal_struct!( - tokens, - CliConfig, - description, - long_description, - before_help, - after_help, - args, - subcommands + additional_browser_args, + shadow, + window_effects, + incognito ); } } @@ -3477,6 +2372,16 @@ mod build { } } + impl ToTokens for UpdaterConfig { + fn to_tokens(&self, tokens: &mut TokenStream) { + let active = self.active; + let pubkey = str_lit(&self.pubkey); + let windows = &self.windows; + + literal_struct!(tokens, UpdaterConfig, active, pubkey, windows); + } + } + impl ToTokens for BundleConfig { fn to_tokens(&self, tokens: &mut TokenStream) { let identifier = str_lit(&self.identifier); @@ -3487,6 +2392,7 @@ mod build { let resources = quote!(None); let copyright = quote!(None); let category = quote!(None); + let file_associations = quote!(None); let short_description = quote!(None); let long_description = quote!(None); let appimage = quote!(Default::default()); @@ -3494,6 +2400,9 @@ mod build { let macos = quote!(Default::default()); let external_bin = opt_vec_str_lit(self.external_bin.as_ref()); let windows = &self.windows; + let ios = quote!(Default::default()); + let android = quote!(Default::default()); + let updater = &self.updater; literal_struct!( tokens, @@ -3506,13 +2415,17 @@ mod build { resources, copyright, category, + file_associations, short_description, long_description, appimage, deb, macos, external_bin, - windows + windows, + ios, + android, + updater ); } } @@ -3574,39 +2487,7 @@ mod build { impl ToTokens for UpdaterWindowsConfig { fn to_tokens(&self, tokens: &mut TokenStream) { let install_mode = &self.install_mode; - let installer_args = vec_lit(&self.installer_args, str_lit); - literal_struct!(tokens, UpdaterWindowsConfig, install_mode, installer_args); - } - } - - impl ToTokens for UpdaterConfig { - fn to_tokens(&self, tokens: &mut TokenStream) { - let active = self.active; - let dialog = self.dialog; - let pubkey = str_lit(&self.pubkey); - let endpoints = opt_lit( - self - .endpoints - .as_ref() - .map(|list| { - vec_lit(list, |url| { - let url = url.0.as_str(); - quote! { ::tauri::utils::config::UpdaterEndpoint(#url.parse().unwrap()) } - }) - }) - .as_ref(), - ); - let windows = &self.windows; - - literal_struct!( - tokens, - UpdaterConfig, - active, - dialog, - pubkey, - endpoints, - windows - ); + literal_struct!(tokens, UpdaterWindowsConfig, install_mode); } } @@ -3671,7 +2552,6 @@ mod build { let domain = str_lit(&self.domain); let windows = vec_lit(&self.windows, str_lit); let plugins = vec_lit(&self.plugins, str_lit); - let enable_tauri_api = self.enable_tauri_api; literal_struct!( tokens, @@ -3679,8 +2559,7 @@ mod build { scheme, domain, windows, - plugins, - enable_tauri_api + plugins ); } } @@ -3693,6 +2572,7 @@ mod build { let dangerous_disable_asset_csp_modification = &self.dangerous_disable_asset_csp_modification; let dangerous_remote_domain_ipc_access = vec_lit(&self.dangerous_remote_domain_ipc_access, identity); + let asset_protocol = &self.asset_protocol; literal_struct!( tokens, @@ -3701,31 +2581,34 @@ mod build { dev_csp, freeze_prototype, dangerous_disable_asset_csp_modification, - dangerous_remote_domain_ipc_access + dangerous_remote_domain_ipc_access, + asset_protocol ); } } - impl ToTokens for SystemTrayConfig { + impl ToTokens for TrayIconConfig { fn to_tokens(&self, tokens: &mut TokenStream) { let icon_as_template = self.icon_as_template; let menu_on_left_click = self.menu_on_left_click; let icon_path = path_buf_lit(&self.icon_path); let title = opt_str_lit(self.title.as_ref()); + let tooltip = opt_str_lit(self.tooltip.as_ref()); literal_struct!( tokens, - SystemTrayConfig, + TrayIconConfig, icon_path, icon_as_template, menu_on_left_click, - title + title, + tooltip ); } } - impl ToTokens for FsAllowlistScope { + impl ToTokens for FsScope { fn to_tokens(&self, tokens: &mut TokenStream) { - let prefix = quote! { ::tauri::utils::config::FsAllowlistScope }; + let prefix = quote! { ::tauri::utils::config::FsScope }; tokens.append_all(match self { Self::AllowedPaths(allow) => { @@ -3742,110 +2625,10 @@ mod build { } } - impl ToTokens for FsAllowlistConfig { - fn to_tokens(&self, tokens: &mut TokenStream) { - let scope = &self.scope; - tokens.append_all(quote! { ::tauri::utils::config::FsAllowlistConfig { scope: #scope, ..Default::default() } }) - } - } - - impl ToTokens for ProtocolAllowlistConfig { - fn to_tokens(&self, tokens: &mut TokenStream) { - let asset_scope = &self.asset_scope; - tokens.append_all(quote! { ::tauri::utils::config::ProtocolAllowlistConfig { asset_scope: #asset_scope, ..Default::default() } }) - } - } - - impl ToTokens for HttpAllowlistScope { - fn to_tokens(&self, tokens: &mut TokenStream) { - let allowed_urls = vec_lit(&self.0, url_lit); - tokens.append_all(quote! { ::tauri::utils::config::HttpAllowlistScope(#allowed_urls) }) - } - } - - impl ToTokens for HttpAllowlistConfig { - fn to_tokens(&self, tokens: &mut TokenStream) { - let scope = &self.scope; - tokens.append_all(quote! { ::tauri::utils::config::HttpAllowlistConfig { scope: #scope, ..Default::default() } }) - } - } - - impl ToTokens for ShellAllowedCommand { - fn to_tokens(&self, tokens: &mut TokenStream) { - let name = str_lit(&self.name); - let command = path_buf_lit(&self.command); - let args = &self.args; - let sidecar = &self.sidecar; - - literal_struct!(tokens, ShellAllowedCommand, name, command, args, sidecar); - } - } - - impl ToTokens for ShellAllowedArgs { - fn to_tokens(&self, tokens: &mut TokenStream) { - let prefix = quote! { ::tauri::utils::config::ShellAllowedArgs }; - - tokens.append_all(match self { - Self::Flag(flag) => quote!(#prefix::Flag(#flag)), - Self::List(list) => { - let list = vec_lit(list, identity); - quote!(#prefix::List(#list)) - } - }) - } - } - - impl ToTokens for ShellAllowedArg { - fn to_tokens(&self, tokens: &mut TokenStream) { - let prefix = quote! { ::tauri::utils::config::ShellAllowedArg }; - - tokens.append_all(match self { - Self::Fixed(fixed) => { - let fixed = str_lit(fixed); - quote!(#prefix::Fixed(#fixed)) - } - Self::Var { validator } => { - let validator = str_lit(validator); - quote!(#prefix::Var { validator: #validator }) - } - }) - } - } - - impl ToTokens for ShellAllowlistOpen { - fn to_tokens(&self, tokens: &mut TokenStream) { - let prefix = quote! { ::tauri::utils::config::ShellAllowlistOpen }; - - tokens.append_all(match self { - Self::Flag(flag) => quote!(#prefix::Flag(#flag)), - Self::Validate(regex) => quote!(#prefix::Validate(#regex)), - }) - } - } - - impl ToTokens for ShellAllowlistScope { - fn to_tokens(&self, tokens: &mut TokenStream) { - let allowed_commands = vec_lit(&self.0, identity); - tokens.append_all(quote! { ::tauri::utils::config::ShellAllowlistScope(#allowed_commands) }) - } - } - - impl ToTokens for ShellAllowlistConfig { + impl ToTokens for AssetProtocolConfig { fn to_tokens(&self, tokens: &mut TokenStream) { let scope = &self.scope; - tokens.append_all(quote! { ::tauri::utils::config::ShellAllowlistConfig { scope: #scope, ..Default::default() } }) - } - } - - impl ToTokens for AllowlistConfig { - fn to_tokens(&self, tokens: &mut TokenStream) { - let fs = &self.fs; - let protocol = &self.protocol; - let http = &self.http; - let shell = &self.shell; - tokens.append_all( - quote! { ::tauri::utils::config::AllowlistConfig { fs: #fs, protocol: #protocol, http: #http, shell: #shell, ..Default::default() } }, - ) + tokens.append_all(quote! { ::tauri::utils::config::AssetProtocolConfig { scope: #scope, ..Default::default() } }) } } @@ -3853,12 +2636,9 @@ mod build { fn to_tokens(&self, tokens: &mut TokenStream) { let pattern = &self.pattern; let windows = vec_lit(&self.windows, identity); - let cli = opt_lit(self.cli.as_ref()); let bundle = &self.bundle; - let updater = &self.updater; let security = &self.security; - let system_tray = opt_lit(self.system_tray.as_ref()); - let allowlist = &self.allowlist; + let tray_icon = opt_lit(self.tray_icon.as_ref()); let macos_private_api = self.macos_private_api; literal_struct!( @@ -3866,12 +2646,9 @@ mod build { TauriConfig, pattern, windows, - cli, bundle, - updater, security, - system_tray, - allowlist, + tray_icon, macos_private_api ); } @@ -3930,8 +2707,6 @@ mod test { let d_windows: Vec = vec![]; // get default bundle let d_bundle = BundleConfig::default(); - // get default updater - let d_updater = UpdaterConfig::default(); // create a tauri config. let tauri = TauriConfig { @@ -3946,6 +2721,7 @@ mod test { resources: None, copyright: None, category: None, + file_associations: None, short_description: None, long_description: None, appimage: Default::default(), @@ -3953,14 +2729,9 @@ mod test { macos: Default::default(), external_bin: None, windows: Default::default(), - }, - cli: None, - updater: UpdaterConfig { - active: false, - dialog: true, - pubkey: "".into(), - endpoints: None, - windows: Default::default(), + ios: Default::default(), + android: Default::default(), + updater: Default::default(), }, security: SecurityConfig { csp: None, @@ -3968,9 +2739,9 @@ mod test { freeze_prototype: false, dangerous_disable_asset_csp_modification: DisabledCspModificationKind::Flag(false), dangerous_remote_domain_ipc_access: Vec::new(), + asset_protocol: AssetProtocolConfig::default(), }, - allowlist: AllowlistConfig::default(), - system_tray: None, + tray_icon: None, macos_private_api: false, }; @@ -3992,7 +2763,6 @@ mod test { assert_eq!(t_config, tauri); assert_eq!(b_config, build); assert_eq!(d_bundle, tauri.bundle); - assert_eq!(d_updater, tauri.updater); assert_eq!( d_path, AppUrl::Url(WindowUrl::External( diff --git a/core/tauri-utils/src/config/parse.rs b/core/tauri-utils/src/config/parse.rs index a643fb1aeac..6590620edd3 100644 --- a/core/tauri-utils/src/config/parse.rs +++ b/core/tauri-utils/src/config/parse.rs @@ -54,6 +54,10 @@ impl ConfigFormat { "tauri.macos.conf.json" } else if cfg!(windows) { "tauri.windows.conf.json" + } else if cfg!(target_os = "android") { + "tauri.android.conf.json" + } else if cfg!(target_os = "ios") { + "tauri.ios.conf.json" } else { "tauri.linux.conf.json" } @@ -63,6 +67,10 @@ impl ConfigFormat { "tauri.macos.conf.json5" } else if cfg!(windows) { "tauri.windows.conf.json5" + } else if cfg!(target_os = "android") { + "tauri.android.conf.json" + } else if cfg!(target_os = "ios") { + "tauri.ios.conf.json" } else { "tauri.linux.conf.json5" } @@ -72,6 +80,10 @@ impl ConfigFormat { "Tauri.macos.toml" } else if cfg!(windows) { "Tauri.windows.toml" + } else if cfg!(target_os = "android") { + "tauri.android.toml" + } else if cfg!(target_os = "ios") { + "tauri.ios.toml" } else { "Tauri.linux.toml" } @@ -170,11 +182,13 @@ pub fn is_configuration_file(path: &Path) -> bool { /// Reads the configuration from the given root directory. /// -/// It first looks for a `tauri.conf.json[5]` file on the given directory. The file must exist. +/// It first looks for a `tauri.conf.json[5]` or `Tauri.toml` file on the given directory. The file must exist. /// Then it looks for a platform-specific configuration file: -/// - `tauri.macos.conf.json[5]` on macOS -/// - `tauri.linux.conf.json[5]` on Linux -/// - `tauri.windows.conf.json[5]` on Windows +/// - `tauri.macos.conf.json[5]` or `Tauri.macos.toml` on macOS +/// - `tauri.linux.conf.json[5]` or `Tauri.linux.toml` on Linux +/// - `tauri.windows.conf.json[5]` or `Tauri.windows.toml` on Windows +/// - `tauri.android.conf.json[5]` or `Tauri.android.toml` on Android +/// - `tauri.ios.conf.json[5]` or `Tauri.ios.toml` on iOS /// Merging the configurations using [JSON Merge Patch (RFC 7396)]. /// /// [JSON Merge Patch (RFC 7396)]: https://datatracker.ietf.org/doc/html/rfc7396. diff --git a/core/tauri-utils/src/html.rs b/core/tauri-utils/src/html.rs index 13600817dde..5b8af5772ca 100644 --- a/core/tauri-utils/src/html.rs +++ b/core/tauri-utils/src/html.rs @@ -302,7 +302,7 @@ mod tests { assert_eq!( document.to_string(), format!( - r#""#, + r#""#, super::CSP_TOKEN ) ); diff --git a/core/tauri-utils/src/lib.rs b/core/tauri-utils/src/lib.rs index ca36e83ca36..a9fbf6622fc 100644 --- a/core/tauri-utils/src/lib.rs +++ b/core/tauri-utils/src/lib.rs @@ -2,8 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -//! Tauri utility helpers +//! [![](https://github.com/tauri-apps/tauri/raw/dev/.github/splash.png)](https://tauri.app) +//! +//! This crate contains common code that is reused in many places and offers useful utilities like parsing configuration files, detecting platform triples, injecting the CSP, and managing assets. + +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] #![warn(missing_docs, rust_2018_idioms)] +#![allow(clippy::deprecated_semver)] use std::{ fmt::Display, @@ -37,6 +45,8 @@ pub struct PackageInfo { pub authors: &'static str, /// The crate description. pub description: &'static str, + /// The crate name. + pub crate_name: &'static str, } impl PackageInfo { @@ -53,6 +63,99 @@ impl PackageInfo { } } +#[allow(deprecated)] +mod window_effects { + use super::*; + + #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)] + #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] + #[serde(rename_all = "camelCase")] + /// Platform-specific window effects + pub enum WindowEffect { + /// A default material appropriate for the view's effectiveAppearance. **macOS 10.14-** + #[deprecated( + since = "macOS 10.14", + note = "You should instead choose an appropriate semantic material." + )] + AppearanceBased, + /// **macOS 10.14-** + #[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")] + Light, + /// **macOS 10.14-** + #[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")] + Dark, + /// **macOS 10.14-** + #[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")] + MediumLight, + /// **macOS 10.14-** + #[deprecated(since = "macOS 10.14", note = "Use a semantic material instead.")] + UltraDark, + /// **macOS 10.10+** + Titlebar, + /// **macOS 10.10+** + Selection, + /// **macOS 10.11+** + Menu, + /// **macOS 10.11+** + Popover, + /// **macOS 10.11+** + Sidebar, + /// **macOS 10.14+** + HeaderView, + /// **macOS 10.14+** + Sheet, + /// **macOS 10.14+** + WindowBackground, + /// **macOS 10.14+** + HudWindow, + /// **macOS 10.14+** + FullScreenUI, + /// **macOS 10.14+** + Tooltip, + /// **macOS 10.14+** + ContentBackground, + /// **macOS 10.14+** + UnderWindowBackground, + /// **macOS 10.14+** + UnderPageBackground, + /// Mica effect that matches the system dark perefence **Windows 11 Only** + Mica, + /// Mica effect with dark mode but only if dark mode is enabled on the system **Windows 11 Only** + MicaDark, + /// Mica effect with light mode **Windows 11 Only** + MicaLight, + /// **Windows 7/10/11(22H1) Only** + /// + /// ## Notes + /// + /// This effect has bad performance when resizing/dragging the window on Windows 11 build 22621. + Blur, + /// **Windows 10/11 Only** + /// + /// ## Notes + /// + /// This effect has bad performance when resizing/dragging the window on Windows 10 v1903+ and Windows 11 build 22000. + Acrylic, + } + + /// Window effect state **macOS only** + /// + /// + #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)] + #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] + #[serde(rename_all = "camelCase")] + pub enum WindowEffectState { + /// Make window effect state follow the window's active state + FollowsWindowActiveState, + /// Make window effect state always active + Active, + /// Make window effect state always inactive + Inactive, + } +} + +pub use window_effects::{WindowEffect, WindowEffectState}; + /// How the window title bar should be displayed on macOS. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] diff --git a/core/tauri-utils/src/pattern/isolation.js b/core/tauri-utils/src/pattern/isolation.js index ce284d59a1d..e236839aabb 100644 --- a/core/tauri-utils/src/pattern/isolation.js +++ b/core/tauri-utils/src/pattern/isolation.js @@ -8,7 +8,8 @@ * isolation frame -> main frame = isolation message */ -;(async function () { +; +(async function () { /** * Sends the message to the isolation frame. * @param {any} message @@ -38,34 +39,52 @@ * @return {Promise<{nonce: number[], payload: number[]}>} */ async function encrypt(data) { - let algorithm = Object.create(null) + const algorithm = Object.create(null) algorithm.name = 'AES-GCM' algorithm.iv = window.crypto.getRandomValues(new Uint8Array(12)) - let encoder = new TextEncoder() - let payloadRaw = encoder.encode(__RAW_stringify_ipc_message_fn__(data)) + const encoder = new TextEncoder() + const encoded = encoder.encode(__RAW_process_ipc_message_fn__(data).data) return window.crypto.subtle - .encrypt(algorithm, aesGcmKey, payloadRaw) + .encrypt(algorithm, aesGcmKey, encoded) .then((payload) => { - let result = Object.create(null) + const result = Object.create(null) result.nonce = Array.from(new Uint8Array(algorithm.iv)) result.payload = Array.from(new Uint8Array(payload)) return result }) } + /** + * Detects if a message event is a valid isolation message. + * + * @param {MessageEvent} event - a message event that is expected to be an isolation message + * @return {boolean} - if the event was a valid isolation message + */ + function isIsolationMessage(data) { + if (typeof data === 'object' && typeof data.payload === 'object') { + const keys = data.payload ? Object.keys(data.payload) : [] + return ( + keys.length > 0 && + keys.every((key) => key === 'nonce' || key === 'payload') + ) + } + return false + } + /** * Detect if a message event is a valid isolation payload. * * @param {MessageEvent} event - a message event that is expected to be an isolation payload * @return boolean */ - function isIsolationPayload(event) { + function isIsolationPayload(data) { return ( - typeof event.data === 'object' && - 'callback' in event.data && - 'error' in event.data + typeof data === 'object' && + 'callback' in data && + 'error' in data && + !isIsolationMessage(data) ) } @@ -74,7 +93,7 @@ * @param {MessageEvent} event */ async function payloadHandler(event) { - if (!isIsolationPayload(event)) { + if (!isIsolationPayload(event.data)) { return } @@ -85,8 +104,13 @@ data = await window.__TAURI_ISOLATION_HOOK__(data) } - const encrypted = await encrypt(data) - sendMessage(encrypted) + const message = Object.create(null) + message.cmd = data.cmd + message.callback = data.callback + message.error = data.error + message.options = data.options + message.payload = await encrypt(data.payload) + sendMessage(message) } window.addEventListener('message', payloadHandler, false) diff --git a/core/tauri-utils/src/pattern/isolation.rs b/core/tauri-utils/src/pattern/isolation.rs index ae538245032..3b2dfc0846c 100644 --- a/core/tauri-utils/src/pattern/isolation.rs +++ b/core/tauri-utils/src/pattern/isolation.rs @@ -96,16 +96,14 @@ impl Keys { } /// Decrypts a message using the generated keys. - pub fn decrypt(&self, raw: RawIsolationPayload<'_>) -> Result { + pub fn decrypt(&self, raw: RawIsolationPayload<'_>) -> Result, Error> { let RawIsolationPayload { nonce, payload } = raw; let nonce: [u8; 12] = nonce.as_ref().try_into()?; - let bytes = self + self .aes_gcm .key .decrypt(Nonce::from_slice(&nonce), payload.as_ref()) - .map_err(|_| self::Error::Aes)?; - - String::from_utf8(bytes).map_err(Into::into) + .map_err(|_| self::Error::Aes) } } @@ -116,11 +114,11 @@ pub struct RawIsolationPayload<'a> { payload: Cow<'a, [u8]>, } -impl<'a> TryFrom<&'a str> for RawIsolationPayload<'a> { +impl<'a> TryFrom<&'a Vec> for RawIsolationPayload<'a> { type Error = Error; - fn try_from(value: &'a str) -> Result { - serde_json::from_str(value).map_err(Into::into) + fn try_from(value: &'a Vec) -> Result { + serde_json::from_slice(value).map_err(Into::into) } } @@ -141,9 +139,9 @@ pub struct IsolationJavascriptCodegen { pub struct IsolationJavascriptRuntime<'a> { /// The key used on the Rust backend and the Isolation Javascript pub runtime_aes_gcm_key: &'a [u8; 32], - /// The function that stringifies a IPC message. + /// The function that processes the IPC message. #[raw] - pub stringify_ipc_message_fn: &'a str, + pub process_ipc_message_fn: &'a str, } #[cfg(test)] diff --git a/core/tauri-utils/src/platform.rs b/core/tauri-utils/src/platform.rs index 5e2e7719232..0ca4fd61796 100644 --- a/core/tauri-utils/src/platform.rs +++ b/core/tauri-utils/src/platform.rs @@ -204,7 +204,7 @@ pub fn resource_dir(package_info: &PackageInfo, env: &Env) -> crate::Result Option { + /// Helper function to dynamically load function pointer. + /// `library` and `function` must be null-terminated. + pub fn get_function_impl(library: &str, function: &str) -> Option { let library = encode_wide(library); assert_eq!(function.chars().last(), Some('\0')); let function = PCSTR::from_raw(function.as_ptr()); diff --git a/core/tauri/CHANGELOG.md b/core/tauri/CHANGELOG.md index e87175be283..3bc6484f400 100644 --- a/core/tauri/CHANGELOG.md +++ b/core/tauri/CHANGELOG.md @@ -1,5 +1,219 @@ # Changelog +## \[2.0.0-alpha.11] + +### New Features + +- [`4db363a0`](https://www.github.com/tauri-apps/tauri/commit/4db363a03c182349f8491f46ced258d84723b11f)([#6589](https://www.github.com/tauri-apps/tauri/pull/6589)) Added `visible_on_all_workspaces` configuration option to `WindowBuilder`, `Window`, and `WindowConfig`. +- [`84c41597`](https://www.github.com/tauri-apps/tauri/commit/84c4159754b2e59244211ed9e1fc702d851a0562)([#6394](https://www.github.com/tauri-apps/tauri/pull/6394)) Add `App::primary_monitor`, `App::available_monitors`, `AppHandle::primary_monitor`, and `AppHandle::available_monitors` +- [`2a000e15`](https://www.github.com/tauri-apps/tauri/commit/2a000e150d02dff28c8b20ad097b29e209160045)([#7235](https://www.github.com/tauri-apps/tauri/pull/7235)) Added `Window::navigate`. +- [`3b98141a`](https://www.github.com/tauri-apps/tauri/commit/3b98141aa26f74c641a4090874247b97079bd58a)([#3736](https://www.github.com/tauri-apps/tauri/pull/3736)) Added support to file associations. +- [`3a2c3e74`](https://www.github.com/tauri-apps/tauri/commit/3a2c3e74710bef9a14932dce74c351cca6215429)([#7306](https://www.github.com/tauri-apps/tauri/pull/7306)) Added `PluginBuilder::on_navigation`. + Added `Plugin::on_navigation`. +- [`753900dd`](https://www.github.com/tauri-apps/tauri/commit/753900dd6e549aaf56f419144382669e3b246404)([#7440](https://www.github.com/tauri-apps/tauri/pull/7440)) Expose `RunEvent::Opened` on macOS and iOS for deep link support. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) Add `App::cleanup_before_exit` and `AppHandle::cleanup_before_exit` to manually call the cleanup logic. **You should always exit the tauri app immediately after this function returns and not use any tauri-related APIs.** +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) On Linux, add `Window::default_vbox` to get a reference to the `gtk::Box` that contains the menu bar and the webview. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) Add `linux-libxdo` feature flag (disabled by default) to enable linking to `libxdo` which is used to make `Cut`, `Copy`, `Paste` and `SelectAll` native menu items work on Linux. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) On macOS, add `Window::ns_view` to get a pointer to the NSWindow content view. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) Expose `run_on_main_thread` method on `App` that is similar to `AppHandle::run_on_main_thread`. + +### Enhancements + +- [`a5752db9`](https://www.github.com/tauri-apps/tauri/commit/a5752db9852bb852e61f19dfb48a9435c1fdc79c)([#7436](https://www.github.com/tauri-apps/tauri/pull/7436)) Listen to `onNewIntent` and forward it to registered plugins. +- [`fbeb5b91`](https://www.github.com/tauri-apps/tauri/commit/fbeb5b9185baeda19e865228179e3e44c165f1d9)([#7170](https://www.github.com/tauri-apps/tauri/pull/7170)) Added `Channel::new` allowing communication from a mobile plugin with Rust. +- [`fbeb5b91`](https://www.github.com/tauri-apps/tauri/commit/fbeb5b9185baeda19e865228179e3e44c165f1d9)([#7170](https://www.github.com/tauri-apps/tauri/pull/7170)) Use custom protocols on the IPC implementation to enhance performance. + +### Dependencies + +- Upgraded to `tauri-runtime@1.0.0-alpha.0` +- Upgraded to `tauri-utils@2.0.0-alpha.7` +- Upgraded to `tauri-macros@2.0.0-alpha.7` +- Upgraded to `tauri-runtime-wry@1.0.0-alpha.0` +- Upgraded to `tauri-build@2.0.0-alpha.7` +- [`d1a6e2f3`](https://www.github.com/tauri-apps/tauri/commit/d1a6e2f33326161a78a9a72bd9320dcb1b1f9710)([#7252](https://www.github.com/tauri-apps/tauri/pull/7252)) Update `state` to v0.6. + +### Breaking Changes + +- [`fd5dc788`](https://www.github.com/tauri-apps/tauri/commit/fd5dc788d10b2a048e0804b5415b84ae8f9152ea)([#7352](https://www.github.com/tauri-apps/tauri/pull/7352)) - Removed `tauri::api::file` and `tauri::api::dir` modules, use `std::fs` instead. + - Removed `tauri::api::version` module, use `semver` crate instead. +- [`fbeb5b91`](https://www.github.com/tauri-apps/tauri/commit/fbeb5b9185baeda19e865228179e3e44c165f1d9)([#7170](https://www.github.com/tauri-apps/tauri/pull/7170)) Moved `tauri::api::ipc` to `tauri::ipc` and refactored all types. +- [`fbeb5b91`](https://www.github.com/tauri-apps/tauri/commit/fbeb5b9185baeda19e865228179e3e44c165f1d9)([#7170](https://www.github.com/tauri-apps/tauri/pull/7170)) Removed the `linux-protocol-headers` feature (now always enabled) and added `linux-ipc-protocol`. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) Changed `App::handle` and `Manager::app_handle` to return a reference to an `AppHandle` instead of an owned value. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) The tray icon and menu have received a huge refactor with a lot of breaking changes in order to add new functionalities and improve the DX around using them and here is an overview of the changes: + + - All menu and tray types are now exported from `tauri::menu` and `tauri::tray` modules with new names so make sure to check the new types. + - Removed `tauri::Builder::system_tray`, instead you should use `tauri::tray::TrayIconBuilder` inside `tauri::Builder::setup` hook to create your tray icons. + - Changed `tauri::Builder::menu` to be a function to accomodate for new menu changes, you can passe `tauri::menu::Menu::default` to it to create a default menu. + - Renamed `tauri::Context` methods `system_tray_icon`, `tauri::Context::system_tray_icon_mut` and `tauri::Context::set_system_tray_icon` to `tauri::Context::tray_icon`, `tauri::Context::tray_icon_mut` and `tauri::Context::set_tray_icon` to be consistent with new type names. + - Added `RunEvent::MenuEvent` and `RunEvent::TrayIconEvent`. + - Added `App/AppHandle::set_menu`, `App/AppHandle::remove_menu`, `App/AppHandle::show_menu`, `App/AppHandle::hide_menu` and `App/AppHandle::menu` to access, remove, hide or show the app-wide menu that is used as the global menu on macOS and on all windows that don't have a specific menu set for it on Windows and Linux. + - Added `Window::set_menu`, `Window::remove_menu`, `Window::show_menu`, `Window::hide_menu`, `Window::is_menu_visible` and `Window::menu` to access, remove, hide or show the menu on this window. + - Added `Window::popup_menu` and `Window::popup_menu_at` to show a context menu on the window at the cursor position or at a specific position. You can also popup a context menu using `popup` and `popup_at` methods from `ContextMenu` trait which is implemented for `Menu` and `Submenu` types. + - Added `App/AppHandle::tray`, `App/AppHandle::tray_by_id`, `App/AppHandle::remove_tray` and `App/AppHandle::remove_tray_by_id` to access or remove a registered tray. + - Added `WindowBuilder/App/AppHandle::on_menu_event` to register a new menu event handler. + - Added `App/AppHandle::on_tray_icon_event` to register a new tray event handler. +- [`7fb419c3`](https://www.github.com/tauri-apps/tauri/commit/7fb419c326aaf72ecd556d8404377444ebb200e7)([#7535](https://www.github.com/tauri-apps/tauri/pull/7535)) Renamed `system-tray` feature flag to `tray-icon`. +- [`3a2c3e74`](https://www.github.com/tauri-apps/tauri/commit/3a2c3e74710bef9a14932dce74c351cca6215429)([#7306](https://www.github.com/tauri-apps/tauri/pull/7306)) The `Window#on_navigation` closure now receives a `&Url` argument instead of `Url`. + +## \[2.0.0-alpha.10] + +### New Features + +- [`f2d68cf7`](https://www.github.com/tauri-apps/tauri/commit/f2d68cf7d4e53443b2d53d2ae841e56c16a92514)([#6767](https://www.github.com/tauri-apps/tauri/pull/6767)) Add `incognito` option to the window configuration object. +- [`f2d68cf7`](https://www.github.com/tauri-apps/tauri/commit/f2d68cf7d4e53443b2d53d2ae841e56c16a92514)([#6767](https://www.github.com/tauri-apps/tauri/pull/6767)) Add `WindowBuilder::incognito` +- [`e0f0dce2`](https://www.github.com/tauri-apps/tauri/commit/e0f0dce220730e2822fc202463aedf0166145de7)([#6442](https://www.github.com/tauri-apps/tauri/pull/6442)) Added the `window_effects` option when creating a window and `Window::set_effects` to change it at runtime. + +### Enhancements + +- [`2d2fd6ab`](https://www.github.com/tauri-apps/tauri/commit/2d2fd6abe291ddf645fa2fdecc08111d2c0e258e)([#7191](https://www.github.com/tauri-apps/tauri/pull/7191)) Use correct HTTP method when making requests to the proxied server on mobile. +- [`b66e7d60`](https://www.github.com/tauri-apps/tauri/commit/b66e7d60f27d9a7973eae48d54cb72e30a710cca)([#7174](https://www.github.com/tauri-apps/tauri/pull/7174)) Implement `Clone` for `Channel` +- [`8124145d`](https://www.github.com/tauri-apps/tauri/commit/8124145d6c6a629809c138d2c34082e1feb4fdbf)([#7171](https://www.github.com/tauri-apps/tauri/pull/7171)) Fixes path commands not being added. +- [`4652c446`](https://www.github.com/tauri-apps/tauri/commit/4652c446b361a801252bcf45e9da39813bf85482)([#7144](https://www.github.com/tauri-apps/tauri/pull/7144)) Add `temp_dir` method to `PathResolver` + +### Bug Fixes + +- [`8e855765`](https://www.github.com/tauri-apps/tauri/commit/8e85576506f5dea066d7e9317dbcab3681baff73)([#6809](https://www.github.com/tauri-apps/tauri/pull/6809)) Fix default log path for linux and windows + +## \[2.0.0-alpha.9] + +- [`256c30c7`](https://www.github.com/tauri-apps/tauri/commit/256c30c72b737e49ced0d6a6483910dc779fc185)([#6863](https://www.github.com/tauri-apps/tauri/pull/6863)) Enhance parsing of annotated Android plugin methods to support private functions. +- [`73c803a5`](https://www.github.com/tauri-apps/tauri/commit/73c803a561181137f20366f5d52511392a619f2b)([#6837](https://www.github.com/tauri-apps/tauri/pull/6837)) Added static function `loadConfig` on the Android `PluginManager` class. +- [`edb16d13`](https://www.github.com/tauri-apps/tauri/commit/edb16d13a503da4b264ce459319fec25374c5c4f)([#6831](https://www.github.com/tauri-apps/tauri/pull/6831)) Adjust Android plugin exception error. +- [`0ab5f40d`](https://www.github.com/tauri-apps/tauri/commit/0ab5f40d3a4207f20e4440587b41c4e78f91d233)([#6813](https://www.github.com/tauri-apps/tauri/pull/6813)) Add channel API for sending data across the IPC. +- [`31444ac1`](https://www.github.com/tauri-apps/tauri/commit/31444ac196add770f2ad18012d7c18bce7538f22)([#6725](https://www.github.com/tauri-apps/tauri/pull/6725)) On Android, update proguard rules. +- [`8ce32e74`](https://www.github.com/tauri-apps/tauri/commit/8ce32e74b5573931c3bc81e8e893a6d3b9686b0e)([#6986](https://www.github.com/tauri-apps/tauri/pull/6986)) Add `default_window_icon` getter on `App` and `AppHandle`. +- [`2a5175a8`](https://www.github.com/tauri-apps/tauri/commit/2a5175a8f8f318aac9a6434271f2cc065e5989ae)([#6779](https://www.github.com/tauri-apps/tauri/pull/6779)) Enhance Android's `JSObject` return types. +- [`bb2a8ccf`](https://www.github.com/tauri-apps/tauri/commit/bb2a8ccf1356e59b98947d827d61e4e99533f2bc)([#6830](https://www.github.com/tauri-apps/tauri/pull/6830)) Use actual iOS plugin instance to run command with `throws`. +- [`94224906`](https://www.github.com/tauri-apps/tauri/commit/942249060ed12a5d21a2b21c30e0638c1d2b9df0)([#6783](https://www.github.com/tauri-apps/tauri/pull/6783)) Generate `TauriActivity` Kotlin class on the build script. +- [`7a4b1fb9`](https://www.github.com/tauri-apps/tauri/commit/7a4b1fb96da475053c61960f362bbecf18cd00d4)([#6839](https://www.github.com/tauri-apps/tauri/pull/6839)) Added support to attibutes for each command path in the `generate_handler` macro. +- [`9a79dc08`](https://www.github.com/tauri-apps/tauri/commit/9a79dc085870e0c1a5df13481ff271b8c6cc3b78)([#6947](https://www.github.com/tauri-apps/tauri/pull/6947)) Remove `enable_tauri_api` from the IPC scope. +- [`dfa407ff`](https://www.github.com/tauri-apps/tauri/commit/dfa407ffcbc8a853d61139b68b55747ae49fb231)([#6763](https://www.github.com/tauri-apps/tauri/pull/6763)) Expose plugin configuration on the Android and iOS plugin classes. +- [`3245d14b`](https://www.github.com/tauri-apps/tauri/commit/3245d14b9eb256a5c5675c7030bac7082855df47)([#6895](https://www.github.com/tauri-apps/tauri/pull/6895)) Moved the `app` feature to its own plugin in the plugins-workspace repository. +- [`09376af5`](https://www.github.com/tauri-apps/tauri/commit/09376af59424cc27803fa2820d2ac0d4cdc90a6d)([#6704](https://www.github.com/tauri-apps/tauri/pull/6704)) Moved the `cli` feature to its own plugin in the plugins-workspace repository. +- [`2d5378bf`](https://www.github.com/tauri-apps/tauri/commit/2d5378bfc1ba817ee2f331b41738a90e5997e5e8)([#6717](https://www.github.com/tauri-apps/tauri/pull/6717)) Moved the dialog APIs to its own plugin in the plugins-workspace repository. +- [`39f1b04f`](https://www.github.com/tauri-apps/tauri/commit/39f1b04f7be4966488484829cd54c8ce72a04200)([#6943](https://www.github.com/tauri-apps/tauri/pull/6943)) Moved the `event` JS APIs to a plugin. +- [`fc4d687e`](https://www.github.com/tauri-apps/tauri/commit/fc4d687ef0ef2ea069ed73c40916da733b5dcb8f)([#6716](https://www.github.com/tauri-apps/tauri/pull/6716)) Moved the file system APIs to its own plugin in the plugins-workspace repository. +- [`f78a3783`](https://www.github.com/tauri-apps/tauri/commit/f78a378344bbec48533641661d865920a8f46f8f)([#6742](https://www.github.com/tauri-apps/tauri/pull/6742)) Moved the `http` feature to its own plugin in the plugins-workspace repository. +- [`29ce9ce2`](https://www.github.com/tauri-apps/tauri/commit/29ce9ce2ce7dfb260d556d5cffd075e8fe06660c)([#6902](https://www.github.com/tauri-apps/tauri/pull/6902)) Moved the `os` feature to its own plugin in the plugins-workspace repository. +- [`60cf9ed2`](https://www.github.com/tauri-apps/tauri/commit/60cf9ed2fcd7be4df41e86cf18735efe9b6cb254)([#6905](https://www.github.com/tauri-apps/tauri/pull/6905)) Moved the `process` feature to its own plugin in the plugins-workspace repository. +- [`e1e85dc2`](https://www.github.com/tauri-apps/tauri/commit/e1e85dc2a5f656fc37867e278cae8042037740ac)([#6925](https://www.github.com/tauri-apps/tauri/pull/6925)) Moved the `protocol` scope configuration to the `asset_protocol` field in `SecurityConfig`. +- [`96639ca2`](https://www.github.com/tauri-apps/tauri/commit/96639ca239c9e4f75142fc07868ac46822111cff)([#6749](https://www.github.com/tauri-apps/tauri/pull/6749)) Moved the `shell` functionality to its own plugin in the plugins-workspace repository. +- [`e1e85dc2`](https://www.github.com/tauri-apps/tauri/commit/e1e85dc2a5f656fc37867e278cae8042037740ac)([#6925](https://www.github.com/tauri-apps/tauri/pull/6925)) Moved the updater configuration to the `BundleConfig`. +- [`b072daa3`](https://www.github.com/tauri-apps/tauri/commit/b072daa3bd3e38b808466666619ddb885052c5b2)([#6919](https://www.github.com/tauri-apps/tauri/pull/6919)) Moved the `updater` feature to its own plugin in the plugins-workspace repository. +- [`3188f376`](https://www.github.com/tauri-apps/tauri/commit/3188f3764978c6d1452ee31d5a91469691e95094)([#6883](https://www.github.com/tauri-apps/tauri/pull/6883)) Bump the MSRV to 1.65. +- [`d693e526`](https://www.github.com/tauri-apps/tauri/commit/d693e526e8607129d7f7b62a10db715f3b87d2b9)([#6780](https://www.github.com/tauri-apps/tauri/pull/6780)) Added the `onNewIntent` Plugin hook on Android. +- [`34b8f339`](https://www.github.com/tauri-apps/tauri/commit/34b8f339a4276ebff20b9d52caa103e8e3a7af66)([#6705](https://www.github.com/tauri-apps/tauri/pull/6705)) Add `app` method for the `PluginApi` struct. +- [`96639ca2`](https://www.github.com/tauri-apps/tauri/commit/96639ca239c9e4f75142fc07868ac46822111cff)([#6749](https://www.github.com/tauri-apps/tauri/pull/6749)) Moved the `tauri::api::process` module to `tauri::process`. +- [`cdad6e08`](https://www.github.com/tauri-apps/tauri/commit/cdad6e083728ea61bd6fc734ef93f6306056ea2e)([#6774](https://www.github.com/tauri-apps/tauri/pull/6774)) Changed how the `tauri-android` dependency is injected. This requires the `gen/android` project to be recreated. +- [`e1e85dc2`](https://www.github.com/tauri-apps/tauri/commit/e1e85dc2a5f656fc37867e278cae8042037740ac)([#6925](https://www.github.com/tauri-apps/tauri/pull/6925)) Removed the allowlist configuration. +- [`cebd7526`](https://www.github.com/tauri-apps/tauri/commit/cebd75261ac71b98976314a450cb292eeeec1515)([#6728](https://www.github.com/tauri-apps/tauri/pull/6728)) Moved the `clipboard` feature to its own plugin in the plugins-workspace repository. +- [`e1e85dc2`](https://www.github.com/tauri-apps/tauri/commit/e1e85dc2a5f656fc37867e278cae8042037740ac)([#6925](https://www.github.com/tauri-apps/tauri/pull/6925)) Removed extract and move APIs from `tauri::api::file`. +- [`3f17ee82`](https://www.github.com/tauri-apps/tauri/commit/3f17ee82f6ff21108806edb7b00500b8512b8dc7)([#6737](https://www.github.com/tauri-apps/tauri/pull/6737)) Moved the `global-shortcut` feature to its own plugin in the plugins-workspace repository. +- [`ae102980`](https://www.github.com/tauri-apps/tauri/commit/ae102980fcdde3f55effdc0623ea425b48d07dd1)([#6719](https://www.github.com/tauri-apps/tauri/pull/6719)) Refactor the `Context` conditional fields and only parse the tray icon on desktop. +- [`2d5378bf`](https://www.github.com/tauri-apps/tauri/commit/2d5378bfc1ba817ee2f331b41738a90e5997e5e8)([#6717](https://www.github.com/tauri-apps/tauri/pull/6717)) Remove the updater's dialog option. +- [`e1e85dc2`](https://www.github.com/tauri-apps/tauri/commit/e1e85dc2a5f656fc37867e278cae8042037740ac)([#6925](https://www.github.com/tauri-apps/tauri/pull/6925)) Removed `UpdaterEvent`. See `tauri-plugin-updater` for new usage. +- [`9a79dc08`](https://www.github.com/tauri-apps/tauri/commit/9a79dc085870e0c1a5df13481ff271b8c6cc3b78)([#6947](https://www.github.com/tauri-apps/tauri/pull/6947)) Moved the `window` JS APIs to its own plugin in the plugins-workspace repository. +- [`22a76338`](https://www.github.com/tauri-apps/tauri/commit/22a763381622407d58ae72aa24c0afff00b40e04)([#6713](https://www.github.com/tauri-apps/tauri/pull/6713)) Expose `SafePathBuf` type in `tauri::path`. +- [`c4171152`](https://www.github.com/tauri-apps/tauri/commit/c4171152c1846f425a937e82f8af1759bcc8c9ac)([#6909](https://www.github.com/tauri-apps/tauri/pull/6909)) Enable shadows by default. +- [`dfa407ff`](https://www.github.com/tauri-apps/tauri/commit/dfa407ffcbc8a853d61139b68b55747ae49fb231)([#6763](https://www.github.com/tauri-apps/tauri/pull/6763)) Change iOS plugin init function signature to `func init_plugin() -> Plugin`. + +## \[2.0.0-alpha.8] + +- Fixes boolean plugin parameters freezing the application. + - [9de89791](https://www.github.com/tauri-apps/tauri/commit/9de897919aa7236913ba6ca7c34a68099f4ff600) fix(core): iOS plugin freezing when receiving a bool parameter ([#6700](https://www.github.com/tauri-apps/tauri/pull/6700)) on 2023-04-13 + +## \[2.0.0-alpha.7] + +- Change minimum Android SDK version to 21 for the plugin library. + - [db4c9dc6](https://www.github.com/tauri-apps/tauri/commit/db4c9dc655e07ee2184fe04571f500f7910890cd) feat(core): add option to configure Android's minimum SDK version ([#6651](https://www.github.com/tauri-apps/tauri/pull/6651)) on 2023-04-07 +- Improve the `run_mobile_plugin` function error handling. + - [f0570d9f](https://www.github.com/tauri-apps/tauri/commit/f0570d9feee05792cc720d26ef32da5eaed7f797) feat(core): improve `run_mobile_plugin` error handling ([#6655](https://www.github.com/tauri-apps/tauri/pull/6655)) on 2023-04-07 +- Implement `Clone` for `plugin::PluginHandle`. + - [052c5822](https://www.github.com/tauri-apps/tauri/commit/052c5822b53d55e118674d13914f58113a0d1121) feat(core): implement Clone for PluginHandle ([#6644](https://www.github.com/tauri-apps/tauri/pull/6644)) on 2023-04-05 + +## \[2.0.0-alpha.6] + +- Fix compilation issues without the shell API features. + - [a8137927](https://www.github.com/tauri-apps/tauri/commit/a813792786b55c51173e557834f515d4b2f7ce00) fix(core): compilation issues without execute or sidecar features ([#6621](https://www.github.com/tauri-apps/tauri/pull/6621)) on 2023-04-03 + +## \[2.0.0-alpha.5] + +- Fixes ProGuard rules. + - [adf4627b](https://www.github.com/tauri-apps/tauri/commit/adf4627b73bd7098772b7f3020b4aca7228bf239) fix(core): adjust ProGuard rules ([#6588](https://www.github.com/tauri-apps/tauri/pull/6588)) on 2023-03-31 +- Added `raw` encoding option to read stdout and stderr raw bytes. + - [f992e7f5](https://www.github.com/tauri-apps/tauri/commit/f992e7f58bf975c654a3daf36780b31a32bac064) chore(changes): readd change file on 2023-04-03 +- Renamed the `default-tls` feature to `native-tls` and added `rustls-tls` feature. + - [cfdee00f](https://www.github.com/tauri-apps/tauri/commit/cfdee00f2b1455a9719bc44823fdaeabbe4c1cb2) refactor(core): fix tls features, use rustls on mobile ([#6591](https://www.github.com/tauri-apps/tauri/pull/6591)) on 2023-03-30 + +## \[2.0.0-alpha.4] + +- Allow a wry plugin to be registered at runtime. + - [ae296f3d](https://www.github.com/tauri-apps/tauri/commit/ae296f3de16fb6a8badbad5555075a5861681fe5) refactor(tauri-runtime-wry): register runtime plugin after run() ([#6478](https://www.github.com/tauri-apps/tauri/pull/6478)) on 2023-03-17 +- Inject `proguard-tauri.pro` file in the Android project. + - [bef4ef51](https://www.github.com/tauri-apps/tauri/commit/bef4ef51bc2c633b88db121c2087a38dddb7d6bf) feat(android): enable minify on release, add proguard rules ([#6257](https://www.github.com/tauri-apps/tauri/pull/6257)) on 2023-02-13 +- Return `bool` in the invoke handler. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 +- Use correct lib name in xcode project. + - [d1752fb1](https://www.github.com/tauri-apps/tauri/commit/d1752fb1f6223fa47d224cb6c62df9b74944a507) fix(cli): use correct lib name in xcode project ([#6387](https://www.github.com/tauri-apps/tauri/pull/6387)) on 2023-03-08 +- Run Android and iOS native plugins on the invoke handler if a Rust plugin command is not found. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 +- Added `initialize_android_plugin` and `initialize_ios_plugin` APIs on `AppHandle`. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 +- Changed the plugin setup hook to take a second argument of type `PluginApi`. + - [6aaba834](https://www.github.com/tauri-apps/tauri/commit/6aaba83476339fa413fe34d28877a932cb485117) refactor(plugin): add PluginApi and PluginHandle, expose on setup hook ([#6291](https://www.github.com/tauri-apps/tauri/pull/6291)) on 2023-02-16 +- Refactored the implementation of the `mobile_entry_point` macro. + - [9feab904](https://www.github.com/tauri-apps/tauri/commit/9feab904bf08b5c168d4779c21d0419409a68d30) feat(core): add API to call Android plugin ([#6239](https://www.github.com/tauri-apps/tauri/pull/6239)) on 2023-02-10 +- Removed the attohttpc client. The `reqwest-*` Cargo features were also removed. + - [dddaa943](https://www.github.com/tauri-apps/tauri/commit/dddaa943e7e0bf13935d567ef2f3f73e1c913300) refactor(core): remove attohttpc client, closes [#6415](https://www.github.com/tauri-apps/tauri/pull/6415) ([#6468](https://www.github.com/tauri-apps/tauri/pull/6468)) on 2023-03-17 +- Added `App::run_mobile_plugin` and `AppHandle::run_mobile_plugin`. + - [bfb2ab24](https://www.github.com/tauri-apps/tauri/commit/bfb2ab24e0b1d0860ea6e37688b5209541f0eda1) feat: add API to call iOS plugin ([#6242](https://www.github.com/tauri-apps/tauri/pull/6242)) on 2023-02-11 +- Added the `shadow` option when creating a window and `Window::set_shadow`. + - [a81750d7](https://www.github.com/tauri-apps/tauri/commit/a81750d779bc72f0fdb7de90b7fbddfd8049b328) feat(core): add shadow APIs ([#6206](https://www.github.com/tauri-apps/tauri/pull/6206)) on 2023-02-08 +- Implemented `with_webview` on Android and iOS. + - [05dad087](https://www.github.com/tauri-apps/tauri/commit/05dad0876842e2a7334431247d49365cee835d3e) feat: initial work for iOS plugins ([#6205](https://www.github.com/tauri-apps/tauri/pull/6205)) on 2023-02-11 + +## \[2.0.0-alpha.3] + +- Update gtk to 0.16. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Show all application logs on iOS. + - [dee9460f](https://www.github.com/tauri-apps/tauri/commit/dee9460f9c9bc92e9c638e7691e616849ac2085b) feat: keep CLI alive when iOS app exits, show logs, closes [#5855](https://www.github.com/tauri-apps/tauri/pull/5855) ([#5902](https://www.github.com/tauri-apps/tauri/pull/5902)) on 2022-12-27 +- Bump the MSRV to 1.64. + - [7eb9aa75](https://www.github.com/tauri-apps/tauri/commit/7eb9aa75cfd6a3176d3f566fdda02d88aa529b0f) Update gtk to 0.16 ([#6155](https://www.github.com/tauri-apps/tauri/pull/6155)) on 2023-01-30 +- Only proxy the dev server on mobile to simplify desktop usage. + - [78eaadae](https://www.github.com/tauri-apps/tauri/commit/78eaadae2e75ab165d1970e592bb1455bb8636e3) refactor(core): only proxy on mobile ([#6126](https://www.github.com/tauri-apps/tauri/pull/6126)) on 2023-01-23 +- Removed mobile logging initialization, which will be handled by `tauri-plugin-log`. + - [](https://www.github.com/tauri-apps/tauri/commit/undefined) on undefined +- Update rfd to 0.11. + - [f0a1d9cd](https://www.github.com/tauri-apps/tauri/commit/f0a1d9cdbcfb645ce1c5f1cdd597f764991772cd) chore: update rfd and wry versions ([#6174](https://www.github.com/tauri-apps/tauri/pull/6174)) on 2023-02-03 + +## \[2.0.0-alpha.2] + +- Fix the filesystem scope allowing sub-directories of the directory picked by the dialog when `recursive` option was `false`. + - [9ad0a9a0](https://www.github.com/tauri-apps/tauri/commit/9ad0a9a0aa88a67c3d81ef84df4aad23556affde) Merge pull request from GHSA-6mv3-wm7j-h4w5 on 2022-12-22 + +## \[2.0.0-alpha.1] + +- Implement response cache on the dev server proxy, used when the server responds with status 304. + - [3ad5e72f](https://www.github.com/tauri-apps/tauri/commit/3ad5e72ff147b76267c010c778a3b94bba209bb0) feat(core): cache dev server proxy responses for 304 status code ([#5818](https://www.github.com/tauri-apps/tauri/pull/5818)) on 2022-12-12 +- Properly proxy dev server requests with query strings and fragments. + - [a9b4cf20](https://www.github.com/tauri-apps/tauri/commit/a9b4cf20a3e9a5cc984727a56111591504e084c0) fix(core): use entire request URL on dev server proxy ([#5819](https://www.github.com/tauri-apps/tauri/pull/5819)) on 2022-12-12 + +## \[2.0.0-alpha.0] + +- Added the `default-tls` and `reqwest-default-tls` Cargo features for enabling TLS suppport to connect over HTTPS. + - [f6f9192a](https://www.github.com/tauri-apps/tauri/commit/f6f9192aa51bd842df8aa1d1aa538b12aa6c2d29) fix(core): Android compilation on Windows ([#5658](https://www.github.com/tauri-apps/tauri/pull/5658)) on 2022-11-20 +- **Breaking change:** Use the custom protocol as a proxy to the development server on all platforms except Linux. + - [6f061504](https://www.github.com/tauri-apps/tauri/commit/6f0615044d09ec58393a7ebca5e45bb175e20db3) feat(cli): add `android dev` and `ios dev` commands ([#4982](https://www.github.com/tauri-apps/tauri/pull/4982)) on 2022-08-20 +- Support `with_webview` for Android platform alowing execution of JNI code in context. + - [8ea87e9c](https://www.github.com/tauri-apps/tauri/commit/8ea87e9c9ca8ba4c7017c8281f78aacd08f45785) feat(android): with_webview access for jni execution ([#5148](https://www.github.com/tauri-apps/tauri/pull/5148)) on 2022-09-08 +- First mobile alpha release! + - [fa3a1098](https://www.github.com/tauri-apps/tauri/commit/fa3a10988a03aed1b66fb17d893b1a9adb90f7cd) feat(ci): prepare 2.0.0-alpha.0 ([#5786](https://www.github.com/tauri-apps/tauri/pull/5786)) on 2022-12-08 +- **Breaking change:** The window creation and setup hook are now called when the event loop is ready. + - [b4622ea4](https://www.github.com/tauri-apps/tauri/commit/b4622ea4d32720bc3bb2a8c740bb70cfe32fed93) refactor(app): run setup and window creation when event loop is ready ([#4914](https://www.github.com/tauri-apps/tauri/pull/4914)) on 2022-08-11 +- Export types required by the `mobile_entry_point` macro. + - [98904863](https://www.github.com/tauri-apps/tauri/commit/9890486321c9c79ccfb7c547fafee85b5c3ffa71) feat(core): add `mobile_entry_point` macro ([#4983](https://www.github.com/tauri-apps/tauri/pull/4983)) on 2022-08-21 + ## \[1.4.1] ### Bug Fixes diff --git a/core/tauri/Cargo.toml b/core/tauri/Cargo.toml index 9b81aa8febe..537929c46db 100644 --- a/core/tauri/Cargo.toml +++ b/core/tauri/Cargo.toml @@ -1,31 +1,27 @@ [package] -authors = [ "Tauri Programme within The Commons Conservancy" ] -categories = [ "gui", "web-programming" ] +name = "tauri" +version = "2.0.0-alpha.11" description = "Make tiny, secure apps for all desktop platforms with Tauri" -edition = "2021" -rust-version = "1.60" exclude = [ "/test", "/.scripts", "CHANGELOG.md", "/target" ] -homepage = "https://tauri.app" -license = "Apache-2.0 OR MIT" -name = "tauri" readme = "README.md" -repository = "https://github.com/tauri-apps/tauri" -version = "1.4.1" +links = "Tauri" +authors = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +categories = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } [package.metadata.docs.rs] no-default-features = true features = [ "wry", "custom-protocol", - "api-all", - "windows7-compat", - "cli", - "updater", - "fs-extract-api", - "system-tray", + "tray-icon", "devtools", - "http-multipart", "icon-png", + "protocol-asset", "test", "dox" ] @@ -39,6 +35,7 @@ targets = [ [package.metadata.cargo-udeps.ignore] normal = [ "reqwest" ] +build = [ "tauri-build" ] [dependencies] serde_json = { version = "1.0", features = [ "raw_value" ] } @@ -46,259 +43,109 @@ serde = { version = "1.0", features = [ "derive" ] } tokio = { version = "1", features = [ "rt", "rt-multi-thread", "sync", "fs", "io-util" ] } futures-util = "0.3" uuid = { version = "1", features = [ "v4" ] } -url = { version = "2.3" } +url = { version = "2.4" } anyhow = "1.0" thiserror = "1.0" once_cell = "1" -tauri-runtime = { version = "0.14.0", path = "../tauri-runtime" } -tauri-macros = { version = "1.4.0", path = "../tauri-macros" } -tauri-utils = { version = "1.4.0", features = [ "resources" ], path = "../tauri-utils" } -tauri-runtime-wry = { version = "0.14.0", path = "../tauri-runtime-wry", optional = true } +tauri-runtime = { version = "1.0.0-alpha.0", path = "../tauri-runtime" } +tauri-macros = { version = "2.0.0-alpha.7", path = "../tauri-macros" } +tauri-utils = { version = "2.0.0-alpha.7", features = [ "resources" ], path = "../tauri-utils" } +tauri-runtime-wry = { version = "1.0.0-alpha.0", path = "../tauri-runtime-wry", optional = true } rand = "0.8" -semver = { version = "1.0", features = [ "serde" ] } serde_repr = "0.1" -state = "0.5" -tar = "0.4.38" -tempfile = "3" -zip = { version = "0.6", default-features = false, optional = true } -ignore = "0.4" -flate2 = "1.0" +state = "0.6" http = "0.2" dirs-next = "2.0" -percent-encoding = "2.2" -base64 = { version = "0.21", optional = true } -clap = { version = "3", optional = true } -reqwest = { version = "0.11", features = [ "json", "stream" ], optional = true } -bytes = { version = "1", features = [ "serde" ], optional = true } -open = { version = "3.2", optional = true } -shared_child = { version = "1.0", optional = true } -os_pipe = { version = "1.0", optional = true } +percent-encoding = "2.3" +reqwest = { version = "0.11", default-features = false, features = [ "json", "stream" ] } +bytes = { version = "1", features = [ "serde" ] } raw-window-handle = "0.5" -minisign-verify = { version = "0.2", optional = true } -time = { version = "0.3", features = [ "parsing", "formatting" ], optional = true } -os_info = { version = "3", optional = true } -regex = { version = "1.6.0", optional = true } glob = "0.3" -data-url = { version = "0.2", optional = true } +mime = "0.3" +data-url = { version = "0.3", optional = true } serialize-to-javascript = "=0.1.1" -infer = { version = "0.9", optional = true } +infer = { version = "0.15", optional = true } png = { version = "0.17", optional = true } -ico = { version = "0.2.0", optional = true } -encoding_rs = "0.8.31" -sys-locale = { version = "0.2.3", optional = true } +ico = { version = "0.3.0", optional = true } -[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, features = [ "gtk3", "common-controls-v6" ] } -notify-rust = { version = "4.5", optional = true } +[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\", target_os = \"windows\", target_os = \"macos\"))".dependencies] +muda = { version = "0.8", default-features = false } +tray-icon = { version = "0.8", default-features = false, optional = true } [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] -gtk = { version = "0.15", features = [ "v3_20" ] } -glib = "0.15" -webkit2gtk = { version = "0.18.2", features = [ "v2_22" ] } +gtk = { version = "0.16", features = [ "v3_24" ] } +glib = "0.16" +webkit2gtk = { version = "1.1", features = [ "v2_38" ] } [target."cfg(target_os = \"macos\")".dependencies] embed_plist = "1.2" -cocoa = "0.24" +cocoa = "0.25" objc = "0.2" [target."cfg(windows)".dependencies] -webview2-com = "0.19.1" -win7-notifications = { version = "0.3.1", optional = true } +webview2-com = "0.25" [target."cfg(windows)".dependencies.windows] - version = "0.39.0" + version = "0.48" features = [ "Win32_Foundation" ] +[target."cfg(any(target_os = \"android\", target_os = \"ios\"))".dependencies] +log = "0.4" +heck = "0.4" + +[target."cfg(target_os = \"android\")".dependencies] +jni = "0.21" + +[target."cfg(target_os = \"ios\")".dependencies] +libc = "0.2" +objc = "0.2" +cocoa = "0.25" +swift-rs = "1.0.5" + [build-dependencies] heck = "0.4" once_cell = "1" +tauri-build = { path = "../tauri-build/", version = "2.0.0-alpha.8" } [dev-dependencies] -mockito = "0.31" -proptest = "1.0.0" +proptest = "1.2.0" quickcheck = "1.0.3" quickcheck_macros = "1.0.0" serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" tauri = { path = ".", default-features = false, features = [ "wry" ] } -tokio-test = "0.4.2" tokio = { version = "1", features = [ "full" ] } -cargo_toml = "0.11" +cargo_toml = "0.15" [features] -default = [ "wry", "compression", "objc-exception" ] +default = [ + "wry", + "compression", + "objc-exception", + "tray-icon?/common-controls-v6", + "muda/common-controls-v6" +] +tray-icon = [ "dep:tray-icon" ] test = [ ] compression = [ "tauri-macros/compression", "tauri-utils/compression" ] wry = [ "tauri-runtime-wry" ] objc-exception = [ "tauri-runtime-wry/objc-exception" ] -linux-protocol-headers = [ "tauri-runtime-wry/linux-headers", "webkit2gtk/v2_36" ] +linux-ipc-protocol = [ "tauri-runtime-wry/linux-protocol-body", "webkit2gtk/v2_40" ] +linux-libxdo = [ "tray-icon/libxdo", "muda/libxdo" ] isolation = [ "tauri-utils/isolation", "tauri-macros/isolation" ] custom-protocol = [ "tauri-macros/custom-protocol" ] -updater = [ - "minisign-verify", - "time", - "base64", - "http-api", - "dialog-ask", - "fs-extract-api" -] -http-api = [ "reqwest", "bytes" ] -http-multipart = [ "reqwest/multipart" ] -os-api = [ "sys-locale" ] -shell-open-api = [ "open", "regex", "tauri-macros/shell-scope" ] -fs-extract-api = [ "zip" ] -reqwest-client = [ "http-api" ] -reqwest-native-tls-vendored = [ "native-tls-vendored" ] +native-tls = [ "reqwest/native-tls" ] native-tls-vendored = [ "reqwest/native-tls-vendored" ] -process-command-api = [ "shared_child", "os_pipe" ] -global-shortcut = [ - "tauri-runtime/global-shortcut", - "tauri-runtime-wry/global-shortcut" -] -clipboard = [ "tauri-runtime/clipboard", "tauri-runtime-wry/clipboard" ] -dialog = [ "rfd" ] -notification = [ "notify-rust" ] -cli = [ "clap" ] -system-tray = [ "tauri-runtime/system-tray", "tauri-runtime-wry/system-tray" ] +rustls-tls = [ "reqwest/rustls-tls" ] devtools = [ "tauri-runtime/devtools", "tauri-runtime-wry/devtools" ] dox = [ "tauri-runtime-wry/dox" ] +process-relaunch-dangerous-allow-symlink-macos = [ "tauri-utils/process-relaunch-dangerous-allow-symlink-macos" ] macos-private-api = [ "tauri-runtime/macos-private-api", "tauri-runtime-wry/macos-private-api" ] -windows7-compat = [ "win7-notifications" ] window-data-url = [ "data-url" ] -api-all = [ - "clipboard-all", - "dialog-all", - "fs-all", - "global-shortcut-all", - "http-all", - "notification-all", - "os-all", - "path-all", - "process-all", - "protocol-all", - "shell-all", - "window-all", - "app-all" -] -clipboard-all = [ "clipboard-write-text", "clipboard-read-text" ] -clipboard-read-text = [ "clipboard" ] -clipboard-write-text = [ "clipboard" ] -dialog-all = [ "dialog-open", "dialog-save", "dialog-message", "dialog-ask" ] -dialog-ask = [ "dialog" ] -dialog-confirm = [ "dialog" ] -dialog-message = [ "dialog" ] -dialog-open = [ "dialog" ] -dialog-save = [ "dialog" ] -fs-all = [ - "fs-copy-file", - "fs-create-dir", - "fs-exists", - "fs-read-file", - "fs-read-dir", - "fs-remove-dir", - "fs-remove-file", - "fs-rename-file", - "fs-write-file" -] -fs-copy-file = [ ] -fs-create-dir = [ ] -fs-exists = [ ] -fs-read-file = [ ] -fs-read-dir = [ ] -fs-remove-dir = [ ] -fs-remove-file = [ ] -fs-rename-file = [ ] -fs-write-file = [ ] -global-shortcut-all = [ "global-shortcut" ] -http-all = [ "http-request" ] -http-request = [ "http-api" ] -notification-all = [ "notification", "dialog-ask" ] -os-all = [ "os_info", "os-api" ] -path-all = [ ] -process-all = [ "process-relaunch", "process-exit" ] -process-exit = [ ] -process-relaunch = [ ] -process-relaunch-dangerous-allow-symlink-macos = [ "tauri-utils/process-relaunch-dangerous-allow-symlink-macos" ] -protocol-all = [ "protocol-asset" ] protocol-asset = [ ] -shell-all = [ "shell-execute", "shell-sidecar", "shell-open" ] -shell-execute = [ "process-command-api", "regex", "tauri-macros/shell-scope" ] -shell-sidecar = [ "process-command-api", "regex", "tauri-macros/shell-scope" ] -shell-open = [ "shell-open-api" ] -window-all = [ - "window-create", - "window-center", - "window-request-user-attention", - "window-set-resizable", - "window-set-maximizable", - "window-set-minimizable", - "window-set-closable", - "window-set-title", - "window-maximize", - "window-unmaximize", - "window-minimize", - "window-unminimize", - "window-show", - "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", - "window-set-position", - "window-set-fullscreen", - "window-set-focus", - "window-set-icon", - "window-set-skip-taskbar", - "window-set-cursor-grab", - "window-set-cursor-visible", - "window-set-cursor-icon", - "window-set-cursor-position", - "window-set-ignore-cursor-events", - "window-start-dragging", - "window-print" -] -window-create = [ ] -window-center = [ ] -window-request-user-attention = [ ] -window-set-resizable = [ ] -window-set-maximizable = [ ] -window-set-minimizable = [ ] -window-set-closable = [ ] -window-set-title = [ ] -window-maximize = [ ] -window-unmaximize = [ ] -window-minimize = [ ] -window-unminimize = [ ] -window-show = [ ] -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 = [ ] -window-set-position = [ ] -window-set-fullscreen = [ ] -window-set-focus = [ ] -window-set-icon = [ ] -window-set-skip-taskbar = [ ] -window-set-cursor-grab = [ ] -window-set-cursor-visible = [ ] -window-set-cursor-icon = [ ] -window-set-cursor-position = [ ] -window-set-ignore-cursor-events = [ ] -window-start-dragging = [ ] -window-print = [ ] -app-all = [ "app-show", "app-hide" ] -app-show = [ ] -app-hide = [ ] config-json5 = [ "tauri-macros/config-json5" ] config-toml = [ "tauri-macros/config-toml" ] icon-ico = [ "infer", "ico" ] @@ -315,7 +162,6 @@ path = "../../examples/helloworld/main.rs" [[example]] name = "multiwindow" path = "../../examples/multiwindow/main.rs" -required-features = [ "window-create" ] [[example]] name = "parent-window" @@ -324,7 +170,6 @@ path = "../../examples/parent-window/main.rs" [[example]] name = "navigation" path = "../../examples/navigation/main.rs" -required-features = [ "window-create" ] [[example]] name = "splashscreen" diff --git a/core/tauri/build.rs b/core/tauri/build.rs index bd095e6b06f..669de4270fd 100644 --- a/core/tauri/build.rs +++ b/core/tauri/build.rs @@ -3,12 +3,18 @@ // SPDX-License-Identifier: MIT use heck::AsShoutySnakeCase; -use heck::AsSnakeCase; -use heck::ToSnakeCase; use once_cell::sync::OnceCell; -use std::{path::Path, sync::Mutex}; +use std::env::var_os; +use std::fs::read_dir; +use std::fs::read_to_string; +use std::fs::write; +use std::{ + env::var, + path::{Path, PathBuf}, + sync::Mutex, +}; static CHECKED_FEATURES: OnceCell>> = OnceCell::new(); @@ -38,107 +44,18 @@ fn alias(alias: &str, has_feature: bool) { fn main() { alias("custom_protocol", has_feature("custom-protocol")); alias("dev", !has_feature("custom-protocol")); - alias("updater", has_feature("updater")); let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let mobile = target_os == "ios" || target_os == "android"; alias("desktop", !mobile); alias("mobile", mobile); - let api_all = has_feature("api-all"); - alias("api_all", api_all); - - alias_module( - "fs", - &[ - "read-file", - "write-file", - "read-dir", - "copy-file", - "create-dir", - "remove-dir", - "remove-file", - "rename-file", - "exists", - ], - api_all, + alias( + "ipc_custom_protocol", + target_os != "android" && (target_os != "linux" || has_feature("linux-ipc-protocol")), ); - alias_module( - "window", - &[ - "create", - "center", - "request-user-attention", - "set-resizable", - "set-maximizable", - "set-minimizable", - "set-closable", - "set-title", - "maximize", - "unmaximize", - "minimize", - "unminimize", - "show", - "hide", - "close", - "set-decorations", - "set-always-on-top", - "set-content-protected", - "set-size", - "set-min-size", - "set-max-size", - "set-position", - "set-fullscreen", - "set-focus", - "set-icon", - "set-skip-taskbar", - "set-cursor-grab", - "set-cursor-visible", - "set-cursor-icon", - "set-cursor-position", - "set-ignore-cursor-events", - "start-dragging", - "print", - ], - api_all, - ); - - alias_module("shell", &["execute", "sidecar", "open"], api_all); - // helper for the command module macro - let shell_script = has_feature("shell-execute") || has_feature("shell-sidecar"); - alias("shell_script", shell_script); - alias("shell_scope", has_feature("shell-open-api") || shell_script); - - if !mobile { - alias_module( - "dialog", - &["open", "save", "message", "ask", "confirm"], - api_all, - ); - } - - alias_module("http", &["request"], api_all); - - alias("cli", has_feature("cli")); - - if !mobile { - alias_module("notification", &[], api_all); - alias_module("global-shortcut", &[], api_all); - } - alias_module("os", &[], api_all); - alias_module("path", &[], api_all); - - alias_module("protocol", &["asset"], api_all); - - alias_module("process", &["relaunch", "exit"], api_all); - - alias_module("clipboard", &["write-text", "read-text"], api_all); - - alias_module("app", &["show", "hide"], api_all); - - let checked_features_out_path = - Path::new(&std::env::var("OUT_DIR").unwrap()).join("checked_features"); + let checked_features_out_path = Path::new(&var("OUT_DIR").unwrap()).join("checked_features"); std::fs::write( checked_features_out_path, CHECKED_FEATURES.get().unwrap().lock().unwrap().join(","), @@ -147,43 +64,78 @@ fn main() { // workaround needed to prevent `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() - { + if is_tauri_workspace && target_os == "windows" && Ok("msvc") == target_env.as_deref() { add_manifest(); } -} -// create aliases for the given module with its apis. -// each api is translated into a feature flag in the format of `-` -// and aliased as `_`. -// -// The `-all` feature is also aliased to `_all`. -// -// If any of the features is enabled, the `_any` alias is created. -// -// 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!("{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; - alias( - &format!("{}_{}", AsSnakeCase(module), AsSnakeCase(api)), - has, - ); - any = any || has; + if target_os == "android" { + if let Ok(kotlin_out_dir) = std::env::var("WRY_ANDROID_KOTLIN_FILES_OUT_DIR") { + fn env_var(var: &str) -> String { + std::env::var(var).unwrap_or_else(|_| { + panic!( + "`{}` is not set, which is needed to generate the kotlin files for android.", + var + ) + }) + } + + let package = env_var("WRY_ANDROID_PACKAGE"); + let library = env_var("WRY_ANDROID_LIBRARY"); + + let kotlin_out_dir = PathBuf::from(&kotlin_out_dir) + .canonicalize() + .unwrap_or_else(move |_| { + panic!("Failed to canonicalize `WRY_ANDROID_KOTLIN_FILES_OUT_DIR` path {kotlin_out_dir}") + }); + + let kotlin_files_path = + PathBuf::from(env_var("CARGO_MANIFEST_DIR")).join("mobile/android-codegen"); + println!("cargo:rerun-if-changed={}", kotlin_files_path.display()); + let kotlin_files = + read_dir(kotlin_files_path).expect("failed to read Android codegen directory"); + + for file in kotlin_files { + let file = file.unwrap(); + + let content = read_to_string(file.path()) + .expect("failed to read kotlin file as string") + .replace("{{package}}", &package) + .replace("{{library}}", &library); + + let out_path = kotlin_out_dir.join(file.file_name()); + write(&out_path, content).expect("Failed to write kotlin file"); + println!("cargo:rerun-if-changed={}", out_path.display()); + } + } + + if let Some(project_dir) = var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) { + let tauri_proguard = include_str!("./mobile/proguard-tauri.pro").replace( + "$PACKAGE", + &var("WRY_ANDROID_PACKAGE").expect("missing `WRY_ANDROID_PACKAGE` environment variable"), + ); + std::fs::write( + project_dir.join("app").join("proguard-tauri.pro"), + tauri_proguard, + ) + .expect("failed to write proguard-tauri.pro"); + } + + let lib_path = + PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("mobile/android"); + println!("cargo:android_library_path={}", lib_path.display()); } - alias(&format!("{}_any", AsSnakeCase(module)), any); + #[cfg(target_os = "macos")] + { + if target_os == "ios" { + let lib_path = + PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("mobile/ios-api"); + tauri_build::mobile::link_swift_library("Tauri", &lib_path); + println!("cargo:ios_library_path={}", lib_path.display()); + } + } } fn add_manifest() { diff --git a/core/tauri/mobile/android-codegen/TauriActivity.kt b/core/tauri/mobile/android-codegen/TauriActivity.kt new file mode 100644 index 00000000000..d610116e8dc --- /dev/null +++ b/core/tauri/mobile/android-codegen/TauriActivity.kt @@ -0,0 +1,27 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +package {{package}} + +import android.os.Bundle +import android.content.Intent +import app.tauri.plugin.PluginManager + +abstract class TauriActivity : WryActivity() { + var pluginManager: PluginManager = PluginManager(this) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (intent != null) { + pluginManager.onNewIntent(intent) + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + pluginManager.onNewIntent(intent) + } +} diff --git a/core/tauri/mobile/android/.gitignore b/core/tauri/mobile/android/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/core/tauri/mobile/android/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/tauri/mobile/android/build.gradle.kts b/core/tauri/mobile/android/build.gradle.kts new file mode 100644 index 00000000000..d23ad0fb148 --- /dev/null +++ b/core/tauri/mobile/android/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.tauri" + compileSdk = 33 + + defaultConfig { + minSdk = 21 + targetSdk = 33 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("proguard-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.7.0") + implementation("androidx.appcompat:appcompat:1.6.0") + implementation("com.google.android.material:material:1.7.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} diff --git a/core/tauri/mobile/android/proguard-rules.pro b/core/tauri/mobile/android/proguard-rules.pro new file mode 100644 index 00000000000..fed38c70362 --- /dev/null +++ b/core/tauri/mobile/android/proguard-rules.pro @@ -0,0 +1,25 @@ +-keep class app.tauri.** { + @app.tauri.JniMethod public ; + native ; +} + +-keep class app.tauri.plugin.JSArray { + public (...); +} + +-keepclassmembers class org.json.JSONArray { + public put(...); +} + +-keep class app.tauri.plugin.JSObject { + public (...); + public put(...); +} + +-keep @app.tauri.annotation.TauriPlugin public class * { + @app.tauri.annotation.Command public ; + @app.tauri.annotation.PermissionCallback ; + @app.tauri.annotation.ActivityCallback ; + @app.tauri.annotation.Permission ; + public (...); +} diff --git a/core/tauri/mobile/android/src/androidTest/java/app/tauri/ExampleInstrumentedTest.kt b/core/tauri/mobile/android/src/androidTest/java/app/tauri/ExampleInstrumentedTest.kt new file mode 100644 index 00000000000..79199103804 --- /dev/null +++ b/core/tauri/mobile/android/src/androidTest/java/app/tauri/ExampleInstrumentedTest.kt @@ -0,0 +1,28 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("app.tauri.test", appContext.packageName) + } +} diff --git a/core/tauri/mobile/android/src/main/AndroidManifest.xml b/core/tauri/mobile/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..9a40236b947 --- /dev/null +++ b/core/tauri/mobile/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/FsUtils.kt b/core/tauri/mobile/android/src/main/java/app/tauri/FsUtils.kt new file mode 100644 index 00000000000..43d09a25264 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/FsUtils.kt @@ -0,0 +1,208 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import android.content.ContentUris +import android.content.Context +import android.content.res.AssetManager +import android.database.Cursor +import android.net.Uri +import android.os.Environment +import android.provider.DocumentsContract +import android.provider.MediaStore +import android.provider.OpenableColumns +import java.io.File +import java.io.FileOutputStream +import kotlin.math.min + +internal class FsUtils { + companion object { + fun readAsset(assetManager: AssetManager, fileName: String): String { + assetManager.open(fileName).bufferedReader().use { + return it.readText() + } + } + + fun getFileUrlForUri(context: Context, uri: Uri): String? { + // DocumentProvider + if (DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + val docId: String = DocumentsContract.getDocumentId(uri) + val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val type = split[0] + if ("primary".equals(type, ignoreCase = true)) { + return legacyPrimaryPath(split[1]) + } else { + val splitIndex = docId.indexOf(':', 1) + val tag = docId.substring(0, splitIndex) + val path = docId.substring(splitIndex + 1) + val nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag) + if (nonPrimaryVolume != null) { + val result = "$nonPrimaryVolume/$path" + val file = File(result) + return if (file.exists() && file.canRead()) { + result + } else null + } + } + } else if (isDownloadsDocument(uri)) { + val id: String = DocumentsContract.getDocumentId(uri) + val contentUri: Uri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), + java.lang.Long.valueOf(id) + ) + return getDataColumn(context, contentUri, null, null) + } else if (isMediaDocument(uri)) { + val docId: String = DocumentsContract.getDocumentId(uri) + val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val type = split[0] + var contentUri: Uri? = null + when (type) { + "image" -> { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + "video" -> { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + } + "audio" -> { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + } + val selection = "_id=?" + val selectionArgs = arrayOf(split[1]) + if (contentUri != null) { + return getDataColumn(context, contentUri, selection, selectionArgs) + } + } + } else if ("content".equals(uri.scheme, ignoreCase = true)) { + // Return the remote address + return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn( + context, + uri, + null, + null + ) + } else if ("file".equals(uri.scheme, ignoreCase = true)) { + return uri.path + } + return null + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + private fun getDataColumn( + context: Context, + uri: Uri, + selection: String?, + selectionArgs: Array? + ): String? { + var path: String? = null + var cursor: Cursor? = null + val column = "_data" + val projection = arrayOf(column) + try { + cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) + if (cursor != null && cursor.moveToFirst()) { + val index = cursor.getColumnIndexOrThrow(column) + path = cursor.getString(index) + } + } catch (ex: IllegalArgumentException) { + return getCopyFilePath(uri, context) + } finally { + cursor?.close() + } + return path ?: getCopyFilePath(uri, context) + } + + private fun getCopyFilePath(uri: Uri, context: Context): String? { + val cursor = context.contentResolver.query(uri, null, null, null, null)!! + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + val name = cursor.getString(nameIndex) + val file = File(context.filesDir, name) + try { + val inputStream = context.contentResolver.openInputStream(uri) + val outputStream = FileOutputStream(file) + var read: Int + val maxBufferSize = 1024 * 1024 + val bufferSize = min(inputStream!!.available(), maxBufferSize) + val buffers = ByteArray(bufferSize) + while (inputStream.read(buffers).also { read = it } != -1) { + outputStream.write(buffers, 0, read) + } + inputStream.close() + outputStream.close() + } catch (e: Exception) { + return null + } finally { + cursor.close() + } + return file.path + } + + private fun legacyPrimaryPath(pathPart: String): String { + return Environment.getExternalStorageDirectory().toString() + "/" + pathPart + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private fun isExternalStorageDocument(uri: Uri): Boolean { + return "com.android.externalstorage.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private fun isDownloadsDocument(uri: Uri): Boolean { + return "com.android.providers.downloads.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private fun isMediaDocument(uri: Uri): Boolean { + return "com.android.providers.media.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + private fun isGooglePhotosUri(uri: Uri): Boolean { + return "com.google.android.apps.photos.content" == uri.authority + } + + private fun getPathToNonPrimaryVolume(context: Context, tag: String): String? { + val volumes = context.externalCacheDirs + if (volumes != null) { + for (volume in volumes) { + if (volume != null) { + val path = volume.absolutePath + val index = path.indexOf(tag) + if (index != -1) { + return path.substring(0, index) + tag + } + } + } + } + return null + } + } +} diff --git a/examples/sidecar/src-tauri/build.rs b/core/tauri/mobile/android/src/main/java/app/tauri/JniMethod.kt similarity index 59% rename from examples/sidecar/src-tauri/build.rs rename to core/tauri/mobile/android/src/main/java/app/tauri/JniMethod.kt index b055ec37c77..d12778b5ffc 100644 --- a/examples/sidecar/src-tauri/build.rs +++ b/core/tauri/mobile/android/src/main/java/app/tauri/JniMethod.kt @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -fn main() { - tauri_build::build() -} +package app.tauri + +@Retention(AnnotationRetention.RUNTIME) +internal annotation class JniMethod diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/Logger.kt b/core/tauri/mobile/android/src/main/java/app/tauri/Logger.kt new file mode 100644 index 00000000000..a4789dd5032 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/Logger.kt @@ -0,0 +1,85 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/Logger.java + +import android.text.TextUtils; +import android.util.Log; + +class Logger { + companion object { + private const val LOG_TAG_CORE = "Tauri" + + fun tags(vararg subtags: String): String { + return if (subtags.isNotEmpty()) { + LOG_TAG_CORE + "/" + TextUtils.join("/", subtags) + } else LOG_TAG_CORE + } + + fun verbose(message: String) { + verbose(LOG_TAG_CORE, message) + } + + fun verbose(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.v(tag, message) + } + + fun debug(message: String) { + debug(LOG_TAG_CORE, message) + } + + fun debug(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.d(tag, message) + } + + fun info(message: String) { + info(LOG_TAG_CORE, message) + } + + fun info(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.i(tag, message) + } + + fun warn(message: String) { + warn(LOG_TAG_CORE, message) + } + + fun warn(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.w(tag, message) + } + + fun error(message: String) { + error(LOG_TAG_CORE, message, null) + } + + fun error(message: String, e: Throwable?) { + error(LOG_TAG_CORE, message, e) + } + + fun error(tag: String, message: String, e: Throwable?) { + if (!shouldLog()) { + return + } + Log.e(tag, message, e) + } + + private fun shouldLog(): Boolean { + return BuildConfig.DEBUG + } + } +} diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/PathPlugin.kt b/core/tauri/mobile/android/src/main/java/app/tauri/PathPlugin.kt new file mode 100644 index 00000000000..65147db5a41 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/PathPlugin.kt @@ -0,0 +1,78 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import android.app.Activity +import android.os.Environment +import app.tauri.annotation.Command +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Plugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.JSObject + +@TauriPlugin +class PathPlugin(private val activity: Activity): Plugin(activity) { + private fun resolvePath(invoke: Invoke, path: String?) { + val obj = JSObject() + obj.put("path", path) + invoke.resolve(obj) + } + + @Command + fun getAudioDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_MUSIC)?.absolutePath) + } + + @Command + fun getExternalCacheDir(invoke: Invoke) { + resolvePath(invoke, activity.externalCacheDir?.absolutePath) + } + + @Command + fun getConfigDir(invoke: Invoke) { + resolvePath(invoke, activity.dataDir.absolutePath) + } + + @Command + fun getDataDir(invoke: Invoke) { + resolvePath(invoke, activity.dataDir.absolutePath) + } + + @Command + fun getDocumentDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)?.absolutePath) + } + + @Command + fun getDownloadDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath) + } + + @Command + fun getPictureDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES)?.absolutePath) + } + + @Command + fun getPublicDir(invoke: Invoke) { + resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_DCIM)?.absolutePath) + } + + @Command + fun getVideoDir(invoke: Invoke) { + resolvePath(invoke, activity.externalCacheDir?.absolutePath) + } + + @Command + fun getResourcesDir(invoke: Invoke) { + // TODO + resolvePath(invoke, activity.cacheDir.absolutePath) + } + + @Command + fun getCacheDir(invoke: Invoke) { + resolvePath(invoke, activity.cacheDir.absolutePath) + } +} diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/PermissionHelper.kt b/core/tauri/mobile/android/src/main/java/app/tauri/PermissionHelper.kt new file mode 100644 index 00000000000..e84ae9b3177 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/PermissionHelper.kt @@ -0,0 +1,115 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/PermissionHelper.java + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import androidx.core.app.ActivityCompat; +import java.util.ArrayList; + +object PermissionHelper { + /** + * Checks if a list of given permissions are all granted by the user + * + * @param permissions Permissions to check. + * @return True if all permissions are granted, false if at least one is not. + */ + fun hasPermissions(context: Context?, permissions: Array): Boolean { + for (perm in permissions) { + if (ActivityCompat.checkSelfPermission( + context!!, + perm + ) != PackageManager.PERMISSION_GRANTED + ) { + return false + } + } + return true + } + + /** + * Check whether the given permission has been defined in the AndroidManifest.xml + * + * @param permission A permission to check. + * @return True if the permission has been defined in the Manifest, false if not. + */ + fun hasDefinedPermission(context: Context, permission: String): Boolean { + var hasPermission = false + val requestedPermissions = getManifestPermissions(context) + if (requestedPermissions != null && requestedPermissions.isNotEmpty()) { + val requestedPermissionsList = listOf(*requestedPermissions) + val requestedPermissionsArrayList = ArrayList(requestedPermissionsList) + if (requestedPermissionsArrayList.contains(permission)) { + hasPermission = true + } + } + return hasPermission + } + + /** + * Check whether all of the given permissions have been defined in the AndroidManifest.xml + * @param context the app context + * @param permissions a list of permissions + * @return true only if all permissions are defined in the AndroidManifest.xml + */ + fun hasDefinedPermissions(context: Context, permissions: Array): Boolean { + for (permission in permissions) { + if (!hasDefinedPermission(context, permission)) { + return false + } + } + return true + } + + /** + * Get the permissions defined in AndroidManifest.xml + * + * @return The permissions defined in AndroidManifest.xml + */ + private fun getManifestPermissions(context: Context): Array? { + var requestedPermissions: Array? = null + try { + val pm = context.packageManager + val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())) + } else { + @Suppress("DEPRECATION") + pm.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS) + } + if (packageInfo != null) { + requestedPermissions = packageInfo.requestedPermissions + } + } catch (_: Exception) { + } + return requestedPermissions + } + + /** + * Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml + * + * @param neededPermissions The permissions needed. + * @return The permissions not present in AndroidManifest.xml + */ + fun getUndefinedPermissions(context: Context, neededPermissions: Array): Array { + val undefinedPermissions = ArrayList() + val requestedPermissions = getManifestPermissions(context) + if (requestedPermissions != null && requestedPermissions.isNotEmpty()) { + val requestedPermissionsList = listOf(*requestedPermissions) + val requestedPermissionsArrayList = ArrayList(requestedPermissionsList) + for (permission in neededPermissions) { + if (!requestedPermissionsArrayList.contains(permission)) { + undefinedPermissions.add(permission) + } + } + var undefinedPermissionArray = arrayOfNulls(undefinedPermissions.size) + undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray) + return undefinedPermissionArray + } + return neededPermissions as Array + } +} diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/PermissionState.kt b/core/tauri/mobile/android/src/main/java/app/tauri/PermissionState.kt new file mode 100644 index 00000000000..4655c0dc03d --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/PermissionState.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import java.util.* + +enum class PermissionState(private val state: String) { + GRANTED("granted"), DENIED("denied"), PROMPT("prompt"), PROMPT_WITH_RATIONALE("prompt-with-rationale"); + + override fun toString(): String { + return state + } + + companion object { + fun byState(state: String): PermissionState { + return valueOf(state.uppercase(Locale.ROOT).replace('-', '_')) + } + } +} diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/annotation/ActivityCallback.kt b/core/tauri/mobile/android/src/main/java/app/tauri/annotation/ActivityCallback.kt new file mode 100644 index 00000000000..7da7c5fb878 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/annotation/ActivityCallback.kt @@ -0,0 +1,9 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class ActivityCallback diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/annotation/Permission.kt b/core/tauri/mobile/android/src/main/java/app/tauri/annotation/Permission.kt new file mode 100644 index 00000000000..c5eb5ac7327 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/annotation/Permission.kt @@ -0,0 +1,19 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +@Retention(AnnotationRetention.RUNTIME) +annotation class Permission( + /** + * An array of Android permission strings. + * Eg: {Manifest.permission.ACCESS_COARSE_LOCATION} + * or {"android.permission.ACCESS_COARSE_LOCATION"} + */ + val strings: Array = [], + /** + * An optional name to use instead of the Android permission string. + */ + val alias: String = "" +) diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/annotation/PermissionCallback.kt b/core/tauri/mobile/android/src/main/java/app/tauri/annotation/PermissionCallback.kt new file mode 100644 index 00000000000..960d9f3a1c3 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/annotation/PermissionCallback.kt @@ -0,0 +1,9 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class PermissionCallback diff --git a/examples/updater/src-tauri/build.rs b/core/tauri/mobile/android/src/main/java/app/tauri/annotation/PluginMethod.kt similarity index 59% rename from examples/updater/src-tauri/build.rs rename to core/tauri/mobile/android/src/main/java/app/tauri/annotation/PluginMethod.kt index b055ec37c77..e102782aeb1 100644 --- a/examples/updater/src-tauri/build.rs +++ b/core/tauri/mobile/android/src/main/java/app/tauri/annotation/PluginMethod.kt @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -fn main() { - tauri_build::build() -} +package app.tauri.annotation + +@Retention(AnnotationRetention.RUNTIME) +annotation class Command diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/annotation/TauriPlugin.kt b/core/tauri/mobile/android/src/main/java/app/tauri/annotation/TauriPlugin.kt new file mode 100644 index 00000000000..41beb501750 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/annotation/TauriPlugin.kt @@ -0,0 +1,19 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.annotation + +import app.tauri.annotation.Permission + +/** + * Base annotation for all Plugins + */ +@Retention(AnnotationRetention.RUNTIME) +annotation class TauriPlugin( + /** + * Permissions this plugin needs, in order to make permission requests + * easy if the plugin only needs basic permission prompting + */ + val permissions: Array = [] +) diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/plugin/Channel.kt b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/Channel.kt new file mode 100644 index 00000000000..eb7afa51a3b --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/Channel.kt @@ -0,0 +1,11 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +class Channel(val id: Long, private val handler: (data: JSObject) -> Unit) { + fun send(data: JSObject) { + handler(data) + } +} diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/plugin/InvalidPluginMethodException.kt b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/InvalidPluginMethodException.kt new file mode 100644 index 00000000000..371b15388e5 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/InvalidPluginMethodException.kt @@ -0,0 +1,11 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +internal class InvalidCommandException : Exception { + constructor(s: String?) : super(s) {} + constructor(t: Throwable?) : super(t) {} + constructor(s: String?, t: Throwable?) : super(s, t) {} +} diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt new file mode 100644 index 00000000000..3f0ea12146c --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/Invoke.kt @@ -0,0 +1,211 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import app.tauri.Logger + +const val CHANNEL_PREFIX = "__CHANNEL__:" + +class Invoke( + val id: Long, + val command: String, + val callback: Long, + val error: Long, + private val sendResponse: (callback: Long, data: PluginResult?) -> Unit, + private val sendChannelData: (channelId: Long, data: PluginResult) -> Unit, + val data: JSObject) { + + fun resolve(data: JSObject?) { + val result = PluginResult(data) + sendResponse(callback, result) + } + + fun resolve() { + sendResponse(callback, null) + } + + fun reject(msg: String?, code: String?, ex: Exception?, data: JSObject?) { + val errorResult = PluginResult() + if (ex != null) { + Logger.error(Logger.tags("Plugin"), msg!!, ex) + } + try { + errorResult.put("message", msg) + errorResult.put("code", code) + if (null != data) { + errorResult.put("data", data) + } + } catch (jsonEx: Exception) { + Logger.error(Logger.tags("Plugin"), jsonEx.message!!, jsonEx) + } + sendResponse(error, errorResult) + } + + fun reject(msg: String?, ex: Exception?, data: JSObject?) { + reject(msg, null, ex, data) + } + + fun reject(msg: String?, code: String?, data: JSObject?) { + reject(msg, code, null, data) + } + + fun reject(msg: String?, code: String?, ex: Exception?) { + reject(msg, code, ex, null) + } + + fun reject(msg: String?, data: JSObject?) { + reject(msg, null, null, data) + } + + fun reject(msg: String?, ex: Exception?) { + reject(msg, null, ex, null) + } + + fun reject(msg: String?, code: String?) { + reject(msg, code, null, null) + } + + fun reject(msg: String?) { + reject(msg, null, null, null) + } + + fun getString(name: String): String? { + return getStringInternal(name, null) + } + + fun getString(name: String, defaultValue: String): String { + return getStringInternal(name, defaultValue)!! + } + + private fun getStringInternal(name: String, defaultValue: String?): String? { + val value = data.opt(name) ?: return defaultValue + return if (value is String) { + value + } else defaultValue + } + + fun getInt(name: String): Int? { + return getIntInternal(name, null) + } + + fun getInt(name: String, defaultValue: Int): Int { + return getIntInternal(name, defaultValue)!! + } + + private fun getIntInternal(name: String, defaultValue: Int?): Int? { + val value = data.opt(name) ?: return defaultValue + return if (value is Int) { + value + } else defaultValue + } + + fun getLong(name: String): Long? { + return getLongInternal(name, null) + } + + fun getLong(name: String, defaultValue: Long): Long { + return getLongInternal(name, defaultValue)!! + } + + private fun getLongInternal(name: String, defaultValue: Long?): Long? { + val value = data.opt(name) ?: return defaultValue + return if (value is Long) { + value + } else defaultValue + } + + fun getFloat(name: String): Float? { + return getFloatInternal(name, null) + } + + fun getFloat(name: String, defaultValue: Float): Float { + return getFloatInternal(name, defaultValue)!! + } + + private fun getFloatInternal(name: String, defaultValue: Float?): Float? { + val value = data.opt(name) ?: return defaultValue + if (value is Float) { + return value + } + if (value is Double) { + return value.toFloat() + } + return if (value is Int) { + value.toFloat() + } else defaultValue + } + + fun getDouble(name: String): Double? { + return getDoubleInternal(name, null) + } + + fun getDouble(name: String, defaultValue: Double): Double { + return getDoubleInternal(name, defaultValue)!! + } + + private fun getDoubleInternal(name: String, defaultValue: Double?): Double? { + val value = data.opt(name) ?: return defaultValue + if (value is Double) { + return value + } + if (value is Float) { + return value.toDouble() + } + return if (value is Int) { + value.toDouble() + } else defaultValue + } + + fun getBoolean(name: String): Boolean? { + return getBooleanInternal(name, null) + } + + fun getBoolean(name: String, defaultValue: Boolean): Boolean { + return getBooleanInternal(name, defaultValue)!! + } + + private fun getBooleanInternal(name: String, defaultValue: Boolean?): Boolean? { + val value = data.opt(name) ?: return defaultValue + return if (value is Boolean) { + value + } else defaultValue + } + + fun getObject(name: String): JSObject? { + return getObjectInternal(name, null) + } + + fun getObject(name: String, defaultValue: JSObject): JSObject { + return getObjectInternal(name, defaultValue)!! + } + + private fun getObjectInternal(name: String, defaultValue: JSObject?): JSObject? { + val value = data.opt(name) ?: return defaultValue + return if (value is JSObject) value else defaultValue + } + + fun getArray(name: String): JSArray? { + return getArrayInternal(name, null) + } + + fun getArray(name: String, defaultValue: JSArray): JSArray { + return getArrayInternal(name, defaultValue)!! + } + + private fun getArrayInternal(name: String, defaultValue: JSArray?): JSArray? { + val value = data.opt(name) ?: return defaultValue + return if (value is JSArray) value else defaultValue + } + + fun hasOption(name: String): Boolean { + return data.has(name) + } + + fun getChannel(name: String): Channel? { + val channelDef = getString(name, "") + val callback = channelDef.substring(CHANNEL_PREFIX.length).toLongOrNull() ?: return null + return Channel(callback) { res -> sendChannelData(callback, PluginResult(res)) } + } +} diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/plugin/JSArray.kt b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/JSArray.kt new file mode 100644 index 00000000000..6db42b86ca8 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/JSArray.kt @@ -0,0 +1,45 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import org.json.JSONArray +import org.json.JSONException + +class JSArray : JSONArray { + constructor() : super() {} + constructor(json: String?) : super(json) {} + constructor(copyFrom: Collection<*>?) : super(copyFrom) {} + constructor(array: Any?) : super(array) {} + + @Suppress("UNCHECKED_CAST", "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + @Throws(JSONException::class) + fun toList(): List { + val items: MutableList = ArrayList() + var o: Any? = null + for (i in 0 until this.length()) { + this.get(i).also { o = it } + try { + items.add(this.get(i) as E) + } catch (ex: Exception) { + throw JSONException("Not all items are instances of the given type") + } + } + return items + } + + companion object { + /** + * Create a new JSArray without throwing a error + */ + fun from(array: Any?): JSArray? { + try { + return JSArray(array) + } catch (ex: JSONException) { + // + } + return null + } + } +} diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/plugin/JSObject.kt b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/JSObject.kt new file mode 100644 index 00000000000..3affc81aa73 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/JSObject.kt @@ -0,0 +1,152 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import org.json.JSONException +import org.json.JSONObject + +class JSObject : JSONObject { + constructor() : super() + constructor(json: String) : super(json) + constructor(obj: JSONObject, names: Array) : super(obj, names) + + override fun getString(key: String): String { + return getString(key, "")!! + } + + fun getString(key: String, defaultValue: String?): String? { + try { + if (!super.isNull(key)) { + return super.getString(key) + } + } catch (_: JSONException) { + } + return defaultValue + } + + fun getInteger(key: String): Int? { + return getIntegerInternal(key, null) + } + + fun getInteger(key: String, defaultValue: Int): Int { + return getIntegerInternal(key, defaultValue)!! + } + + private fun getIntegerInternal(key: String, defaultValue: Int?): Int? { + try { + return super.getInt(key) + } catch (_: JSONException) { + } + return defaultValue + } + + override fun getBoolean(key: String): Boolean { + return getBooleanInternal(key, false)!! + } + + fun getBoolean(key: String, defaultValue: Boolean?): Boolean { + return getBooleanInternal(key, defaultValue)!! + } + + private fun getBooleanInternal(key: String, defaultValue: Boolean?): Boolean? { + try { + return super.getBoolean(key) + } catch (_: JSONException) { + } + return defaultValue + } + + fun getJSObject(name: String): JSObject? { + try { + return getJSObjectInternal(name, null) + } catch (_: JSONException) { + } + return null + } + + fun getJSObject(name: String, defaultValue: JSObject): JSObject { + return getJSObjectInternal(name, defaultValue)!! + } + + private fun getJSObjectInternal(name: String, defaultValue: JSObject?): JSObject? { + try { + val obj = get(name) + if (obj is JSONObject) { + val keysIter = obj.keys() + val keys: MutableList = ArrayList() + while (keysIter.hasNext()) { + keys.add(keysIter.next()) + } + return JSObject(obj, keys.toTypedArray()) + } + } catch (_: JSONException) { + } + return defaultValue + } + + override fun put(key: String, value: Boolean): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + override fun put(key: String, value: Int): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + override fun put(key: String, value: Long): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + override fun put(key: String, value: Double): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + override fun put(key: String, value: Any?): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + fun put(key: String, value: String?): JSObject { + try { + super.put(key, value) + } catch (_: JSONException) { + } + return this + } + + companion object { + /** + * Convert a pathetic JSONObject into a JSObject + * @param obj + */ + @Throws(JSONException::class) + fun fromJSONObject(obj: JSONObject): JSObject { + val keysIter = obj.keys() + val keys: MutableList = ArrayList() + while (keysIter.hasNext()) { + keys.add(keysIter.next()) + } + return JSObject(obj, keys.toTypedArray()) + } + } +} diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt new file mode 100644 index 00000000000..5f1ee212fa4 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt @@ -0,0 +1,417 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.webkit.WebView +import androidx.core.app.ActivityCompat +import app.tauri.FsUtils +import app.tauri.Logger +import app.tauri.PermissionHelper +import app.tauri.PermissionState +import app.tauri.annotation.ActivityCallback +import app.tauri.annotation.Command +import app.tauri.annotation.PermissionCallback +import app.tauri.annotation.TauriPlugin +import org.json.JSONException +import java.util.* +import java.util.concurrent.CopyOnWriteArrayList + +abstract class Plugin(private val activity: Activity) { + var handle: PluginHandle? = null + private val listeners: MutableMap> = mutableMapOf() + + open fun load(webView: WebView) {} + + fun getConfig(): JSObject { + return handle!!.config + } + + /** + * Handle a new intent being received by the application + */ + open fun onNewIntent(intent: Intent) {} + + /** + * Start activity for result with the provided Intent and resolve calling the provided callback method name. + * + * If there is no registered activity callback for the method name passed in, the call will + * be rejected. Make sure a valid activity result callback method is registered using the + * [ActivityCallback] annotation. + * + * @param invoke the invoke object + * @param intent the intent used to start an activity + * @param callbackName the name of the callback to run when the launched activity is finished + */ + fun startActivityForResult(invoke: Invoke, intent: Intent, callbackName: String) { + handle!!.startActivityForResult(invoke, intent, callbackName) + } + + /** + * Get the plugin log tags. + * @param subTags + */ + protected fun getLogTag(vararg subTags: String): String { + return Logger.tags(*subTags) + } + + /** + * Gets a log tag with the plugin's class name as subTag. + */ + protected fun getLogTag(): String { + return Logger.tags(this.javaClass.simpleName) + } + + /** + * Convert an URI to an URL that can be loaded by the webview. + */ + fun assetUrl(u: Uri): String { + var path = FsUtils.getFileUrlForUri(activity, u) + if (path?.startsWith("file://") == true) { + path = path.replace("file://", "") + } + return "asset://localhost$path" + } + + fun trigger(event: String, payload: JSObject) { + val eventListeners = listeners[event] + if (!eventListeners.isNullOrEmpty()) { + val listeners = CopyOnWriteArrayList(eventListeners) + for (channel in listeners) { + channel.send(payload) + } + } + } + + @Command + open fun registerListener(invoke: Invoke) { + val event = invoke.getString("event") + val channel = invoke.getChannel("handler") + + if (event == null || channel == null) { + invoke.reject("`event` or `handler` not provided") + } else { + val eventListeners = listeners[event] + if (eventListeners.isNullOrEmpty()) { + listeners[event] = mutableListOf(channel) + } else { + eventListeners.add(channel) + } + } + + invoke.resolve() + } + + @Command + open fun removeListener(invoke: Invoke) { + val event = invoke.getString("event") + val channelId = invoke.getLong("channelId") + + if (event == null || channelId == null) { + invoke.reject("`event` or `channelId` not provided") + } else { + val eventListeners = listeners[event] + if (!eventListeners.isNullOrEmpty()) { + val c = eventListeners.find { c -> c.id == channelId } + if (c != null) { + eventListeners.remove(c) + } + } + } + + invoke.resolve() + } + + /** + * Exported plugin method for checking the granted status for each permission + * declared on the plugin. This plugin call responds with a mapping of permissions to + * the associated granted status. + */ + @Command + @PermissionCallback + open fun checkPermissions(invoke: Invoke) { + val permissionsResult: Map = getPermissionStates() + if (permissionsResult.isEmpty()) { + // if no permissions are defined on the plugin, resolve undefined + invoke.resolve() + } else { + val permissionsResultJSON = JSObject() + for ((key, value) in permissionsResult) { + permissionsResultJSON.put(key, value) + } + invoke.resolve(permissionsResultJSON) + } + } + + /** + * Exported plugin method to request all permissions for this plugin. + * To manually request permissions within a plugin use: + * [.requestAllPermissions], or + * [.requestPermissionForAlias], or + * [.requestPermissionForAliases] + * + * @param invoke + */ + @Command + open fun requestPermissions(invoke: Invoke) { + val annotation = handle?.annotation + if (annotation != null) { + // handle permission requests for plugins defined with @TauriPlugin + var permAliases: Array? = null + val autoGrantPerms: MutableSet = HashSet() + + // If call was made with a list of specific permission aliases to request, save them + // to be requested + val providedPerms: JSArray = invoke.getArray("permissions", JSArray()) + var providedPermsList: List? = null + try { + providedPermsList = providedPerms.toList() + } catch (ignore: JSONException) { + // do nothing + } + + // If call was made without any custom permissions, request all from plugin annotation + val aliasSet: MutableSet = HashSet() + if (providedPermsList.isNullOrEmpty()) { + for (perm in annotation.permissions) { + // If a permission is defined with no permission strings, separate it for auto-granting. + // Otherwise, the alias is added to the list to be requested. + if (perm.strings.isEmpty() || perm.strings.size == 1 && perm.strings[0] + .isEmpty() + ) { + if (!perm.alias.isEmpty()) { + autoGrantPerms.add(perm.alias) + } + } else { + aliasSet.add(perm.alias) + } + } + permAliases = aliasSet.toTypedArray() + } else { + for (perm in annotation.permissions) { + if (providedPermsList.contains(perm.alias)) { + aliasSet.add(perm.alias) + } + } + if (aliasSet.isEmpty()) { + invoke.reject("No valid permission alias was requested of this plugin.") + } else { + permAliases = aliasSet.toTypedArray() + } + } + if (!permAliases.isNullOrEmpty()) { + // request permissions using provided aliases or all defined on the plugin + requestPermissionForAliases(permAliases, invoke, "checkPermissions") + } else if (autoGrantPerms.isNotEmpty()) { + // if the plugin only has auto-grant permissions, return all as GRANTED + val permissionsResults = JSObject() + for (perm in autoGrantPerms) { + permissionsResults.put(perm, PermissionState.GRANTED.toString()) + } + invoke.resolve(permissionsResults) + } else { + // no permissions are defined on the plugin, resolve undefined + invoke.resolve() + } + } + } + + /** + * Checks if the given permission alias is correctly declared in AndroidManifest.xml + * @param alias a permission alias defined on the plugin + * @return true only if all permissions associated with the given alias are declared in the manifest + */ + fun isPermissionDeclared(alias: String): Boolean { + val annotation = handle?.annotation + if (annotation != null) { + for (perm in annotation.permissions) { + if (alias.equals(perm.alias, ignoreCase = true)) { + var result = true + for (permString in perm.strings) { + result = result && PermissionHelper.hasDefinedPermission(activity, permString) + } + return result + } + } + } + Logger.error( + String.format( + "isPermissionDeclared: No alias defined for %s " + "or missing @TauriPlugin annotation.", + alias + ) + ) + return false + } + + private fun permissionActivityResult( + invoke: Invoke, + permissionStrings: Array, + callbackName: String + ) { + handle!!.requestPermissions(invoke, permissionStrings, callbackName) + } + + /** + * Request all of the specified permissions in the TauriPlugin annotation (if any) + * + * If there is no registered permission callback for the Invoke passed in, the call will + * be rejected. Make sure a valid permission callback method is registered using the + * [PermissionCallback] annotation. + * + * @param invoke + * @param callbackName the name of the callback to run when the permission request is complete + */ + protected fun requestAllPermissions( + invoke: Invoke, + callbackName: String + ) { + val annotation = handle!!.annotation + if (annotation != null) { + val perms: HashSet = HashSet() + for (perm in annotation.permissions) { + perms.addAll(perm.strings) + } + permissionActivityResult(invoke, perms.toArray(arrayOfNulls(0)), callbackName) + } + } + + /** + * Request permissions using an alias defined on the plugin. + * + * If there is no registered permission callback for the Invoke passed in, the call will + * be rejected. Make sure a valid permission callback method is registered using the + * [PermissionCallback] annotation. + * + * @param alias an alias defined on the plugin + * @param invoke the invoke involved in originating the request + * @param callbackName the name of the callback to run when the permission request is complete + */ + protected fun requestPermissionForAlias( + alias: String, + call: Invoke, + callbackName: String + ) { + requestPermissionForAliases(arrayOf(alias), call, callbackName) + } + + /** + * Request permissions using aliases defined on the plugin. + * + * If there is no registered permission callback for the Invoke passed in, the call will + * be rejected. Make sure a valid permission callback method is registered using the + * [PermissionCallback] annotation. + * + * @param aliases a set of aliases defined on the plugin + * @param invoke the invoke involved in originating the request + * @param callbackName the name of the callback to run when the permission request is complete + */ + fun requestPermissionForAliases( + aliases: Array, + invoke: Invoke, + callbackName: String + ) { + if (aliases.isEmpty()) { + Logger.error("No permission alias was provided") + return + } + val permissions = getPermissionStringsForAliases(aliases) + if (permissions.isNotEmpty()) { + permissionActivityResult(invoke, permissions, callbackName) + } + } + + /** + * Gets the Android permission strings defined on the [TauriPlugin] annotation with + * the provided aliases. + * + * @param aliases aliases for permissions defined on the plugin + * @return Android permission strings associated with the provided aliases, if exists + */ + private fun getPermissionStringsForAliases(aliases: Array): Array { + val annotation = handle?.annotation + val perms: HashSet = HashSet() + if (annotation != null) { + for (perm in annotation.permissions) { + if (aliases.contains(perm.alias)) { + perms.addAll(perm.strings) + } + } + } + return perms.toArray(arrayOfNulls(0)) + } + + /** + * Get the permission state for the provided permission alias. + * + * @param alias the permission alias to get + * @return the state of the provided permission alias or null + */ + fun getPermissionState(alias: String): PermissionState? { + return getPermissionStates()[alias] + } + + /** + * Helper to check all permissions defined on a plugin and see the state of each. + * + * @return A mapping of permission aliases to the associated granted status. + */ + open fun getPermissionStates(): Map { + val permissionsResults: MutableMap = HashMap() + val annotation = handle?.annotation + if (annotation != null) { + for (perm in annotation.permissions) { + // If a permission is defined with no permission constants, return GRANTED for it. + // Otherwise, get its true state. + if (perm.strings.isEmpty() || perm.strings.size == 1 && perm.strings[0] + .isEmpty() + ) { + val key = perm.alias + if (key.isNotEmpty()) { + val existingResult = permissionsResults[key] + + // auto set permission state to GRANTED if the alias is empty. + if (existingResult == null) { + permissionsResults[key] = PermissionState.GRANTED + } + } + } else { + for (permString in perm.strings) { + val key = perm.alias.ifEmpty { permString } + var permissionStatus: PermissionState + if (ActivityCompat.checkSelfPermission( + activity, + permString + ) == PackageManager.PERMISSION_GRANTED + ) { + permissionStatus = PermissionState.GRANTED + } else { + permissionStatus = PermissionState.PROMPT + + // Check if there is a cached permission state for the "Never ask again" state + val prefs = + activity.getSharedPreferences("PluginPermStates", Activity.MODE_PRIVATE) + val state = prefs.getString(permString, null) + if (state != null) { + permissionStatus = PermissionState.byState(state) + } + } + val existingResult = permissionsResults[key] + + // multiple permissions with the same alias must all be true, otherwise all false. + if (existingResult == null || existingResult === PermissionState.GRANTED) { + permissionsResults[key] = permissionStatus + } + } + } + } + } + + return permissionsResults + } + +} diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginHandle.kt b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginHandle.kt new file mode 100644 index 00000000000..08f4a086251 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginHandle.kt @@ -0,0 +1,154 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import android.app.Activity +import android.content.Intent +import android.content.SharedPreferences +import android.webkit.WebView +import androidx.core.app.ActivityCompat +import app.tauri.PermissionHelper +import app.tauri.PermissionState +import app.tauri.annotation.ActivityCallback +import app.tauri.annotation.Command +import app.tauri.annotation.PermissionCallback +import app.tauri.annotation.TauriPlugin +import java.lang.reflect.Method +import java.util.Arrays + + +class PluginHandle(private val manager: PluginManager, val name: String, val instance: Plugin, val config: JSObject) { + private val commands: HashMap = HashMap() + private val permissionCallbackMethods: HashMap = HashMap() + private val startActivityCallbackMethods: HashMap = HashMap() + var annotation: TauriPlugin? + var loaded = false + + init { + indexMethods() + instance.handle = this + annotation = instance.javaClass.getAnnotation(TauriPlugin::class.java) + } + + fun load(webView: WebView) { + instance.load(webView) + loaded = true + } + + fun startActivityForResult(invoke: Invoke, intent: Intent, callbackName: String) { + manager.startActivityForResult(intent) { result -> + val method = startActivityCallbackMethods[callbackName] + if (method != null) { + method.isAccessible = true + method(instance, invoke, result) + } + } + } + + fun requestPermissions( + invoke: Invoke, + permissions: Array, + callbackName: String + ) { + manager.requestPermissions(permissions) { result -> + if (validatePermissions(invoke, result)) { + val method = permissionCallbackMethods[callbackName] + if (method != null) { + method.isAccessible = true + method(instance, invoke) + } + } + } + } + + /** + * Saves permission states and rejects if permissions were not correctly defined in + * the AndroidManifest.xml file. + * + * @param permissions + * @return true if permissions were saved and defined correctly, false if not + */ + private fun validatePermissions( + invoke: Invoke, + permissions: Map + ): Boolean { + val activity = manager.activity + val prefs = + activity.getSharedPreferences("PluginPermStates", Activity.MODE_PRIVATE) + for ((permString, isGranted) in permissions) { + if (isGranted) { + // Permission granted. If previously denied, remove cached state + val state = prefs.getString(permString, null) + if (state != null) { + val editor: SharedPreferences.Editor = prefs.edit() + editor.remove(permString) + editor.apply() + } + } else { + val editor: SharedPreferences.Editor = prefs.edit() + if (ActivityCompat.shouldShowRequestPermissionRationale( + activity, + permString + ) + ) { + // Permission denied, can prompt again with rationale + editor.putString(permString, PermissionState.PROMPT_WITH_RATIONALE.toString()) + } else { + // Permission denied permanently, store this state for future reference + editor.putString(permString, PermissionState.DENIED.toString()) + } + editor.apply() + } + } + val permStrings = permissions.keys.toTypedArray() + if (!PermissionHelper.hasDefinedPermissions(activity, permStrings)) { + val builder = StringBuilder() + builder.append("Missing the following permissions in AndroidManifest.xml:\n") + val missing = PermissionHelper.getUndefinedPermissions(activity, permStrings) + for (perm in missing) { + builder.append( + """ + $perm + + """.trimIndent() + ) + } + invoke.reject(builder.toString()) + return false + } + return true + } + + @Throws( + InvalidCommandException::class, + IllegalAccessException::class + ) + fun invoke(invoke: Invoke) { + val methodMeta = commands[invoke.command] + ?: throw InvalidCommandException("No command " + invoke.command + " found for plugin " + instance.javaClass.name) + methodMeta.method.invoke(instance, invoke) + } + + private fun indexMethods() { + val methods = mutableListOf() + var pluginCursor: Class<*> = instance.javaClass + while (pluginCursor.name != Any::class.java.name) { + methods.addAll(listOf(*pluginCursor.declaredMethods)) + pluginCursor = pluginCursor.superclass + } + + for (method in methods) { + if (method.isAnnotationPresent(Command::class.java)) { + val command = method.getAnnotation(Command::class.java) ?: continue + val methodMeta = CommandData(method, command) + commands[method.name] = methodMeta + } else if (method.isAnnotationPresent(ActivityCallback::class.java)) { + startActivityCallbackMethods[method.name] = method + } else if (method.isAnnotationPresent(PermissionCallback::class.java)) { + permissionCallbackMethods[method.name] = method + } + } + } +} diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt new file mode 100644 index 00000000000..62ddc8d19d5 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginManager.kt @@ -0,0 +1,137 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import android.content.Context +import android.content.Intent +import android.webkit.WebView +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import app.tauri.FsUtils +import app.tauri.JniMethod +import app.tauri.Logger + +class PluginManager(val activity: AppCompatActivity) { + fun interface RequestPermissionsCallback { + fun onResult(permissions: Map) + } + + fun interface ActivityResultCallback { + fun onResult(result: ActivityResult) + } + + private val plugins: HashMap = HashMap() + private val startActivityForResultLauncher: ActivityResultLauncher + private val requestPermissionsLauncher: ActivityResultLauncher> + private var requestPermissionsCallback: RequestPermissionsCallback? = null + private var startActivityForResultCallback: ActivityResultCallback? = null + + init { + startActivityForResultLauncher = + activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult() + ) { result -> + if (startActivityForResultCallback != null) { + startActivityForResultCallback!!.onResult(result) + } + } + + requestPermissionsLauncher = + activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions() + ) { result -> + if (requestPermissionsCallback != null) { + requestPermissionsCallback!!.onResult(result) + } + } + } + + fun onNewIntent(intent: Intent) { + for (plugin in plugins.values) { + plugin.instance.onNewIntent(intent) + } + } + + fun startActivityForResult(intent: Intent, callback: ActivityResultCallback) { + startActivityForResultCallback = callback + startActivityForResultLauncher.launch(intent) + } + + fun requestPermissions( + permissionStrings: Array, + callback: RequestPermissionsCallback + ) { + requestPermissionsCallback = callback + requestPermissionsLauncher.launch(permissionStrings) + } + + @JniMethod + fun onWebViewCreated(webView: WebView) { + for ((_, plugin) in plugins) { + if (!plugin.loaded) { + plugin.load(webView) + } + } + } + + @JniMethod + fun load(webView: WebView?, name: String, plugin: Plugin, config: JSObject) { + val handle = PluginHandle(this, name, plugin, config) + plugins[name] = handle + if (webView != null) { + plugin.load(webView) + } + } + + @JniMethod + fun runCommand(id: Int, pluginId: String, command: String, data: JSObject) { + val successId = 0L + val errorId = 1L + val invoke = Invoke(id.toLong(), command, successId, errorId, { fn, result -> + var success: PluginResult? = null + var error: PluginResult? = null + if (fn == successId) { + success = result + } else { + error = result + } + handlePluginResponse(id, success?.toString(), error?.toString()) + }, { channelId, payload -> + sendChannelData(channelId, payload.toString()) + }, data) + + dispatchPluginMessage(invoke, pluginId) + } + + private fun dispatchPluginMessage(invoke: Invoke, pluginId: String) { + Logger.verbose( + Logger.tags("Plugin"), + "Tauri plugin: pluginId: $pluginId, command: ${invoke.command}" + ) + + try { + val plugin = plugins[pluginId] + if (plugin == null) { + invoke.reject("Plugin $pluginId not initialized") + } else { + plugins[pluginId]?.invoke(invoke) + } + } catch (e: Exception) { + invoke.reject(if (e.message?.isEmpty() != false) { e.toString() } else { e.message }) + } + } + + companion object { + fun loadConfig(context: Context, plugin: String): JSObject { + val tauriConfigJson = FsUtils.readAsset(context.assets, "tauri.conf.json") + val tauriConfig = JSObject(tauriConfigJson) + val plugins = tauriConfig.getJSObject("plugins", JSObject()) + return plugins.getJSObject(plugin, JSObject()) + } + } + + private external fun handlePluginResponse(id: Int, success: String?, error: String?) + private external fun sendChannelData(id: Long, data: String) +} diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginMethodData.kt b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginMethodData.kt new file mode 100644 index 00000000000..87b2543122b --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginMethodData.kt @@ -0,0 +1,16 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import app.tauri.annotation.Command +import java.lang.reflect.Method + +class CommandData( + val method: Method, methodDecorator: Command +) { + + // The name of the method + val name: String = method.name +} diff --git a/core/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginResult.kt b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginResult.kt new file mode 100644 index 00000000000..142954ba989 --- /dev/null +++ b/core/tauri/mobile/android/src/main/java/app/tauri/plugin/PluginResult.kt @@ -0,0 +1,67 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.plugin + +import android.annotation.SuppressLint +import app.tauri.Logger +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* + +class PluginResult @JvmOverloads constructor(json: JSObject? = JSObject()) { + private val json: JSObject + + init { + this.json = json ?: JSObject() + } + + fun put(name: String, value: Boolean): PluginResult { + return jsonPut(name, value) + } + + fun put(name: String, value: Double): PluginResult { + return jsonPut(name, value) + } + + fun put(name: String, value: Int): PluginResult { + return jsonPut(name, value) + } + + fun put(name: String, value: Long): PluginResult { + return jsonPut(name, value) + } + + /** + * Format a date as an ISO string + */ + @SuppressLint("SimpleDateFormat") + fun put(name: String, value: Date): PluginResult { + val tz: TimeZone = TimeZone.getTimeZone("UTC") + val df: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'") + df.timeZone = tz + return jsonPut(name, df.format(value)) + } + + fun put(name: String, value: Any?): PluginResult { + return jsonPut(name, value) + } + + fun put(name: String, value: PluginResult): PluginResult { + return jsonPut(name, value.json) + } + + private fun jsonPut(name: String, value: Any?): PluginResult { + try { + json.put(name, value) + } catch (ex: Exception) { + Logger.error(Logger.tags("Plugin"), "", ex) + } + return this + } + + override fun toString(): String { + return json.toString() + } +} diff --git a/core/tauri/mobile/android/src/test/java/app/tauri/ExampleUnitTest.kt b/core/tauri/mobile/android/src/test/java/app/tauri/ExampleUnitTest.kt new file mode 100644 index 00000000000..7db9cdf69f7 --- /dev/null +++ b/core/tauri/mobile/android/src/test/java/app/tauri/ExampleUnitTest.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/core/tauri/mobile/ios-api/.gitignore b/core/tauri/mobile/ios-api/.gitignore new file mode 100644 index 00000000000..5922fdaa563 --- /dev/null +++ b/core/tauri/mobile/ios-api/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/core/tauri/mobile/ios-api/Package.swift b/core/tauri/mobile/ios-api/Package.swift new file mode 100644 index 00000000000..50bafbd805d --- /dev/null +++ b/core/tauri/mobile/ios-api/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version:5.3 +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "Tauri", + platforms: [ + .macOS(.v10_13), + .iOS(.v11), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "Tauri", + type: .static, + targets: ["Tauri"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(name: "SwiftRs", url: "https://github.com/Brendonovich/swift-rs", from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "Tauri", + dependencies: [ + .byName(name: "SwiftRs"), + ], + path: "Sources" + ), + .testTarget( + name: "TauriTests", + dependencies: ["Tauri"] + ), + ] +) diff --git a/core/tauri/mobile/ios-api/README.md b/core/tauri/mobile/ios-api/README.md new file mode 100644 index 00000000000..52c3f1c7fa4 --- /dev/null +++ b/core/tauri/mobile/ios-api/README.md @@ -0,0 +1,3 @@ +# Tauri + +Tauri iOS API. diff --git a/core/tauri/mobile/ios-api/Sources/Tauri/Channel.swift b/core/tauri/mobile/ios-api/Sources/Tauri/Channel.swift new file mode 100644 index 00000000000..f3d03ba662b --- /dev/null +++ b/core/tauri/mobile/ios-api/Sources/Tauri/Channel.swift @@ -0,0 +1,17 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +public class Channel { + public let id: UInt64 + let handler: (JsonValue) -> Void + + public init(id: UInt64, handler: @escaping (JsonValue) -> Void) { + self.id = id + self.handler = handler + } + + public func send(_ data: JsonObject) { + handler(.dictionary(data)) + } +} diff --git a/core/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift b/core/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift new file mode 100644 index 00000000000..f7490ae35bd --- /dev/null +++ b/core/tauri/mobile/ios-api/Sources/Tauri/Invoke.swift @@ -0,0 +1,99 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation +import UIKit + +let CHANNEL_PREFIX = "__CHANNEL__:" + +@objc public class Invoke: NSObject, JSValueContainer, BridgedJSValueContainer { + public var dictionaryRepresentation: NSDictionary { + return data as NSDictionary + } + + public static var jsDateFormatter: ISO8601DateFormatter = { + return ISO8601DateFormatter() + }() + + public var command: String + var callback: UInt64 + var error: UInt64 + public var data: JSObject + var sendResponse: (UInt64, JsonValue?) -> Void + var sendChannelData: (UInt64, JsonValue) -> Void + + public init( + command: String, callback: UInt64, error: UInt64, + sendResponse: @escaping (UInt64, JsonValue?) -> Void, + sendChannelData: @escaping (UInt64, JsonValue) -> Void, data: JSObject? + ) { + self.command = command + self.callback = callback + self.error = error + self.data = data ?? [:] + self.sendResponse = sendResponse + self.sendChannelData = sendChannelData + } + + public func resolve() { + sendResponse(callback, nil) + } + + public func resolve(_ data: JsonObject) { + resolve(.dictionary(data)) + } + + public func resolve(_ data: JsonValue) { + sendResponse(callback, data) + } + + public func reject( + _ message: String, _ code: String? = nil, _ error: Error? = nil, _ data: JsonValue? = nil + ) { + let payload: NSMutableDictionary = [ + "message": message, "code": code ?? "", "error": error ?? "", + ] + if let data = data { + switch data { + case .dictionary(let dict): + for entry in dict { + payload[entry.key] = entry.value + } + } + } + sendResponse(self.error, .dictionary(payload as! JsonObject)) + } + + public func unimplemented() { + unimplemented("not implemented") + } + + public func unimplemented(_ message: String) { + sendResponse(error, .dictionary(["message": message])) + } + + public func unavailable() { + unavailable("not available") + } + + public func unavailable(_ message: String) { + sendResponse(error, .dictionary(["message": message])) + } + + public func getChannel(_ key: String) -> Channel? { + let channelDef = getString(key, "") + let components = channelDef.components(separatedBy: CHANNEL_PREFIX) + if components.count < 2 { + return nil + } + guard let channelId = UInt64(components[1]) else { + return nil + } + return Channel( + id: channelId, + handler: { (res: JsonValue) -> Void in + self.sendChannelData(channelId, res) + }) + } +} diff --git a/core/tauri/mobile/ios-api/Sources/Tauri/JSTypes.swift b/core/tauri/mobile/ios-api/Sources/Tauri/JSTypes.swift new file mode 100644 index 00000000000..d1c3fb07b48 --- /dev/null +++ b/core/tauri/mobile/ios-api/Sources/Tauri/JSTypes.swift @@ -0,0 +1,242 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation + +// declare our empty protocol, and conformance, for typing +public protocol JSValue { } +extension String: JSValue { } +extension Bool: JSValue { } +extension Int: JSValue { } +extension Float: JSValue { } +extension Double: JSValue { } +extension NSNumber: JSValue { } +extension NSNull: JSValue { } +extension Array: JSValue { } +extension Date: JSValue { } +extension Dictionary: JSValue where Key == String, Value == JSValue { } + +// convenience aliases +public typealias JSObject = [String: JSValue] +public typealias JSArray = [JSValue] + +// string types +public protocol JSStringContainer { + func getString(_ key: String, _ defaultValue: String) -> String + func getString(_ key: String) -> String? +} + +extension JSStringContainer { + public func getString(_ key: String, _ defaultValue: String) -> String { + return getString(key) ?? defaultValue + } +} + +// boolean types +public protocol JSBoolContainer { + func getBool(_ key: String, _ defaultValue: Bool) -> Bool + func getBool(_ key: String) -> Bool? +} + +extension JSBoolContainer { + public func getBool(_ key: String, _ defaultValue: Bool) -> Bool { + return getBool(key) ?? defaultValue + } +} + +// integer types +public protocol JSIntContainer { + func getInt(_ key: String, _ defaultValue: Int) -> Int + func getInt(_ key: String) -> Int? +} + +extension JSIntContainer { + public func getInt(_ key: String, _ defaultValue: Int) -> Int { + return getInt(key) ?? defaultValue + } +} + +// float types +public protocol JSFloatContainer { + func getFloat(_ key: String, _ defaultValue: Float) -> Float + func getFloat(_ key: String) -> Float? +} + +extension JSFloatContainer { + public func getFloat(_ key: String, _ defaultValue: Float) -> Float { + return getFloat(key) ?? defaultValue + } +} + +// double types +public protocol JSDoubleContainer { + func getDouble(_ key: String, _ defaultValue: Double) -> Double + func getDouble(_ key: String) -> Double? +} + +extension JSDoubleContainer { + public func getDouble(_ key: String, _ defaultValue: Double) -> Double { + return getDouble(key) ?? defaultValue + } +} + +// date types +public protocol JSDateContainer { + func getDate(_ key: String, _ defaultValue: Date) -> Date + func getDate(_ key: String) -> Date? +} + +extension JSDateContainer { + public func getDate(_ key: String, _ defaultValue: Date) -> Date { + return getDate(key) ?? defaultValue + } +} + +// array types +public protocol JSArrayContainer { + func getArray(_ key: String, _ defaultValue: JSArray) -> JSArray + func getArray(_ key: String, _ ofType: T.Type) -> [T]? + func getArray(_ key: String) -> JSArray? +} + +extension JSArrayContainer { + public func getArray(_ key: String, _ defaultValue: JSArray) -> JSArray { + return getArray(key) ?? defaultValue + } + + public func getArray(_ key: String, _ ofType: T.Type) -> [T]? { + return getArray(key) as? [T] + } +} + +// dictionary types +public protocol JSObjectContainer { + func getObject(_ key: String, _ defaultValue: JSObject) -> JSObject + func getObject(_ key: String) -> JSObject? +} + +extension JSObjectContainer { + public func getObject(_ key: String, _ defaultValue: JSObject) -> JSObject { + return getObject(key) ?? defaultValue + } +} + +public protocol JSValueContainer: JSStringContainer, JSBoolContainer, JSIntContainer, JSFloatContainer, + JSDoubleContainer, JSDateContainer, JSArrayContainer, JSObjectContainer { + static var jsDateFormatter: ISO8601DateFormatter { get } + var data: JSObject { get } +} + +extension JSValueContainer { + public func getValue(_ key: String) -> JSValue? { + return data[key] + } + + public func getString(_ key: String) -> String? { + return data[key] as? String + } + + public func getBool(_ key: String) -> Bool? { + return data[key] as? Bool + } + + public func getInt(_ key: String) -> Int? { + return data[key] as? Int + } + + public func getFloat(_ key: String) -> Float? { + if let floatValue = data[key] as? Float { + return floatValue + } else if let doubleValue = data[key] as? Double { + return Float(doubleValue) + } + return nil + } + + public func getDouble(_ key: String) -> Double? { + return data[key] as? Double + } + + public func getDate(_ key: String) -> Date? { + if let isoString = data[key] as? String { + return Self.jsDateFormatter.date(from: isoString) + } + return data[key] as? Date + } + + public func getArray(_ key: String) -> JSArray? { + return data[key] as? JSArray + } + + public func getObject(_ key: String) -> JSObject? { + return data[key] as? JSObject + } +} + +@objc protocol BridgedJSValueContainer: NSObjectProtocol { + static var jsDateFormatter: ISO8601DateFormatter { get } + var dictionaryRepresentation: NSDictionary { get } +} + +/* + Simply casting objects from foundation class clusters (such as __NSArrayM) + doesn't work with the JSValue protocol and will always fail. So we need to + recursively and explicitly convert each value in the dictionary. + */ +public enum JSTypes { } +extension JSTypes { + public static func coerceDictionaryToJSObject(_ dictionary: NSDictionary?, formattingDatesAsStrings: Bool = false) -> JSObject? { + return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject + } + + public static func coerceDictionaryToJSObject(_ dictionary: [AnyHashable: Any]?, formattingDatesAsStrings: Bool = false) -> JSObject? { + return coerceToJSValue(dictionary, formattingDates: formattingDatesAsStrings) as? JSObject + } + + public static func coerceArrayToJSArray(_ array: [Any]?, formattingDatesAsStrings: Bool = false) -> JSArray? { + return array?.compactMap { coerceToJSValue($0, formattingDates: formattingDatesAsStrings) } + } +} + +private let dateStringFormatter = ISO8601DateFormatter() + +// We need a large switch statement because we have a lot of types. +// swiftlint:disable:next cyclomatic_complexity +private func coerceToJSValue(_ value: Any?, formattingDates: Bool) -> JSValue? { + guard let value = value else { + return nil + } + switch value { + case let stringValue as String: + return stringValue + case let numberValue as NSNumber: + return numberValue + case let boolValue as Bool: + return boolValue + case let intValue as Int: + return intValue + case let floatValue as Float: + return floatValue + case let doubleValue as Double: + return doubleValue + case let dateValue as Date: + if formattingDates { + return dateStringFormatter.string(from: dateValue) + } + return dateValue + case let nullValue as NSNull: + return nullValue + case let arrayValue as NSArray: + return arrayValue.compactMap { coerceToJSValue($0, formattingDates: formattingDates) } + case let dictionaryValue as NSDictionary: + let keys = dictionaryValue.allKeys.compactMap { $0 as? String } + var result: JSObject = [:] + for key in keys { + result[key] = coerceToJSValue(dictionaryValue[key], formattingDates: formattingDates) + } + return result + default: + return nil + } +} diff --git a/core/tauri/mobile/ios-api/Sources/Tauri/JsonValue.swift b/core/tauri/mobile/ios-api/Sources/Tauri/JsonValue.swift new file mode 100644 index 00000000000..7c863ded5db --- /dev/null +++ b/core/tauri/mobile/ios-api/Sources/Tauri/JsonValue.swift @@ -0,0 +1,58 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation + +public typealias JsonObject = [String: Any] + +public enum JsonValue { + case dictionary(JsonObject) + + enum SerializationError: Error { + case invalidObject + } + + public func jsonRepresentation(includingFields: JsonObject? = nil) throws -> String? { + switch self { + case .dictionary(var dictionary): + if let fields = includingFields { + dictionary.merge(fields) { (current, _) in current } + } + dictionary = prepare(dictionary: dictionary) + guard JSONSerialization.isValidJSONObject(dictionary) else { + throw SerializationError.invalidObject + } + let data = try JSONSerialization.data(withJSONObject: dictionary, options: []) + return String(data: data, encoding: .utf8) + } + } + + private static let formatter = ISO8601DateFormatter() + + private func prepare(dictionary: JsonObject) -> JsonObject { + return dictionary.mapValues { (value) -> Any in + if let date = value as? Date { + return JsonValue.formatter.string(from: date) + } else if let aDictionary = value as? JsonObject { + return prepare(dictionary: aDictionary) + } else if let anArray = value as? [Any] { + return prepare(array: anArray) + } + return value + } + } + + private func prepare(array: [Any]) -> [Any] { + return array.map { (value) -> Any in + if let date = value as? Date { + return JsonValue.formatter.string(from: date) + } else if let aDictionary = value as? JsonObject { + return prepare(dictionary: aDictionary) + } else if let anArray = value as? [Any] { + return prepare(array: anArray) + } + return value + } + } +} diff --git a/core/tauri/mobile/ios-api/Sources/Tauri/Logger.swift b/core/tauri/mobile/ios-api/Sources/Tauri/Logger.swift new file mode 100644 index 00000000000..115359f8ffb --- /dev/null +++ b/core/tauri/mobile/ios-api/Sources/Tauri/Logger.swift @@ -0,0 +1,50 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import os.log +import UIKit + +/// Wrapper class for os_log function +public class Logger { + private static var _enabled = false + public static var enabled: Bool { + get { + #if DEBUG + return true + #else + return _enabled + #endif + } + set { + Logger._enabled = newValue + } + } + + static func log(_ items: Any..., category: String, type: OSLogType) { + if Logger.enabled { + var message = "" + let last = items.count - 1 + for (index, item) in items.enumerated() { + message += "\(item)" + if index != last { + message += " " + } + } + let log = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "-", category: category) + os_log("%{public}@", log: log, type: type, String(message.prefix(4068))) + } + } + + public static func debug(_ items: Any..., category: String = "app") { + Logger.log(items, category: category, type: OSLogType.debug) + } + + public static func info(_ items: Any..., category: String = "app") { + Logger.log(items, category: category, type: OSLogType.info) + } + + public static func error(_ items: Any..., category: String = "app") { + Logger.log(items, category: category, type: OSLogType.error) + } +} diff --git a/core/tauri/mobile/ios-api/Sources/Tauri/Plugin/Plugin.swift b/core/tauri/mobile/ios-api/Sources/Tauri/Plugin/Plugin.swift new file mode 100644 index 00000000000..50179e2da09 --- /dev/null +++ b/core/tauri/mobile/ios-api/Sources/Tauri/Plugin/Plugin.swift @@ -0,0 +1,71 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import WebKit +import os.log + +open class Plugin: NSObject { + public let manager: PluginManager = PluginManager.shared + public var config: JSObject = [:] + private var listeners = [String: [Channel]]() + + internal func setConfig(_ config: JSObject) { + self.config = config + } + + @objc open func load(webview: WKWebView) {} + + @objc open func checkPermissions(_ invoke: Invoke) { + invoke.resolve() + } + + @objc open func requestPermissions(_ invoke: Invoke) { + invoke.resolve() + } + + public func trigger(_ event: String, data: JSObject) { + if let eventListeners = listeners[event] { + for channel in eventListeners { + channel.send(data) + } + } + } + + @objc func registerListener(_ invoke: Invoke) { + guard let event = invoke.getString("event") else { + invoke.reject("`event` not provided") + return + } + guard let channel = invoke.getChannel("handler") else { + invoke.reject("`handler` not provided") + return + } + + if var eventListeners = listeners[event] { + eventListeners.append(channel) + } else { + listeners[event] = [channel] + } + + invoke.resolve() + } + + @objc func removeListener(_ invoke: Invoke) { + guard let event = invoke.getString("event") else { + invoke.reject("`event` not provided") + return + } + + if let eventListeners = listeners[event] { + guard let channelId = invoke.getInt("channelId") else { + invoke.reject("`channelId` not provided") + return + } + + listeners[event] = eventListeners.filter { $0.id != channelId } + } + + invoke.resolve() + } +} diff --git a/core/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift b/core/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift new file mode 100644 index 00000000000..80a80fab764 --- /dev/null +++ b/core/tauri/mobile/ios-api/Sources/Tauri/Tauri.swift @@ -0,0 +1,148 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import Foundation +import SwiftRs +import UIKit +import WebKit +import os.log + +class PluginHandle { + var instance: Plugin + var loaded = false + + init(plugin: Plugin) { + instance = plugin + } +} + +public class PluginManager { + static let shared: PluginManager = PluginManager() + public var viewController: UIViewController? + var plugins: [String: PluginHandle] = [:] + var ipcDispatchQueue = DispatchQueue(label: "ipc") + public var isSimEnvironment: Bool { + #if targetEnvironment(simulator) + return true + #else + return false + #endif + } + + public func assetUrl(fromLocalURL url: URL?) -> URL? { + guard let inputURL = url else { + return nil + } + + return URL(string: "asset://localhost")!.appendingPathComponent(inputURL.path) + } + + func onWebviewCreated(_ webview: WKWebView) { + for (_, handle) in plugins { + if !handle.loaded { + handle.instance.load(webview: webview) + } + } + } + + func load(name: String, plugin: P, config: JSObject, webview: WKWebView?) { + plugin.setConfig(config) + let handle = PluginHandle(plugin: plugin) + if let webview = webview { + handle.instance.load(webview: webview) + handle.loaded = true + } + plugins[name] = handle + } + + func invoke(name: String, invoke: Invoke) { + if let plugin = plugins[name] { + ipcDispatchQueue.async { + let selectorWithThrows = Selector(("\(invoke.command):error:")) + if plugin.instance.responds(to: selectorWithThrows) { + var error: NSError? = nil + withUnsafeMutablePointer(to: &error) { + let methodIMP: IMP! = plugin.instance.method(for: selectorWithThrows) + unsafeBitCast( + methodIMP, to: (@convention(c) (Any?, Selector, Invoke, OpaquePointer) -> Void).self)( + plugin.instance, selectorWithThrows, invoke, OpaquePointer($0)) + } + if let error = error { + invoke.reject("\(error)") + // TODO: app crashes without this leak + let _ = Unmanaged.passRetained(error) + } + } else { + let selector = Selector(("\(invoke.command):")) + if plugin.instance.responds(to: selector) { + plugin.instance.perform(selector, with: invoke) + } else { + invoke.reject("No command \(invoke.command) found for plugin \(name)") + } + } + } + } else { + invoke.reject("Plugin \(name) not initialized") + } + } +} + +extension PluginManager: NSCopying { + public func copy(with zone: NSZone? = nil) -> Any { + return self + } +} + +@_cdecl("register_plugin") +func registerPlugin(name: SRString, plugin: NSObject, config: NSDictionary?, webview: WKWebView?) { + PluginManager.shared.load( + name: name.toString(), + plugin: plugin as! Plugin, + config: JSTypes.coerceDictionaryToJSObject(config ?? [:], formattingDatesAsStrings: true)!, + webview: webview + ) +} + +@_cdecl("on_webview_created") +func onWebviewCreated(webview: WKWebView, viewController: UIViewController) { + PluginManager.shared.viewController = viewController + PluginManager.shared.onWebviewCreated(webview) +} + +@_cdecl("run_plugin_command") +func runCommand( + id: Int, + name: SRString, + command: SRString, + data: NSDictionary, + callback: @escaping @convention(c) (Int, Bool, UnsafePointer?) -> Void, + sendChannelData: @escaping @convention(c) (UInt64, UnsafePointer) -> Void +) { + let callbackId: UInt64 = 0 + let errorId: UInt64 = 1 + let invoke = Invoke( + command: command.toString(), callback: callbackId, error: errorId, + sendResponse: { (fn: UInt64, payload: JsonValue?) -> Void in + let success = fn == callbackId + var payloadJson: String = "" + do { + try payloadJson = + payload == nil ? "null" : payload!.jsonRepresentation() ?? "`Failed to serialize payload`" + } catch { + payloadJson = "`\(error)`" + } + callback(id, success, payloadJson.cString(using: String.Encoding.utf8)) + }, + sendChannelData: { (id: UInt64, payload: JsonValue) -> Void in + var payloadJson: String = "" + do { + try payloadJson = + payload.jsonRepresentation() ?? "`Failed to serialize payload`" + } catch { + payloadJson = "`\(error)`" + } + sendChannelData(id, payloadJson) + }, data: JSTypes.coerceDictionaryToJSObject(data, formattingDatesAsStrings: true)) + PluginManager.shared.invoke(name: name.toString(), invoke: invoke) +} diff --git a/core/tauri/mobile/ios-api/Sources/Tauri/UiUtils.swift b/core/tauri/mobile/ios-api/Sources/Tauri/UiUtils.swift new file mode 100644 index 00000000000..6ca74b960e7 --- /dev/null +++ b/core/tauri/mobile/ios-api/Sources/Tauri/UiUtils.swift @@ -0,0 +1,15 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import UIKit + +public class UIUtils { + public static func centerPopover(rootViewController: UIViewController?, popoverController: UIViewController) { + if let viewController = rootViewController { + popoverController.popoverPresentationController?.sourceRect = CGRect(x: viewController.view.center.x, y: viewController.view.center.y, width: 0, height: 0) + popoverController.popoverPresentationController?.sourceView = viewController.view + popoverController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.up + } + } +} diff --git a/core/tauri/mobile/ios-api/Tests/TauriTests/TauriTests.swift b/core/tauri/mobile/ios-api/Tests/TauriTests/TauriTests.swift new file mode 100644 index 00000000000..0681f06fd5e --- /dev/null +++ b/core/tauri/mobile/ios-api/Tests/TauriTests/TauriTests.swift @@ -0,0 +1,15 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import XCTest +@testable import Tauri + +final class TauriTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(Tauri().text, "Hello, World!") + } +} diff --git a/core/tauri/mobile/proguard-tauri.pro b/core/tauri/mobile/proguard-tauri.pro new file mode 100644 index 00000000000..6bc0b4b1ca8 --- /dev/null +++ b/core/tauri/mobile/proguard-tauri.pro @@ -0,0 +1,5 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. + +-keep class $PACKAGE.TauriActivity { + public app.tauri.plugin.PluginManager getPluginManager(); +} diff --git a/core/tauri/scripts/bundle.global.js b/core/tauri/scripts/bundle.global.js index cb155ddfef6..299376ccf66 100644 --- a/core/tauri/scripts/bundle.global.js +++ b/core/tauri/scripts/bundle.global.js @@ -1,8 +1,2 @@ -"use strict";var __TAURI_IIFE__=(()=>{var L=Object.defineProperty;var pe=Object.getOwnPropertyDescriptor;var ge=Object.getOwnPropertyNames;var he=Object.prototype.hasOwnProperty;var d=(i,e)=>{for(var t in e)L(i,t,{get:e[t],enumerable:!0})},ye=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of ge(e))!he.call(i,s)&&s!==t&&L(i,s,{get:()=>e[s],enumerable:!(r=pe(e,s))||r.enumerable});return i};var fe=i=>ye(L({},"__esModule",{value:!0}),i);var Yt={};d(Yt,{app:()=>k,cli:()=>U,clipboard:()=>z,dialog:()=>I,event:()=>V,fs:()=>j,globalShortcut:()=>q,http:()=>$,invoke:()=>Qt,notification:()=>J,os:()=>ne,path:()=>K,process:()=>Q,shell:()=>Y,tauri:()=>R,updater:()=>X,window:()=>ie});var k={};d(k,{getName:()=>we,getTauriVersion:()=>ve,getVersion:()=>Pe,hide:()=>Te,show:()=>Me});var R={};d(R,{convertFileSrc:()=>_e,invoke:()=>f,transformCallback:()=>c});function be(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function c(i,e=!1){let t=be(),r=`_${t}`;return Object.defineProperty(window,r,{value:s=>(e&&Reflect.deleteProperty(window,r),i?.(s)),writable:!1,configurable:!0}),t}async function f(i,e={}){return new Promise((t,r)=>{let s=c(l=>{t(l),Reflect.deleteProperty(window,`_${a}`)},!0),a=c(l=>{r(l),Reflect.deleteProperty(window,`_${s}`)},!0);window.__TAURI_IPC__({cmd:i,callback:s,error:a,...e})})}function _e(i,e="asset"){let t=encodeURIComponent(i);return navigator.userAgent.includes("Windows")?`https://${e}.localhost/${t}`:`${e}://localhost/${t}`}async function n(i){return f("tauri",i)}async function Pe(){return n({__tauriModule:"App",message:{cmd:"getAppVersion"}})}async function we(){return n({__tauriModule:"App",message:{cmd:"getAppName"}})}async function ve(){return n({__tauriModule:"App",message:{cmd:"getTauriVersion"}})}async function Me(){return n({__tauriModule:"App",message:{cmd:"show"}})}async function Te(){return n({__tauriModule:"App",message:{cmd:"hide"}})}var U={};d(U,{getMatches:()=>Oe});async function Oe(){return n({__tauriModule:"Cli",message:{cmd:"cliMatches"}})}var z={};d(z,{readText:()=>Ce,writeText:()=>Fe});async function Fe(i){return n({__tauriModule:"Clipboard",message:{cmd:"writeText",data:i}})}async function Ce(){return n({__tauriModule:"Clipboard",message:{cmd:"readText",data:null}})}var I={};d(I,{ask:()=>De,confirm:()=>Se,message:()=>We,open:()=>Ee,save:()=>Ae});async function Ee(i={}){return typeof i=="object"&&Object.freeze(i),n({__tauriModule:"Dialog",message:{cmd:"openDialog",options:i}})}async function Ae(i={}){return typeof i=="object"&&Object.freeze(i),n({__tauriModule:"Dialog",message:{cmd:"saveDialog",options:i}})}async function We(i,e){let t=typeof e=="string"?{title:e}:e;return n({__tauriModule:"Dialog",message:{cmd:"messageDialog",message:i.toString(),title:t?.title?.toString(),type:t?.type,buttonLabel:t?.okLabel?.toString()}})}async function De(i,e){let t=typeof e=="string"?{title:e}:e;return n({__tauriModule:"Dialog",message:{cmd:"askDialog",message:i.toString(),title:t?.title?.toString(),type:t?.type,buttonLabels:[t?.okLabel?.toString()??"Yes",t?.cancelLabel?.toString()??"No"]}})}async function Se(i,e){let t=typeof e=="string"?{title:e}:e;return n({__tauriModule:"Dialog",message:{cmd:"confirmDialog",message:i.toString(),title:t?.title?.toString(),type:t?.type,buttonLabels:[t?.okLabel?.toString()??"Ok",t?.cancelLabel?.toString()??"Cancel"]}})}var V={};d(V,{TauriEvent:()=>M,emit:()=>T,listen:()=>N,once:()=>H});async function re(i,e){return n({__tauriModule:"Event",message:{cmd:"unlisten",event:i,eventId:e}})}async function w(i,e,t){await n({__tauriModule:"Event",message:{cmd:"emit",event:i,windowLabel:e,payload:t}})}async function b(i,e,t){return n({__tauriModule:"Event",message:{cmd:"listen",event:i,windowLabel:e,handler:c(t)}}).then(r=>async()=>re(i,r))}async function v(i,e,t){return b(i,e,r=>{t(r),re(i,r.id).catch(()=>{})})}var M=(u=>(u.WINDOW_RESIZED="tauri://resize",u.WINDOW_MOVED="tauri://move",u.WINDOW_CLOSE_REQUESTED="tauri://close-requested",u.WINDOW_CREATED="tauri://window-created",u.WINDOW_DESTROYED="tauri://destroyed",u.WINDOW_FOCUS="tauri://focus",u.WINDOW_BLUR="tauri://blur",u.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",u.WINDOW_THEME_CHANGED="tauri://theme-changed",u.WINDOW_FILE_DROP="tauri://file-drop",u.WINDOW_FILE_DROP_HOVER="tauri://file-drop-hover",u.WINDOW_FILE_DROP_CANCELLED="tauri://file-drop-cancelled",u.MENU="tauri://menu",u.CHECK_UPDATE="tauri://update",u.UPDATE_AVAILABLE="tauri://update-available",u.INSTALL_UPDATE="tauri://update-install",u.STATUS_UPDATE="tauri://update-status",u.DOWNLOAD_PROGRESS="tauri://update-download-progress",u))(M||{});async function N(i,e){return b(i,null,e)}async function H(i,e){return v(i,null,e)}async function T(i,e){return w(i,void 0,e)}var j={};d(j,{BaseDirectory:()=>O,Dir:()=>O,copyFile:()=>He,createDir:()=>Ie,exists:()=>qe,readBinaryFile:()=>Re,readDir:()=>ze,readTextFile:()=>Le,removeDir:()=>Ne,removeFile:()=>Ve,renameFile:()=>je,writeBinaryFile:()=>Ue,writeFile:()=>ke,writeTextFile:()=>ke});var O=(o=>(o[o.Audio=1]="Audio",o[o.Cache=2]="Cache",o[o.Config=3]="Config",o[o.Data=4]="Data",o[o.LocalData=5]="LocalData",o[o.Desktop=6]="Desktop",o[o.Document=7]="Document",o[o.Download=8]="Download",o[o.Executable=9]="Executable",o[o.Font=10]="Font",o[o.Home=11]="Home",o[o.Picture=12]="Picture",o[o.Public=13]="Public",o[o.Runtime=14]="Runtime",o[o.Template=15]="Template",o[o.Video=16]="Video",o[o.Resource=17]="Resource",o[o.App=18]="App",o[o.Log=19]="Log",o[o.Temp=20]="Temp",o[o.AppConfig=21]="AppConfig",o[o.AppData=22]="AppData",o[o.AppLocalData=23]="AppLocalData",o[o.AppCache=24]="AppCache",o[o.AppLog=25]="AppLog",o))(O||{});async function Le(i,e={}){return n({__tauriModule:"Fs",message:{cmd:"readTextFile",path:i,options:e}})}async function Re(i,e={}){let t=await n({__tauriModule:"Fs",message:{cmd:"readFile",path:i,options:e}});return Uint8Array.from(t)}async function ke(i,e,t){typeof t=="object"&&Object.freeze(t),typeof i=="object"&&Object.freeze(i);let r={path:"",contents:""},s=t;return typeof i=="string"?r.path=i:(r.path=i.path,r.contents=i.contents),typeof e=="string"?r.contents=e??"":s=e,n({__tauriModule:"Fs",message:{cmd:"writeFile",path:r.path,contents:Array.from(new TextEncoder().encode(r.contents)),options:s}})}async function Ue(i,e,t){typeof t=="object"&&Object.freeze(t),typeof i=="object"&&Object.freeze(i);let r={path:"",contents:[]},s=t;return typeof i=="string"?r.path=i:(r.path=i.path,r.contents=i.contents),e&&"dir"in e?s=e:typeof i=="string"&&(r.contents=e??[]),n({__tauriModule:"Fs",message:{cmd:"writeFile",path:r.path,contents:Array.from(r.contents instanceof ArrayBuffer?new Uint8Array(r.contents):r.contents),options:s}})}async function ze(i,e={}){return n({__tauriModule:"Fs",message:{cmd:"readDir",path:i,options:e}})}async function Ie(i,e={}){return n({__tauriModule:"Fs",message:{cmd:"createDir",path:i,options:e}})}async function Ne(i,e={}){return n({__tauriModule:"Fs",message:{cmd:"removeDir",path:i,options:e}})}async function He(i,e,t={}){return n({__tauriModule:"Fs",message:{cmd:"copyFile",source:i,destination:e,options:t}})}async function Ve(i,e={}){return n({__tauriModule:"Fs",message:{cmd:"removeFile",path:i,options:e}})}async function je(i,e,t={}){return n({__tauriModule:"Fs",message:{cmd:"renameFile",oldPath:i,newPath:e,options:t}})}async function qe(i,e={}){return n({__tauriModule:"Fs",message:{cmd:"exists",path:i,options:e}})}var q={};d(q,{isRegistered:()=>Je,register:()=>Ge,registerAll:()=>$e,unregister:()=>Ke,unregisterAll:()=>Qe});async function Ge(i,e){return n({__tauriModule:"GlobalShortcut",message:{cmd:"register",shortcut:i,handler:c(e)}})}async function $e(i,e){return n({__tauriModule:"GlobalShortcut",message:{cmd:"registerAll",shortcuts:i,handler:c(e)}})}async function Je(i){return n({__tauriModule:"GlobalShortcut",message:{cmd:"isRegistered",shortcut:i}})}async function Ke(i){return n({__tauriModule:"GlobalShortcut",message:{cmd:"unregister",shortcut:i}})}async function Qe(){return n({__tauriModule:"GlobalShortcut",message:{cmd:"unregisterAll"}})}var $={};d($,{Body:()=>p,Client:()=>C,Response:()=>F,ResponseType:()=>se,fetch:()=>Ye,getClient:()=>ae});var se=(r=>(r[r.JSON=1]="JSON",r[r.Text=2]="Text",r[r.Binary=3]="Binary",r))(se||{}),p=class{constructor(e,t){this.type=e,this.payload=t}static form(e){let t={},r=(s,a)=>{if(a!==null){let l;typeof a=="string"?l=a:a instanceof Uint8Array||Array.isArray(a)?l=Array.from(a):a instanceof File?l={file:a.name,mime:a.type,fileName:a.name}:typeof a.file=="string"?l={file:a.file,mime:a.mime,fileName:a.fileName}:l={file:Array.from(a.file),mime:a.mime,fileName:a.fileName},t[String(s)]=l}};if(e instanceof FormData)for(let[s,a]of e)r(s,a);else for(let[s,a]of Object.entries(e))r(s,a);return new p("Form",t)}static json(e){return new p("Json",e)}static text(e){return new p("Text",e)}static bytes(e){return new p("Bytes",Array.from(e instanceof ArrayBuffer?new Uint8Array(e):e))}},F=class{constructor(e){this.url=e.url,this.status=e.status,this.ok=this.status>=200&&this.status<300,this.headers=e.headers,this.rawHeaders=e.rawHeaders,this.data=e.data}},C=class{constructor(e){this.id=e}async drop(){return n({__tauriModule:"Http",message:{cmd:"dropClient",client:this.id}})}async request(e){let t=!e.responseType||e.responseType===1;return t&&(e.responseType=2),n({__tauriModule:"Http",message:{cmd:"httpRequest",client:this.id,options:e}}).then(r=>{let s=new F(r);if(t){try{s.data=JSON.parse(s.data)}catch(a){if(s.ok&&s.data==="")s.data={};else if(s.ok)throw Error(`Failed to parse response \`${s.data}\` as JSON: ${a}; - try setting the \`responseType\` option to \`ResponseType.Text\` or \`ResponseType.Binary\` if the API does not return a JSON response.`)}return s}return s})}async get(e,t){return this.request({method:"GET",url:e,...t})}async post(e,t,r){return this.request({method:"POST",url:e,body:t,...r})}async put(e,t,r){return this.request({method:"PUT",url:e,body:t,...r})}async patch(e,t){return this.request({method:"PATCH",url:e,...t})}async delete(e,t){return this.request({method:"DELETE",url:e,...t})}};async function ae(i){return n({__tauriModule:"Http",message:{cmd:"createClient",options:i}}).then(e=>new C(e))}var G=null;async function Ye(i,e){return G===null&&(G=await ae()),G.request({url:i,method:e?.method??"GET",...e})}var J={};d(J,{isPermissionGranted:()=>Ze,requestPermission:()=>Xe,sendNotification:()=>Be});async function Ze(){return window.Notification.permission!=="default"?Promise.resolve(window.Notification.permission==="granted"):n({__tauriModule:"Notification",message:{cmd:"isNotificationPermissionGranted"}})}async function Xe(){return window.Notification.requestPermission()}function Be(i){typeof i=="string"?new window.Notification(i):new window.Notification(i.title,i)}var K={};d(K,{BaseDirectory:()=>O,appCacheDir:()=>nt,appConfigDir:()=>oe,appDataDir:()=>tt,appDir:()=>et,appLocalDataDir:()=>it,appLogDir:()=>le,audioDir:()=>rt,basename:()=>Wt,cacheDir:()=>st,configDir:()=>at,dataDir:()=>ot,delimiter:()=>Tt,desktopDir:()=>lt,dirname:()=>Et,documentDir:()=>ut,downloadDir:()=>dt,executableDir:()=>ct,extname:()=>At,fontDir:()=>mt,homeDir:()=>pt,isAbsolute:()=>Dt,join:()=>Ct,localDataDir:()=>gt,logDir:()=>vt,normalize:()=>Ft,pictureDir:()=>ht,publicDir:()=>yt,resolve:()=>Ot,resolveResource:()=>bt,resourceDir:()=>ft,runtimeDir:()=>_t,sep:()=>Mt,templateDir:()=>Pt,videoDir:()=>wt});function _(){return navigator.appVersion.includes("Win")}async function et(){return oe()}async function oe(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:21}})}async function tt(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:22}})}async function it(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:23}})}async function nt(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:24}})}async function rt(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:1}})}async function st(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:2}})}async function at(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:3}})}async function ot(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:4}})}async function lt(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:6}})}async function ut(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:7}})}async function dt(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:8}})}async function ct(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:9}})}async function mt(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:10}})}async function pt(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:11}})}async function gt(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:5}})}async function ht(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:12}})}async function yt(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:13}})}async function ft(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:17}})}async function bt(i){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:i,directory:17}})}async function _t(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:14}})}async function Pt(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:15}})}async function wt(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:16}})}async function vt(){return le()}async function le(){return n({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:25}})}var Mt=_()?"\\":"/",Tt=_()?";":":";async function Ot(...i){return n({__tauriModule:"Path",message:{cmd:"resolve",paths:i}})}async function Ft(i){return n({__tauriModule:"Path",message:{cmd:"normalize",path:i}})}async function Ct(...i){return n({__tauriModule:"Path",message:{cmd:"join",paths:i}})}async function Et(i){return n({__tauriModule:"Path",message:{cmd:"dirname",path:i}})}async function At(i){return n({__tauriModule:"Path",message:{cmd:"extname",path:i}})}async function Wt(i,e){return n({__tauriModule:"Path",message:{cmd:"basename",path:i,ext:e}})}async function Dt(i){return n({__tauriModule:"Path",message:{cmd:"isAbsolute",path:i}})}var Q={};d(Q,{exit:()=>St,relaunch:()=>xt});async function St(i=0){return n({__tauriModule:"Process",message:{cmd:"exit",exitCode:i}})}async function xt(){return n({__tauriModule:"Process",message:{cmd:"relaunch"}})}var Y={};d(Y,{Child:()=>E,Command:()=>P,EventEmitter:()=>g,open:()=>Rt});async function Lt(i,e,t=[],r){return typeof t=="object"&&Object.freeze(t),n({__tauriModule:"Shell",message:{cmd:"execute",program:e,args:t,options:r,onEventFn:c(i)}})}var g=class{constructor(){this.eventListeners=Object.create(null)}addListener(e,t){return this.on(e,t)}removeListener(e,t){return this.off(e,t)}on(e,t){return e in this.eventListeners?this.eventListeners[e].push(t):this.eventListeners[e]=[t],this}once(e,t){let r=(...s)=>{this.removeListener(e,r),t(...s)};return this.addListener(e,r)}off(e,t){return e in this.eventListeners&&(this.eventListeners[e]=this.eventListeners[e].filter(r=>r!==t)),this}removeAllListeners(e){return e?delete this.eventListeners[e]:this.eventListeners=Object.create(null),this}emit(e,...t){if(e in this.eventListeners){let r=this.eventListeners[e];for(let s of r)s(...t);return!0}return!1}listenerCount(e){return e in this.eventListeners?this.eventListeners[e].length:0}prependListener(e,t){return e in this.eventListeners?this.eventListeners[e].unshift(t):this.eventListeners[e]=[t],this}prependOnceListener(e,t){let r=(...s)=>{this.removeListener(e,r),t(...s)};return this.prependListener(e,r)}},E=class{constructor(e){this.pid=e}async write(e){return n({__tauriModule:"Shell",message:{cmd:"stdinWrite",pid:this.pid,buffer:typeof e=="string"?e:Array.from(e)}})}async kill(){return n({__tauriModule:"Shell",message:{cmd:"killChild",pid:this.pid}})}},P=class extends g{constructor(t,r=[],s){super();this.stdout=new g;this.stderr=new g;this.program=t,this.args=typeof r=="string"?[r]:r,this.options=s??{}}static sidecar(t,r=[],s){let a=new P(t,r,s);return a.options.sidecar=!0,a}async spawn(){return Lt(t=>{switch(t.event){case"Error":this.emit("error",t.payload);break;case"Terminated":this.emit("close",t.payload);break;case"Stdout":this.stdout.emit("data",t.payload);break;case"Stderr":this.stderr.emit("data",t.payload);break}},this.program,this.args,this.options).then(t=>new E(t))}async execute(){return new Promise((t,r)=>{this.on("error",r);let s=[],a=[];this.stdout.on("data",l=>{s.push(l)}),this.stderr.on("data",l=>{a.push(l)}),this.on("close",l=>{t({code:l.code,signal:l.signal,stdout:s.join(` -`),stderr:a.join(` -`)})}),this.spawn().catch(r)})}};async function Rt(i,e){return n({__tauriModule:"Shell",message:{cmd:"open",path:i,with:e}})}var X={};d(X,{checkUpdate:()=>Ut,installUpdate:()=>kt,onUpdaterEvent:()=>Z});async function Z(i){return N("tauri://update-status",e=>{i(e?.payload)})}async function kt(){let i;function e(){i&&i(),i=void 0}return new Promise((t,r)=>{function s(a){if(a.error){e(),r(a.error);return}a.status==="DONE"&&(e(),t())}Z(s).then(a=>{i=a}).catch(a=>{throw e(),a}),T("tauri://update-install").catch(a=>{throw e(),a})})}async function Ut(){let i;function e(){i&&i(),i=void 0}return new Promise((t,r)=>{function s(l){e(),t({manifest:l,shouldUpdate:!0})}function a(l){if(l.error){e(),r(l.error);return}l.status==="UPTODATE"&&(e(),t({shouldUpdate:!1}))}H("tauri://update-available",l=>{s(l?.payload)}).catch(l=>{throw e(),l}),Z(a).then(l=>{i=l}).catch(l=>{throw e(),l}),T("tauri://update").catch(l=>{throw e(),l})})}var ie={};d(ie,{CloseRequestedEvent:()=>x,LogicalPosition:()=>W,LogicalSize:()=>A,PhysicalPosition:()=>y,PhysicalSize:()=>h,UserAttentionType:()=>de,WebviewWindow:()=>m,WebviewWindowHandle:()=>D,WindowManager:()=>S,appWindow:()=>ee,availableMonitors:()=>Ht,currentMonitor:()=>It,getAll:()=>B,getCurrent:()=>zt,primaryMonitor:()=>Nt});var A=class{constructor(e,t){this.type="Logical";this.width=e,this.height=t}},h=class{constructor(e,t){this.type="Physical";this.width=e,this.height=t}toLogical(e){return new A(this.width/e,this.height/e)}},W=class{constructor(e,t){this.type="Logical";this.x=e,this.y=t}},y=class{constructor(e,t){this.type="Physical";this.x=e,this.y=t}toLogical(e){return new W(this.x/e,this.y/e)}},de=(t=>(t[t.Critical=1]="Critical",t[t.Informational=2]="Informational",t))(de||{});function zt(){return new m(window.__TAURI_METADATA__.__currentWindow.label,{skip:!0})}function B(){return window.__TAURI_METADATA__.__windows.map(i=>new m(i.label,{skip:!0}))}var ue=["tauri://created","tauri://error"],D=class{constructor(e){this.label=e,this.listeners=Object.create(null)}async listen(e,t){return this._handleTauriEvent(e,t)?Promise.resolve(()=>{let r=this.listeners[e];r.splice(r.indexOf(t),1)}):b(e,this.label,t)}async once(e,t){return this._handleTauriEvent(e,t)?Promise.resolve(()=>{let r=this.listeners[e];r.splice(r.indexOf(t),1)}):v(e,this.label,t)}async emit(e,t){if(ue.includes(e)){for(let r of this.listeners[e]||[])r({event:e,id:-1,windowLabel:this.label,payload:t});return Promise.resolve()}return w(e,this.label,t)}_handleTauriEvent(e,t){return ue.includes(e)?(e in this.listeners?this.listeners[e].push(t):this.listeners[e]=[t],!0):!1}},S=class extends D{async scaleFactor(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"scaleFactor"}}}})}async innerPosition(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"innerPosition"}}}}).then(({x:e,y:t})=>new y(e,t))}async outerPosition(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"outerPosition"}}}}).then(({x:e,y:t})=>new y(e,t))}async innerSize(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"innerSize"}}}}).then(({width:e,height:t})=>new h(e,t))}async outerSize(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"outerSize"}}}}).then(({width:e,height:t})=>new h(e,t))}async isFullscreen(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isFullscreen"}}}})}async isMinimized(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isMinimized"}}}})}async isMaximized(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isMaximized"}}}})}async isFocused(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isFocused"}}}})}async isDecorated(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isDecorated"}}}})}async isResizable(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isResizable"}}}})}async isMaximizable(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isMaximizable"}}}})}async isMinimizable(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isMinimizable"}}}})}async isClosable(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isClosable"}}}})}async isVisible(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isVisible"}}}})}async title(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"title"}}}})}async theme(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"theme"}}}})}async center(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"center"}}}})}async requestUserAttention(e){let t=null;return e&&(e===1?t={type:"Critical"}:t={type:"Informational"}),n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"requestUserAttention",payload:t}}}})}async setResizable(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setResizable",payload:e}}}})}async setMaximizable(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setMaximizable",payload:e}}}})}async setMinimizable(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setMinimizable",payload:e}}}})}async setClosable(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setClosable",payload:e}}}})}async setTitle(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setTitle",payload:e}}}})}async maximize(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"maximize"}}}})}async unmaximize(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"unmaximize"}}}})}async toggleMaximize(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"toggleMaximize"}}}})}async minimize(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"minimize"}}}})}async unminimize(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"unminimize"}}}})}async show(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"show"}}}})}async hide(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"hide"}}}})}async close(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"close"}}}})}async setDecorations(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setDecorations",payload:e}}}})}async setAlwaysOnTop(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setAlwaysOnTop",payload:e}}}})}async setContentProtected(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setContentProtected",payload:e}}}})}async setSize(e){if(!e||e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `size` argument must be either a LogicalSize or a PhysicalSize instance");return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setSize",payload:{type:e.type,data:{width:e.width,height:e.height}}}}}})}async setMinSize(e){if(e&&e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `size` argument must be either a LogicalSize or a PhysicalSize instance");return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setMinSize",payload:e?{type:e.type,data:{width:e.width,height:e.height}}:null}}}})}async setMaxSize(e){if(e&&e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `size` argument must be either a LogicalSize or a PhysicalSize instance");return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setMaxSize",payload:e?{type:e.type,data:{width:e.width,height:e.height}}:null}}}})}async setPosition(e){if(!e||e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `position` argument must be either a LogicalPosition or a PhysicalPosition instance");return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setPosition",payload:{type:e.type,data:{x:e.x,y:e.y}}}}}})}async setFullscreen(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setFullscreen",payload:e}}}})}async setFocus(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setFocus"}}}})}async setIcon(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setIcon",payload:{icon:typeof e=="string"?e:Array.from(e)}}}}})}async setSkipTaskbar(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setSkipTaskbar",payload:e}}}})}async setCursorGrab(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setCursorGrab",payload:e}}}})}async setCursorVisible(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setCursorVisible",payload:e}}}})}async setCursorIcon(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setCursorIcon",payload:e}}}})}async setCursorPosition(e){if(!e||e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `position` argument must be either a LogicalPosition or a PhysicalPosition instance");return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setCursorPosition",payload:{type:e.type,data:{x:e.x,y:e.y}}}}}})}async setIgnoreCursorEvents(e){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setIgnoreCursorEvents",payload:e}}}})}async startDragging(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"startDragging"}}}})}async onResized(e){return this.listen("tauri://resize",t=>{t.payload=me(t.payload),e(t)})}async onMoved(e){return this.listen("tauri://move",t=>{t.payload=ce(t.payload),e(t)})}async onCloseRequested(e){return this.listen("tauri://close-requested",t=>{let r=new x(t);Promise.resolve(e(r)).then(()=>{if(!r.isPreventDefault())return this.close()})})}async onFocusChanged(e){let t=await this.listen("tauri://focus",s=>{e({...s,payload:!0})}),r=await this.listen("tauri://blur",s=>{e({...s,payload:!1})});return()=>{t(),r()}}async onScaleChanged(e){return this.listen("tauri://scale-change",e)}async onMenuClicked(e){return this.listen("tauri://menu",e)}async onFileDropEvent(e){let t=await this.listen("tauri://file-drop",a=>{e({...a,payload:{type:"drop",paths:a.payload}})}),r=await this.listen("tauri://file-drop-hover",a=>{e({...a,payload:{type:"hover",paths:a.payload}})}),s=await this.listen("tauri://file-drop-cancelled",a=>{e({...a,payload:{type:"cancel"}})});return()=>{t(),r(),s()}}async onThemeChanged(e){return this.listen("tauri://theme-changed",e)}},x=class{constructor(e){this._preventDefault=!1;this.event=e.event,this.windowLabel=e.windowLabel,this.id=e.id}preventDefault(){this._preventDefault=!0}isPreventDefault(){return this._preventDefault}},m=class extends S{constructor(e,t={}){super(e),t?.skip||n({__tauriModule:"Window",message:{cmd:"createWebview",data:{options:{label:e,...t}}}}).then(async()=>this.emit("tauri://created")).catch(async r=>this.emit("tauri://error",r))}static getByLabel(e){return B().some(t=>t.label===e)?new m(e,{skip:!0}):null}static async getFocusedWindow(){for(let e of B())if(await e.isFocused())return e;return null}},ee;"__TAURI_METADATA__"in window?ee=new m(window.__TAURI_METADATA__.__currentWindow.label,{skip:!0}):(console.warn(`Could not find "window.__TAURI_METADATA__". The "appWindow" value will reference the "main" window label. -Note that this is not an issue if running this frontend on a browser instead of a Tauri window.`),ee=new m("main",{skip:!0}));function te(i){return i===null?null:{name:i.name,scaleFactor:i.scaleFactor,position:ce(i.position),size:me(i.size)}}function ce(i){return new y(i.x,i.y)}function me(i){return new h(i.width,i.height)}async function It(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{cmd:{type:"currentMonitor"}}}}).then(te)}async function Nt(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{cmd:{type:"primaryMonitor"}}}}).then(te)}async function Ht(){return n({__tauriModule:"Window",message:{cmd:"manage",data:{cmd:{type:"availableMonitors"}}}}).then(i=>i.map(te))}var ne={};d(ne,{EOL:()=>Vt,arch:()=>$t,locale:()=>Kt,platform:()=>jt,tempdir:()=>Jt,type:()=>Gt,version:()=>qt});var Vt=_()?`\r -`:` -`;async function jt(){return n({__tauriModule:"Os",message:{cmd:"platform"}})}async function qt(){return n({__tauriModule:"Os",message:{cmd:"version"}})}async function Gt(){return n({__tauriModule:"Os",message:{cmd:"osType"}})}async function $t(){return n({__tauriModule:"Os",message:{cmd:"arch"}})}async function Jt(){return n({__tauriModule:"Os",message:{cmd:"tempdir"}})}async function Kt(){return n({__tauriModule:"Os",message:{cmd:"locale"}})}var Qt=f;return fe(Yt);})(); +"use strict";var __TAURI_IIFE__=(()=>{var m=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var E=Object.getOwnPropertyNames;var N=Object.prototype.hasOwnProperty;var p=(n,r)=>{for(var i in r)m(n,i,{get:r[i],enumerable:!0})},W=(n,r,i,o)=>{if(r&&typeof r=="object"||typeof r=="function")for(let a of E(r))!N.call(n,a)&&a!==i&&m(n,a,{get:()=>r[a],enumerable:!(o=C(r,a))||o.enumerable});return n};var k=n=>W(m({},"__esModule",{value:!0}),n);var D=(n,r,i)=>{if(!r.has(n))throw TypeError("Cannot "+i)};var _=(n,r,i)=>(D(n,r,"read from private field"),i?i.call(n):r.get(n)),w=(n,r,i)=>{if(r.has(n))throw TypeError("Cannot add the same private member more than once");r instanceof WeakSet?r.add(n):r.set(n,i)},A=(n,r,i,o)=>(D(n,r,"write to private field"),o?o.call(n,i):r.set(n,i),i);var vn={};p(vn,{event:()=>f,invoke:()=>hn,path:()=>h,tauri:()=>y});var f={};p(f,{TauriEvent:()=>b,emit:()=>x,listen:()=>R,once:()=>F});var y={};p(y,{Channel:()=>l,PluginListener:()=>g,addPluginListener:()=>L,convertFileSrc:()=>U,invoke:()=>t,transformCallback:()=>u});function T(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function u(n,r=!1){let i=T(),o=`_${i}`;return Object.defineProperty(window,o,{value:a=>(r&&Reflect.deleteProperty(window,o),n?.(a)),writable:!1,configurable:!0}),i}var c,l=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0;w(this,c,()=>{});this.id=u(r=>{_(this,c).call(this,r)})}set onmessage(r){A(this,c,r)}get onmessage(){return _(this,c)}toJSON(){return`__CHANNEL__:${this.id}`}};c=new WeakMap;var g=class{constructor(r,i,o){this.plugin=r,this.event=i,this.channelId=o}async unregister(){return t(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function L(n,r,i){let o=new l;return o.onmessage=i,t(`plugin:${n}|register_listener`,{event:r,handler:o}).then(()=>new g(n,r,o.id))}async function t(n,r={},i){return new Promise((o,a)=>{let v=u(d=>{o(d),Reflect.deleteProperty(window,`_${P}`)},!0),P=u(d=>{a(d),Reflect.deleteProperty(window,`_${v}`)},!0);window.__TAURI_IPC__({cmd:n,callback:v,error:P,payload:r,options:i})})}function U(n,r="asset"){return window.__TAURI__.convertFileSrc(n,r)}var b=(s=>(s.WINDOW_RESIZED="tauri://resize",s.WINDOW_MOVED="tauri://move",s.WINDOW_CLOSE_REQUESTED="tauri://close-requested",s.WINDOW_CREATED="tauri://window-created",s.WINDOW_DESTROYED="tauri://destroyed",s.WINDOW_FOCUS="tauri://focus",s.WINDOW_BLUR="tauri://blur",s.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",s.WINDOW_THEME_CHANGED="tauri://theme-changed",s.WINDOW_FILE_DROP="tauri://file-drop",s.WINDOW_FILE_DROP_HOVER="tauri://file-drop-hover",s.WINDOW_FILE_DROP_CANCELLED="tauri://file-drop-cancelled",s.MENU="tauri://menu",s))(b||{});async function I(n,r){await t("plugin:event|unlisten",{event:n,eventId:r})}async function R(n,r,i){return t("plugin:event|listen",{event:n,windowLabel:i?.target,handler:u(r)}).then(o=>async()=>I(n,o))}async function F(n,r,i){return R(n,o=>{r(o),I(n,o.id).catch(()=>{})},i)}async function x(n,r,i){await t("plugin:event|emit",{event:n,windowLabel:i?.target,payload:r})}var h={};p(h,{BaseDirectory:()=>O,appCacheDir:()=>V,appConfigDir:()=>S,appDataDir:()=>H,appLocalDataDir:()=>$,appLogDir:()=>an,audioDir:()=>M,basename:()=>yn,cacheDir:()=>j,configDir:()=>z,dataDir:()=>G,delimiter:()=>pn,desktopDir:()=>q,dirname:()=>mn,documentDir:()=>J,downloadDir:()=>K,executableDir:()=>Q,extname:()=>_n,fontDir:()=>Y,homeDir:()=>Z,isAbsolute:()=>fn,join:()=>dn,localDataDir:()=>X,normalize:()=>gn,pictureDir:()=>B,publicDir:()=>nn,resolve:()=>ln,resolveResource:()=>en,resourceDir:()=>rn,runtimeDir:()=>tn,sep:()=>un,tempDir:()=>cn,templateDir:()=>on,videoDir:()=>sn});var O=(e=>(e[e.Audio=1]="Audio",e[e.Cache=2]="Cache",e[e.Config=3]="Config",e[e.Data=4]="Data",e[e.LocalData=5]="LocalData",e[e.Document=6]="Document",e[e.Download=7]="Download",e[e.Picture=8]="Picture",e[e.Public=9]="Public",e[e.Video=10]="Video",e[e.Resource=11]="Resource",e[e.Temp=12]="Temp",e[e.AppConfig=13]="AppConfig",e[e.AppData=14]="AppData",e[e.AppLocalData=15]="AppLocalData",e[e.AppCache=16]="AppCache",e[e.AppLog=17]="AppLog",e[e.Desktop=18]="Desktop",e[e.Executable=19]="Executable",e[e.Font=20]="Font",e[e.Home=21]="Home",e[e.Runtime=22]="Runtime",e[e.Template=23]="Template",e))(O||{});async function S(){return t("plugin:path|resolve_directory",{directory:13})}async function H(){return t("plugin:path|resolve_directory",{directory:14})}async function $(){return t("plugin:path|resolve_directory",{directory:15})}async function V(){return t("plugin:path|resolve_directory",{directory:16})}async function M(){return t("plugin:path|resolve_directory",{directory:1})}async function j(){return t("plugin:path|resolve_directory",{directory:2})}async function z(){return t("plugin:path|resolve_directory",{directory:3})}async function G(){return t("plugin:path|resolve_directory",{directory:4})}async function q(){return t("plugin:path|resolve_directory",{directory:18})}async function J(){return t("plugin:path|resolve_directory",{directory:6})}async function K(){return t("plugin:path|resolve_directory",{directory:7})}async function Q(){return t("plugin:path|resolve_directory",{directory:19})}async function Y(){return t("plugin:path|resolve_directory",{directory:20})}async function Z(){return t("plugin:path|resolve_directory",{directory:21})}async function X(){return t("plugin:path|resolve_directory",{directory:5})}async function B(){return t("plugin:path|resolve_directory",{directory:8})}async function nn(){return t("plugin:path|resolve_directory",{directory:9})}async function rn(){return t("plugin:path|resolve_directory",{directory:11})}async function en(n){return t("plugin:path|resolve_directory",{directory:11,path:n})}async function tn(){return t("plugin:path|resolve_directory",{directory:22})}async function on(){return t("plugin:path|resolve_directory",{directory:23})}async function sn(){return t("plugin:path|resolve_directory",{directory:10})}async function an(){return t("plugin:path|resolve_directory",{directory:17})}async function cn(n){return t("plugin:path|resolve_directory",{directory:12})}function un(){return window.__TAURI__.path.__sep}function pn(){return window.__TAURI__.path.__delimiter}async function ln(...n){return t("plugin:path|resolve",{paths:n})}async function gn(n){return t("plugin:path|normalize",{path:n})}async function dn(...n){return t("plugin:path|join",{paths:n})}async function mn(n){return t("plugin:path|dirname",{path:n})}async function _n(n){return t("plugin:path|extname",{path:n})}async function yn(n,r){return t("plugin:path|basename",{path:n,ext:r})}async function fn(n){return t("plugin:path|isAbsolute",{path:n})}var hn=t;return k(vn);})(); window.__TAURI__ = __TAURI_IIFE__ diff --git a/core/tauri/scripts/bundle.js b/core/tauri/scripts/bundle.js deleted file mode 100644 index 4a7da586b92..00000000000 --- a/core/tauri/scripts/bundle.js +++ /dev/null @@ -1 +0,0 @@ -function _inherits(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&_setPrototypeOf(e,t)}function _setPrototypeOf(e,t){return _setPrototypeOf=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},_setPrototypeOf(e,t)}function _createSuper(e){var t=_isNativeReflectConstruct();return function(){var r,n=_getPrototypeOf(e);if(t){var a=_getPrototypeOf(this).constructor;r=Reflect.construct(n,arguments,a)}else r=n.apply(this,arguments);return _possibleConstructorReturn(this,r)}}function _possibleConstructorReturn(e,t){if(t&&("object"===_typeof(t)||"function"==typeof t))return t;if(void 0!==t)throw new TypeError("Derived constructors may only return object or undefined");return _assertThisInitialized(e)}function _assertThisInitialized(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function _isNativeReflectConstruct(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}function _getPrototypeOf(e){return _getPrototypeOf=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},_getPrototypeOf(e)}function _createForOfIteratorHelper(e,t){var r="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!r){if(Array.isArray(e)||(r=_unsupportedIterableToArray(e))||t&&e&&"number"==typeof e.length){r&&(e=r);var n=0,a=function(){};return{s:a,n:function(){return n>=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,i=!0,u=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return i=e.done,e},e:function(e){u=!0,o=e},f:function(){try{i||null==r.return||r.return()}finally{if(u)throw o}}}}function _unsupportedIterableToArray(e,t){if(e){if("string"==typeof e)return _arrayLikeToArray(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);return"Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?_arrayLikeToArray(e,t):void 0}}function _arrayLikeToArray(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0;--a){var o=this.tryEntries[a],i=o.completion;if("root"===o.tryLoc)return n("end");if(o.tryLoc<=this.prev){var u=r.call(o,"catchLoc"),s=r.call(o,"finallyLoc");if(u&&s){if(this.prev=0;--n){var a=this.tryEntries[n];if(a.tryLoc<=this.prev&&r.call(a,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),R(r),p}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var a=n.arg;R(r)}return a}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:x(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=void 0),p}},e}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _defineProperties(e,t){for(var r=0;r=0;--o){var i=this.tryEntries[o],u=i.completion;if("root"===i.tryLoc)return a("end");if(i.tryLoc<=this.prev){var s=n.call(i,"catchLoc"),c=n.call(i,"finallyLoc");if(s&&c){if(this.prev=0;--r){var a=this.tryEntries[r];if(a.tryLoc<=this.prev&&n.call(a,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),P(r),m}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var a=n.arg;P(r)}return a}}throw new Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:A(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),m}},e}("object"===("undefined"==typeof module?"undefined":_typeof(module))?module.exports:{});try{regeneratorRuntime=t}catch(e){"object"===("undefined"==typeof globalThis?"undefined":_typeof(globalThis))?globalThis.regeneratorRuntime=t:Function("r","regeneratorRuntime = r")(t)}function r(e){for(var t=void 0,r=e[0],n=1;n1&&void 0!==arguments[1]&&arguments[1],a=n(),o="_".concat(a);return Object.defineProperty(window,o,{value:function(n){return t&&Reflect.deleteProperty(window,o),r([e,"optionalCall",function(e){return e(n)}])},writable:!1,configurable:!0}),a}function o(e){return i.apply(this,arguments)}function i(){return i=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){var r,n=arguments;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=n.length>1&&void 0!==n[1]?n[1]:{},e.abrupt("return",new Promise((function(e,n){var o=a((function(t){e(t),Reflect.deleteProperty(window,"_".concat(i))}),!0),i=a((function(e){n(e),Reflect.deleteProperty(window,"_".concat(o))}),!0);window.__TAURI_IPC__(_objectSpread({cmd:t,callback:o,error:i},r))})));case 2:case"end":return e.stop()}}),e)}))),i.apply(this,arguments)}var u=Object.freeze({__proto__:null,transformCallback:a,invoke:o,convertFileSrc:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"asset",r=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?"https://".concat(t,".localhost/").concat(r):"".concat(t,"://").concat(r)}});function s(e){return c.apply(this,arguments)}function c(){return(c=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",o("tauri",t));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function p(){return(p=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"App",message:{cmd:"getAppVersion"}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function l(){return(l=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"App",message:{cmd:"getAppName"}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function f(){return(f=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"App",message:{cmd:"getTauriVersion"}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var h=Object.freeze({__proto__:null,getName:function(){return l.apply(this,arguments)},getVersion:function(){return p.apply(this,arguments)},getTauriVersion:function(){return f.apply(this,arguments)}});function d(){return(d=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Cli",message:{cmd:"cliMatches"}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var m=Object.freeze({__proto__:null,getMatches:function(){return d.apply(this,arguments)}});function _(){return(_=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Clipboard",message:{cmd:"writeText",data:t}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function y(){return(y=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Clipboard",message:{cmd:"readText",data:null}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var g=Object.freeze({__proto__:null,writeText:function(e){return _.apply(this,arguments)},readText:function(){return y.apply(this,arguments)}});function v(e,t){return null!=e?e:t()}function w(e){for(var t=void 0,r=e[0],n=1;n0&&void 0!==r[0]?r[0]:{})&&Object.freeze(t),e.abrupt("return",s({__tauriModule:"Dialog",message:{cmd:"openDialog",options:t}}));case 3:case"end":return e.stop()}}),e)}))),b.apply(this,arguments)}function R(){return R=_asyncToGenerator(_regeneratorRuntime().mark((function e(){var t,r=arguments;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return"object"===_typeof(t=r.length>0&&void 0!==r[0]?r[0]:{})&&Object.freeze(t),e.abrupt("return",s({__tauriModule:"Dialog",message:{cmd:"saveDialog",options:t}}));case 3:case"end":return e.stop()}}),e)}))),R.apply(this,arguments)}function k(){return k=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){var n;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n="string"==typeof r?{title:r}:r,e.abrupt("return",s({__tauriModule:"Dialog",message:{cmd:"messageDialog",message:t.toString(),title:w([n,"optionalAccess",function(e){return e.title},"optionalAccess",function(e){return e.toString},"call",function(e){return e()}]),type:w([n,"optionalAccess",function(e){return e.type}]),buttonLabel:w([n,"optionalAccess",function(e){return e.okLabel},"optionalAccess",function(e){return e.toString},"call",function(e){return e()}])}}));case 2:case"end":return e.stop()}}),e)}))),k.apply(this,arguments)}function x(){return x=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){var n;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n="string"==typeof r?{title:r}:r,e.abrupt("return",s({__tauriModule:"Dialog",message:{cmd:"askDialog",message:t.toString(),title:w([n,"optionalAccess",function(e){return e.title},"optionalAccess",function(e){return e.toString},"call",function(e){return e()}]),type:w([n,"optionalAccess",function(e){return e.type}]),buttonLabels:[v(w([n,"optionalAccess",function(e){return e.okLabel},"optionalAccess",function(e){return e.toString},"call",function(e){return e()}]),(function(){return"Yes"})),v(w([n,"optionalAccess",function(e){return e.cancelLabel},"optionalAccess",function(e){return e.toString},"call",function(e){return e()}]),(function(){return"No"}))]}}));case 2:case"end":return e.stop()}}),e)}))),x.apply(this,arguments)}function T(){return T=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){var n;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n="string"==typeof r?{title:r}:r,e.abrupt("return",s({__tauriModule:"Dialog",message:{cmd:"confirmDialog",message:t.toString(),title:w([n,"optionalAccess",function(e){return e.title},"optionalAccess",function(e){return e.toString},"call",function(e){return e()}]),type:w([n,"optionalAccess",function(e){return e.type}]),buttonLabels:[v(w([n,"optionalAccess",function(e){return e.okLabel},"optionalAccess",function(e){return e.toString},"call",function(e){return e()}]),(function(){return"Ok"})),v(w([n,"optionalAccess",function(e){return e.cancelLabel},"optionalAccess",function(e){return e.toString},"call",function(e){return e()}]),(function(){return"Cancel"}))]}}));case 2:case"end":return e.stop()}}),e)}))),T.apply(this,arguments)}var G,P=Object.freeze({__proto__:null,open:function(){return b.apply(this,arguments)},save:function(){return R.apply(this,arguments)},message:function(e,t){return k.apply(this,arguments)},ask:function(e,t){return x.apply(this,arguments)},confirm:function(e,t){return T.apply(this,arguments)}});function O(e,t){return A.apply(this,arguments)}function A(){return A=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Event",message:{cmd:"unlisten",event:t,eventId:r}}));case 1:case"end":return e.stop()}}),e)}))),A.apply(this,arguments)}function L(e,t,r){return M.apply(this,arguments)}function M(){return M=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r,n){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,s({__tauriModule:"Event",message:{cmd:"emit",event:t,windowLabel:r,payload:"string"==typeof n?n:JSON.stringify(n)}});case 2:case"end":return e.stop()}}),e)}))),M.apply(this,arguments)}function E(e,t,r){return D.apply(this,arguments)}function D(){return D=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r,n){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Event",message:{cmd:"listen",event:t,windowLabel:r,handler:a(n)}}).then((function(e){return _asyncToGenerator(_regeneratorRuntime().mark((function r(){return _regeneratorRuntime().wrap((function(r){for(;;)switch(r.prev=r.next){case 0:return r.abrupt("return",O(t,e));case 1:case"end":return r.stop()}}),r)})))})));case 1:case"end":return e.stop()}}),e)}))),D.apply(this,arguments)}function C(e,t,r){return S.apply(this,arguments)}function S(){return S=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r,n){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",E(t,r,(function(e){n(e),O(t,e.id).catch((function(){}))})));case 1:case"end":return e.stop()}}),e)}))),S.apply(this,arguments)}function j(e,t){return W.apply(this,arguments)}function W(){return W=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",E(t,null,r));case 1:case"end":return e.stop()}}),e)}))),W.apply(this,arguments)}function N(e,t){return I.apply(this,arguments)}function I(){return I=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",C(t,null,r));case 1:case"end":return e.stop()}}),e)}))),I.apply(this,arguments)}function z(e,t){return F.apply(this,arguments)}function F(){return F=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",L(t,void 0,r));case 1:case"end":return e.stop()}}),e)}))),F.apply(this,arguments)}!function(e){e.WINDOW_RESIZED="tauri://resize";e.WINDOW_MOVED="tauri://move";e.WINDOW_CLOSE_REQUESTED="tauri://close-requested";e.WINDOW_CREATED="tauri://window-created";e.WINDOW_DESTROYED="tauri://destroyed";e.WINDOW_FOCUS="tauri://focus";e.WINDOW_BLUR="tauri://blur";e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change";e.WINDOW_THEME_CHANGED="tauri://theme-changed";e.WINDOW_FILE_DROP="tauri://file-drop";e.WINDOW_FILE_DROP_HOVER="tauri://file-drop-hover";e.WINDOW_FILE_DROP_CANCELLED="tauri://file-drop-cancelled";e.MENU="tauri://menu";e.CHECK_UPDATE="tauri://update";e.UPDATE_AVAILABLE="tauri://update-available";e.INSTALL_UPDATE="tauri://update-install";e.STATUS_UPDATE="tauri://update-status";e.DOWNLOAD_PROGRESS="tauri://update-download-progress"}(G||(G={}));var U,H=Object.freeze({__proto__:null,listen:j,once:N,emit:z});function V(e,t){return null!=e?e:t()}function B(){return B=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){var r,n=arguments;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=n.length>1&&void 0!==n[1]?n[1]:{},e.abrupt("return",s({__tauriModule:"Fs",message:{cmd:"readTextFile",path:t,options:r}}));case 2:case"end":return e.stop()}}),e)}))),B.apply(this,arguments)}function q(){return q=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){var r,n,a=arguments;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=a.length>1&&void 0!==a[1]?a[1]:{},e.next=3,s({__tauriModule:"Fs",message:{cmd:"readFile",path:t,options:r}});case 3:return n=e.sent,e.abrupt("return",Uint8Array.from(n));case 5:case"end":return e.stop()}}),e)}))),q.apply(this,arguments)}function J(e,t,r){return Y.apply(this,arguments)}function Y(){return Y=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r,n){var a,o;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return"object"===_typeof(n)&&Object.freeze(n),"object"===_typeof(t)&&Object.freeze(t),a={path:"",contents:""},o=n,"string"==typeof t?a.path=t:(a.path=t.path,a.contents=t.contents),"string"==typeof r?a.contents=V(r,(function(){return""})):o=r,e.abrupt("return",s({__tauriModule:"Fs",message:{cmd:"writeFile",path:a.path,contents:Array.from((new TextEncoder).encode(a.contents)),options:o}}));case 7:case"end":return e.stop()}}),e)}))),Y.apply(this,arguments)}function K(){return K=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r,n){var a,o;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return"object"===_typeof(n)&&Object.freeze(n),"object"===_typeof(t)&&Object.freeze(t),a={path:"",contents:[]},o=n,"string"==typeof t?a.path=t:(a.path=t.path,a.contents=t.contents),r&&"dir"in r?o=r:"string"==typeof t&&(a.contents=V(r,(function(){return[]}))),e.abrupt("return",s({__tauriModule:"Fs",message:{cmd:"writeFile",path:a.path,contents:Array.from(a.contents instanceof ArrayBuffer?new Uint8Array(a.contents):a.contents),options:o}}));case 7:case"end":return e.stop()}}),e)}))),K.apply(this,arguments)}function Q(){return Q=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){var r,n=arguments;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=n.length>1&&void 0!==n[1]?n[1]:{},e.abrupt("return",s({__tauriModule:"Fs",message:{cmd:"readDir",path:t,options:r}}));case 2:case"end":return e.stop()}}),e)}))),Q.apply(this,arguments)}function Z(){return Z=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){var r,n=arguments;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=n.length>1&&void 0!==n[1]?n[1]:{},e.abrupt("return",s({__tauriModule:"Fs",message:{cmd:"createDir",path:t,options:r}}));case 2:case"end":return e.stop()}}),e)}))),Z.apply(this,arguments)}function $(){return $=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){var r,n=arguments;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=n.length>1&&void 0!==n[1]?n[1]:{},e.abrupt("return",s({__tauriModule:"Fs",message:{cmd:"removeDir",path:t,options:r}}));case 2:case"end":return e.stop()}}),e)}))),$.apply(this,arguments)}function X(){return X=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){var n,a=arguments;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=a.length>2&&void 0!==a[2]?a[2]:{},e.abrupt("return",s({__tauriModule:"Fs",message:{cmd:"copyFile",source:t,destination:r,options:n}}));case 2:case"end":return e.stop()}}),e)}))),X.apply(this,arguments)}function ee(){return ee=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){var r,n=arguments;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r=n.length>1&&void 0!==n[1]?n[1]:{},e.abrupt("return",s({__tauriModule:"Fs",message:{cmd:"removeFile",path:t,options:r}}));case 2:case"end":return e.stop()}}),e)}))),ee.apply(this,arguments)}function te(){return te=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){var n,a=arguments;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=a.length>2&&void 0!==a[2]?a[2]:{},e.abrupt("return",s({__tauriModule:"Fs",message:{cmd:"renameFile",oldPath:t,newPath:r,options:n}}));case 2:case"end":return e.stop()}}),e)}))),te.apply(this,arguments)}!function(e){e[e.Audio=1]="Audio";e[e.Cache=2]="Cache";e[e.Config=3]="Config";e[e.Data=4]="Data";e[e.LocalData=5]="LocalData";e[e.Desktop=6]="Desktop";e[e.Document=7]="Document";e[e.Download=8]="Download";e[e.Executable=9]="Executable";e[e.Font=10]="Font";e[e.Home=11]="Home";e[e.Picture=12]="Picture";e[e.Public=13]="Public";e[e.Runtime=14]="Runtime";e[e.Template=15]="Template";e[e.Video=16]="Video";e[e.Resource=17]="Resource";e[e.App=18]="App";e[e.Log=19]="Log";e[e.Temp=20]="Temp"}(U||(U={}));var re=Object.freeze({__proto__:null,get BaseDirectory(){return U},get Dir(){return U},readTextFile:function(e){return B.apply(this,arguments)},readBinaryFile:function(e){return q.apply(this,arguments)},writeTextFile:J,writeFile:J,writeBinaryFile:function(e,t,r){return K.apply(this,arguments)},readDir:function(e){return Q.apply(this,arguments)},createDir:function(e){return Z.apply(this,arguments)},removeDir:function(e){return $.apply(this,arguments)},copyFile:function(e,t){return X.apply(this,arguments)},removeFile:function(e){return ee.apply(this,arguments)},renameFile:function(e,t){return te.apply(this,arguments)}});function ne(){return(ne=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"GlobalShortcut",message:{cmd:"register",shortcut:t,handler:a(r)}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function ae(){return(ae=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"GlobalShortcut",message:{cmd:"registerAll",shortcuts:t,handler:a(r)}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function oe(){return(oe=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"GlobalShortcut",message:{cmd:"isRegistered",shortcut:t}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function ie(){return(ie=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"GlobalShortcut",message:{cmd:"unregister",shortcut:t}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function ue(){return(ue=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"GlobalShortcut",message:{cmd:"unregisterAll"}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var se,ce=Object.freeze({__proto__:null,register:function(e,t){return ne.apply(this,arguments)},registerAll:function(e,t){return ae.apply(this,arguments)},isRegistered:function(e){return oe.apply(this,arguments)},unregister:function(e){return ie.apply(this,arguments)},unregisterAll:function(){return ue.apply(this,arguments)}});function pe(e,t){return null!=e?e:t()}function le(e){for(var t=void 0,r=e[0],n=1;n=200&&this.status<300,this.headers=t.headers,this.rawHeaders=t.rawHeaders,this.data=t.data})),de=function(){function e(t){_classCallCheck(this,e),this.id=t}var t,r,n,a,o,i,u;return _createClass(e,[{key:"drop",value:(u=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Http",message:{cmd:"dropClient",client:this.id}}));case 1:case"end":return e.stop()}}),e,this)}))),function(){return u.apply(this,arguments)})},{key:"request",value:(i=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){var r;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return(r=!t.responseType||t.responseType===se.JSON)&&(t.responseType=se.Text),e.abrupt("return",s({__tauriModule:"Http",message:{cmd:"httpRequest",client:this.id,options:t}}).then((function(e){var t=new he(e);if(r){try{t.data=JSON.parse(t.data)}catch(e){if(t.ok&&""===t.data)t.data={};else if(t.ok)throw Error("Failed to parse response `".concat(t.data,"` as JSON: ").concat(e,";\n try setting the `responseType` option to `ResponseType.Text` or `ResponseType.Binary` if the API does not return a JSON response."))}return t}return t})));case 3:case"end":return e.stop()}}),e,this)}))),function(e){return i.apply(this,arguments)})},{key:"get",value:(o=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",this.request(_objectSpread({method:"GET",url:t},r)));case 1:case"end":return e.stop()}}),e,this)}))),function(e,t){return o.apply(this,arguments)})},{key:"post",value:(a=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r,n){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",this.request(_objectSpread({method:"POST",url:t,body:r},n)));case 1:case"end":return e.stop()}}),e,this)}))),function(e,t,r){return a.apply(this,arguments)})},{key:"put",value:(n=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r,n){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",this.request(_objectSpread({method:"PUT",url:t,body:r},n)));case 1:case"end":return e.stop()}}),e,this)}))),function(e,t,r){return n.apply(this,arguments)})},{key:"patch",value:(r=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",this.request(_objectSpread({method:"PATCH",url:t},r)));case 1:case"end":return e.stop()}}),e,this)}))),function(e,t){return r.apply(this,arguments)})},{key:"delete",value:(t=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",this.request(_objectSpread({method:"DELETE",url:t},r)));case 1:case"end":return e.stop()}}),e,this)}))),function(e,r){return t.apply(this,arguments)})}]),e}();function me(e){return _e.apply(this,arguments)}function _e(){return(_e=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Http",message:{cmd:"createClient",options:t}}).then((function(e){return new de(e)})));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var ye=null;function ge(){return(ge=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(null!==ye){e.next=4;break}return e.next=3,me();case 3:ye=e.sent;case 4:return e.abrupt("return",ye.request(_objectSpread({url:t,method:pe(le([r,"optionalAccess",function(e){return e.method}]),(function(){return"GET"}))},r)));case 5:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var ve=Object.freeze({__proto__:null,getClient:me,fetch:function(e,t){return ge.apply(this,arguments)},Body:fe,Client:de,Response:he,get ResponseType(){return se}});function we(){return(we=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if("default"===window.Notification.permission){e.next=2;break}return e.abrupt("return",Promise.resolve("granted"===window.Notification.permission));case 2:return e.abrupt("return",s({__tauriModule:"Notification",message:{cmd:"isNotificationPermissionGranted"}}));case 3:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function be(){return(be=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",window.Notification.requestPermission());case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var Re=Object.freeze({__proto__:null,sendNotification:function(e){"string"==typeof e?new window.Notification(e):new window.Notification(e.title,e)},requestPermission:function(){return be.apply(this,arguments)},isPermissionGranted:function(){return we.apply(this,arguments)}});function ke(){return navigator.appVersion.includes("Win")}function xe(){return(xe=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.App}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Te(){return(Te=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Audio}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Ge(){return(Ge=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Cache}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Pe(){return(Pe=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Config}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Oe(){return(Oe=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Data}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Ae(){return(Ae=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Desktop}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Le(){return(Le=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Document}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Me(){return(Me=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Download}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Ee(){return(Ee=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Executable}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function De(){return(De=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Font}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Ce(){return(Ce=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Home}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Se(){return(Se=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.LocalData}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function je(){return(je=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Picture}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function We(){return(We=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Public}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Ne(){return(Ne=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Resource}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Ie(){return(Ie=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:t,directory:U.Resource}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function ze(){return(ze=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Runtime}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Fe(){return(Fe=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Template}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Ue(){return(Ue=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Video}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function He(){return(He=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Path",message:{cmd:"resolvePath",path:"",directory:U.Log}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var Ve=ke()?"\\":"/",Be=ke()?";":":";function qe(){return qe=_asyncToGenerator(_regeneratorRuntime().mark((function e(){var t,r,n,a=arguments;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:for(t=a.length,r=new Array(t),n=0;n0&&void 0!==r[0]?r[0]:0,e.abrupt("return",s({__tauriModule:"Process",message:{cmd:"exit",exitCode:t}}));case 2:case"end":return e.stop()}}),e)}))),et.apply(this,arguments)}function tt(){return(tt=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Process",message:{cmd:"relaunch"}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var rt=Object.freeze({__proto__:null,exit:function(){return et.apply(this,arguments)},relaunch:function(){return tt.apply(this,arguments)}});function nt(e,t){return null!=e?e:t()}function at(e,t){return ot.apply(this,arguments)}function ot(){return ot=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){var n,o,i=arguments;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=i.length>2&&void 0!==i[2]?i[2]:[],o=i.length>3?i[3]:void 0,"object"===_typeof(n)&&Object.freeze(n),e.abrupt("return",s({__tauriModule:"Shell",message:{cmd:"execute",program:r,args:n,options:o,onEventFn:a(t)}}));case 4:case"end":return e.stop()}}),e)}))),ot.apply(this,arguments)}var it=function(){function e(){_classCallCheck(this,e),e.prototype.__init.call(this)}return _createClass(e,[{key:"__init",value:function(){this.eventListeners=Object.create(null)}},{key:"addListener",value:function(e,t){return this.on(e,t)}},{key:"removeListener",value:function(e,t){return this.off(e,t)}},{key:"on",value:function(e,t){return e in this.eventListeners?this.eventListeners[e].push(t):this.eventListeners[e]=[t],this}},{key:"once",value:function(e,t){var r=this;return this.addListener(e,(function n(){r.removeListener(e,n),t.apply(void 0,arguments)}))}},{key:"off",value:function(e,t){return e in this.eventListeners&&(this.eventListeners[e]=this.eventListeners[e].filter((function(e){return e!==t}))),this}},{key:"removeAllListeners",value:function(e){return e?delete this.eventListeners[e]:this.eventListeners=Object.create(null),this}},{key:"emit",value:function(e){if(e in this.eventListeners){for(var t=this.eventListeners[e],r=arguments.length,n=new Array(r>1?r-1:0),a=1;a1&&void 0!==arguments[1]?arguments[1]:[],o=arguments.length>2?arguments[2]:void 0;return _classCallCheck(this,a),t=n.call(this),a.prototype.__init2.call(_assertThisInitialized(t)),a.prototype.__init3.call(_assertThisInitialized(t)),t.program=e,t.args="string"==typeof r?[r]:r,t.options=nt(o,(function(){return{}})),t}return _createClass(a,[{key:"__init2",value:function(){this.stdout=new it}},{key:"__init3",value:function(){this.stderr=new it}},{key:"spawn",value:(r=_asyncToGenerator(_regeneratorRuntime().mark((function e(){var t=this;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",at((function(e){switch(e.event){case"Error":t.emit("error",e.payload);break;case"Terminated":t.emit("close",e.payload);break;case"Stdout":t.stdout.emit("data",e.payload);break;case"Stderr":t.stderr.emit("data",e.payload)}}),this.program,this.args,this.options).then((function(e){return new ut(e)})));case 1:case"end":return e.stop()}}),e,this)}))),function(){return r.apply(this,arguments)})},{key:"execute",value:(t=_asyncToGenerator(_regeneratorRuntime().mark((function e(){var t=this;return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",new Promise((function(e,r){t.on("error",r);var n=[],a=[];t.stdout.on("data",(function(e){n.push(e)})),t.stderr.on("data",(function(e){a.push(e)})),t.on("close",(function(t){e({code:t.code,signal:t.signal,stdout:n.join("\n"),stderr:a.join("\n")})})),t.spawn().catch(r)})));case 1:case"end":return e.stop()}}),e)}))),function(){return t.apply(this,arguments)})}],[{key:"sidecar",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[],r=arguments.length>2?arguments[2]:void 0,n=new a(e,t,r);return n.options.sidecar=!0,n}}]),a}(it);function ct(){return ct=_asyncToGenerator(_regeneratorRuntime().mark((function e(t,r){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Shell",message:{cmd:"open",path:t,with:r}}));case 1:case"end":return e.stop()}}),e)}))),ct.apply(this,arguments)}var pt=Object.freeze({__proto__:null,Command:st,Child:ut,EventEmitter:it,open:function(e,t){return ct.apply(this,arguments)}});function lt(e){for(var t=void 0,r=e[0],n=1;n1&&void 0!==arguments[1]?arguments[1]:{};return _classCallCheck(this,r),n=t.call(this,e),yt([a,"optionalAccess",function(e){return e.skip}])||s({__tauriModule:"Window",message:{cmd:"createWebview",data:{options:_objectSpread({label:e},a)}}}).then(_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",n.emit("tauri://created"));case 1:case"end":return e.stop()}}),e)})))).catch(function(){var e=_asyncToGenerator(_regeneratorRuntime().mark((function e(t){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",n.emit("tauri://error",t));case 1:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}()),n}return _createClass(r,null,[{key:"getByLabel",value:function(e){return kt().some((function(t){return t.label===e}))?new r(e,{skip:!0}):null}}]),r}(Pt);function Lt(){return(Lt=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Window",message:{cmd:"manage",data:{cmd:{type:"currentMonitor"}}}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Mt(){return(Mt=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Window",message:{cmd:"manage",data:{cmd:{type:"primaryMonitor"}}}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Et(){return(Et=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Window",message:{cmd:"manage",data:{cmd:{type:"availableMonitors"}}}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}"__TAURI_METADATA__"in window?xt=new At(window.__TAURI_METADATA__.__currentWindow.label,{skip:!0}):(console.warn('Could not find "window.__TAURI_METADATA__". The "appWindow" value will reference the "main" window label.\nNote that this is not an issue if running this frontend on a browser instead of a Tauri window.'),xt=new At("main",{skip:!0}));var Dt=Object.freeze({__proto__:null,WebviewWindow:At,WebviewWindowHandle:Gt,WindowManager:Pt,CloseRequestedEvent:Ot,getCurrent:function(){return new At(window.__TAURI_METADATA__.__currentWindow.label,{skip:!0})},getAll:kt,get appWindow(){return xt},LogicalSize:vt,PhysicalSize:wt,LogicalPosition:bt,PhysicalPosition:Rt,get UserAttentionType(){return gt},currentMonitor:function(){return Lt.apply(this,arguments)},primaryMonitor:function(){return Mt.apply(this,arguments)},availableMonitors:function(){return Et.apply(this,arguments)}}),Ct=ke()?"\r\n":"\n";function St(){return(St=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Os",message:{cmd:"platform"}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function jt(){return(jt=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Os",message:{cmd:"version"}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Wt(){return(Wt=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Os",message:{cmd:"osType"}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function Nt(){return(Nt=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Os",message:{cmd:"arch"}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}function It(){return(It=_asyncToGenerator(_regeneratorRuntime().mark((function e(){return _regeneratorRuntime().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",s({__tauriModule:"Os",message:{cmd:"tempdir"}}));case 1:case"end":return e.stop()}}),e)})))).apply(this,arguments)}var zt=Object.freeze({__proto__:null,EOL:Ct,platform:function(){return St.apply(this,arguments)},version:function(){return jt.apply(this,arguments)},type:function(){return Wt.apply(this,arguments)},arch:function(){return Nt.apply(this,arguments)},tempdir:function(){return It.apply(this,arguments)}}),Ft=o;e.app=h,e.cli=m,e.clipboard=g,e.dialog=P,e.event=H,e.fs=re,e.globalShortcut=ce,e.http=ve,e.invoke=Ft,e.notification=Re,e.os=zt,e.path=Xe,e.process=rt,e.shell=pt,e.tauri=u,e.updater=_t,e.window=Dt,Object.defineProperty(e,"__esModule",{value:!0})})); diff --git a/core/tauri/scripts/core.js b/core/tauri/scripts/core.js index 0ac489544be..6c33e0a0b64 100644 --- a/core/tauri/scripts/core.js +++ b/core/tauri/scripts/core.js @@ -13,6 +13,15 @@ }) } + const osName = __TEMPLATE_os_name__ + + window.__TAURI__.convertFileSrc = function convertFileSrc(filePath, protocol = 'asset') { + const path = encodeURIComponent(filePath) + return osName === 'windows' || osName === 'android' + ? `https://${protocol}.localhost/${path}` + : `${protocol}://localhost/${path}` + } + window.__TAURI__.transformCallback = function transformCallback( callback, once @@ -48,7 +57,7 @@ } } - window.__TAURI_INVOKE__ = function invoke(cmd, args = {}) { + window.__TAURI_INVOKE__ = function invoke(cmd, payload = {}, options) { return new Promise(function (resolve, reject) { var callback = window.__TAURI__.transformCallback(function (r) { resolve(r) @@ -59,19 +68,13 @@ delete window[`_${callback}`] }, true) - if (typeof cmd === 'string') { - args.cmd = cmd - } else if (typeof cmd === 'object') { - args = cmd - } else { - return reject(new Error('Invalid argument type.')) - } - const action = () => { window.__TAURI_IPC__({ - ...args, + cmd, callback, - error: error + error, + payload, + options }) } if (window.__TAURI_IPC__) { @@ -85,196 +88,4 @@ } }) } - - // open links with the Tauri API - function __openLinks() { - document.querySelector('body').addEventListener( - 'click', - function (e) { - var target = e.target - while (target != null) { - if (target.matches('a')) { - if ( - target.href && - (['http://', 'https://', 'mailto:', 'tel:'].some(v => target.href.startsWith(v))) && - target.target === '_blank' - ) { - window.__TAURI_INVOKE__('tauri', { - __tauriModule: 'Shell', - message: { - cmd: 'open', - path: target.href - } - }) - e.preventDefault() - } - break - } - target = target.parentElement - } - } - ) - } - - if ( - document.readyState === 'complete' || - document.readyState === 'interactive' - ) { - __openLinks() - } else { - window.addEventListener( - 'DOMContentLoaded', - function () { - __openLinks() - }, - true - ) - } - - // drag region - document.addEventListener('mousedown', (e) => { - 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', - message: { - cmd: 'manage', - data: { - cmd: { - type: e.detail === 2 ? '__toggleMaximize' : 'startDragging' - } - } - } - }) - } - }) - - let permissionSettable = false - let permissionValue = 'default' - - function isPermissionGranted() { - if (window.Notification.permission !== 'default') { - return Promise.resolve(window.Notification.permission === 'granted') - } - return window.__TAURI_INVOKE__('tauri', { - __tauriModule: 'Notification', - message: { - cmd: 'isNotificationPermissionGranted' - } - }) - } - - function setNotificationPermission(value) { - permissionSettable = true - window.Notification.permission = value - permissionSettable = false - } - - function requestPermission() { - return window - .__TAURI_INVOKE__('tauri', { - __tauriModule: 'Notification', - message: { - cmd: 'requestNotificationPermission' - } - }) - .then(function (permission) { - setNotificationPermission(permission) - return permission - }) - } - - function sendNotification(options) { - if (typeof options === 'object') { - Object.freeze(options) - } - - return window.__TAURI_INVOKE__('tauri', { - __tauriModule: 'Notification', - message: { - cmd: 'notification', - options: - typeof options === 'string' - ? { - title: options - } - : options - } - }) - } - - window.Notification = function (title, options) { - var opts = options || {} - sendNotification( - Object.assign(opts, { - title: title - }) - ) - } - - window.Notification.requestPermission = requestPermission - - Object.defineProperty(window.Notification, 'permission', { - enumerable: true, - get: function () { - return permissionValue - }, - set: function (v) { - if (!permissionSettable) { - throw new Error('Readonly property') - } - permissionValue = v - } - }) - - isPermissionGranted().then(function (response) { - if (response === null) { - setNotificationPermission('default') - } else { - setNotificationPermission(response ? 'granted' : 'denied') - } - }) - - window.alert = function (message) { - window.__TAURI_INVOKE__('tauri', { - __tauriModule: 'Dialog', - message: { - cmd: 'messageDialog', - message: message.toString() - } - }) - } - - window.confirm = function (message) { - return window.__TAURI_INVOKE__('tauri', { - __tauriModule: 'Dialog', - message: { - cmd: 'confirmDialog', - message: message.toString() - } - }) - } - - // window.print works on Linux/Windows; need to use the API on macOS - if (navigator.userAgent.includes('Mac')) { - window.print = function () { - return window.__TAURI_INVOKE__('tauri', { - __tauriModule: 'Window', - message: { - cmd: 'manage', - data: { - cmd: { - type: 'print' - } - } - } - }) - } - } })() diff --git a/core/tauri/scripts/hotkey.js b/core/tauri/scripts/hotkey.js deleted file mode 100644 index 0bf6f6d7364..00000000000 --- a/core/tauri/scripts/hotkey.js +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -/*! hotkeys-js v3.8.7 | MIT (c) 2021 kenny wong | http://jaywcjlove.github.io/hotkeys */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).hotkeys=t()}(this,function(){"use strict";var e="undefined"!=typeof navigator&&0 { + const { cmd, callback, error, payload, options } = message + + // use custom protocol for IPC if the flag is set to true, the command is the fetch data command or when not on Linux/Android + if (useCustomProtocol || cmd === fetchChannelDataCommand || (osName !== 'linux' && osName !== 'android')) { + const { contentType, data } = processIpcMessage(payload) + fetch(window.__TAURI__.convertFileSrc(cmd, 'ipc'), { + method: 'POST', + body: data, + headers: { + 'Content-Type': contentType, + 'Tauri-Callback': callback, + 'Tauri-Error': error, + ...options?.headers + } + }).then((response) => { + const cb = response.ok ? callback : error + // we need to split here because on Android the content-type gets duplicated + switch ((response.headers.get('content-type') || '').split(',')[0]) { + case 'application/json': + return response.json().then((r) => [cb, r]) + case 'text/plain': + return response.text().then((r) => [cb, r]) + default: + return response.arrayBuffer().then((r) => [cb, r]) + } + }).then(([cb, data]) => { + if (window[`_${cb}`]) { + window[`_${cb}`](data) + } else { + console.warn(`[TAURI] Couldn't find callback id {cb} in window. This might happen when the app is reloaded while Rust is running an asynchronous operation.`) + } + }) + } else { + // otherwise use the postMessage interface + const { data } = processIpcMessage({ cmd, callback, error, options, ...payload }) + window.ipc.postMessage(data) + } + } + }) +})() diff --git a/core/tauri/scripts/ipc.js b/core/tauri/scripts/ipc.js index 83ba1121d28..433eba1e5db 100644 --- a/core/tauri/scripts/ipc.js +++ b/core/tauri/scripts/ipc.js @@ -33,11 +33,14 @@ * @return {boolean} - if the event was a valid isolation message */ function isIsolationMessage(event) { - return ( - typeof event.data === 'object' && - 'nonce' in event.data && - 'payload' in event.data - ) + if (typeof event.data === 'object' && typeof event.data.payload === 'object') { + const keys = Object.keys(event.data.payload) + return ( + keys.length > 0 && + keys.every((key) => key === 'nonce' || key === 'payload') + ) + } + return false } /** @@ -47,7 +50,12 @@ * @return {boolean} - if the data is able to transform into an isolation payload */ function isIsolationPayload(data) { - return typeof data === 'object' && 'callback' in data && 'error' in data + return ( + typeof data === 'object' && + 'callback' in data && + 'error' in data && + !isIsolationMessage(data) + ) } /** diff --git a/core/tauri/scripts/process-ipc-message-fn.js b/core/tauri/scripts/process-ipc-message-fn.js new file mode 100644 index 00000000000..2e8dade7bda --- /dev/null +++ b/core/tauri/scripts/process-ipc-message-fn.js @@ -0,0 +1,31 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// this is a function and not an iife so use it carefully + +(function (message) { + if (message instanceof ArrayBuffer || ArrayBuffer.isView(message) || Array.isArray(message)) { + return { + contentType: 'application/octet-stream', + data: message + } + } else { + const data = JSON.stringify(message, (_k, val) => { + if (val instanceof Map) { + let o = {}; + val.forEach((v, k) => o[k] = v); + return o; + } else if (val instanceof Object && '__TAURI_CHANNEL_MARKER__' in val && typeof val.id === 'number') { + return `__CHANNEL__:${val.id}` + } else { + return val; + } + }) + + return { + contentType: 'application/json', + data + } + } +}) diff --git a/core/tauri/scripts/stringify-ipc-message-fn.js b/core/tauri/scripts/stringify-ipc-message-fn.js deleted file mode 100644 index f601b551416..00000000000 --- a/core/tauri/scripts/stringify-ipc-message-fn.js +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -(function (message) { - return JSON.stringify(message, (_k, val) => { - if (val instanceof Map) { - let o = {}; - val.forEach((v, k) => o[k] = v); - return o; - } else { - return val; - } - }) -}) diff --git a/core/tauri/src/api/cli.rs b/core/tauri/src/api/cli.rs deleted file mode 100644 index 4cef96abb4e..00000000000 --- a/core/tauri/src/api/cli.rs +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! Types and functions related to CLI arguments. - -use crate::{ - utils::config::{CliArg, CliConfig}, - PackageInfo, -}; - -use clap::{Arg, ArgMatches, ErrorKind}; -use serde::Serialize; -use serde_json::Value; -use std::collections::HashMap; - -#[macro_use] -mod macros; - -mod clapfix { - //! Compatibility between `clap` 3.0 and 3.1+ without deprecation errors. - #![allow(deprecated)] - - pub type ClapCommand<'help> = clap::App<'help>; - - pub trait ErrorExt { - fn kind(&self) -> clap::ErrorKind; - } - - impl ErrorExt for clap::Error { - fn kind(&self) -> clap::ErrorKind { - self.kind - } - } -} - -use clapfix::{ClapCommand as App, ErrorExt}; - -/// The resolution of a argument match. -#[derive(Default, Debug, Serialize)] -#[non_exhaustive] -pub struct ArgData { - /// - [`Value::Bool`] if it's a flag, - /// - [`Value::Array`] if it's multiple, - /// - [`Value::String`] if it has value, - /// - [`Value::Null`] otherwise. - pub value: Value, - /// The number of occurrences of the argument. - /// e.g. `./app --arg 1 --arg 2 --arg 2 3 4` results in three occurrences. - pub occurrences: u64, -} - -/// The matched subcommand. -#[derive(Default, Debug, Serialize)] -#[non_exhaustive] -pub struct SubcommandMatches { - /// The subcommand name. - pub name: String, - /// The subcommand argument matches. - pub matches: Matches, -} - -/// The argument matches of a command. -#[derive(Default, Debug, Serialize)] -#[non_exhaustive] -pub struct Matches { - /// Data structure mapping each found arg with its resolution. - pub args: HashMap, - /// The matched subcommand if found. - pub subcommand: Option>, -} - -impl Matches { - /// Set a arg match. - pub(crate) fn set_arg(&mut self, name: String, value: ArgData) { - self.args.insert(name, value); - } - - /// Sets the subcommand matches. - pub(crate) fn set_subcommand(&mut self, name: String, matches: Matches) { - self.subcommand = Some(Box::new(SubcommandMatches { name, matches })); - } -} - -/// Gets the argument matches of the CLI definition. -/// -/// This is a low level API. If the application has been built, -/// prefer [`App::get_cli_matches`](`crate::App#method.get_cli_matches`). -/// -/// # Examples -/// -/// ```rust,no_run -/// use tauri::api::cli::get_matches; -/// tauri::Builder::default() -/// .setup(|app| { -/// let matches = get_matches(app.config().tauri.cli.as_ref().unwrap(), app.package_info())?; -/// Ok(()) -/// }); -/// ``` -pub fn get_matches(cli: &CliConfig, package_info: &PackageInfo) -> crate::api::Result { - let about = cli - .description() - .unwrap_or(&package_info.description.to_string()) - .to_string(); - let version = &*package_info.version.to_string(); - let app = get_app(package_info, version, &package_info.name, Some(&about), cli); - match app.try_get_matches() { - Ok(matches) => Ok(get_matches_internal(cli, &matches)), - Err(e) => match ErrorExt::kind(&e) { - ErrorKind::DisplayHelp => { - let mut matches = Matches::default(); - let help_text = e.to_string(); - matches.args.insert( - "help".to_string(), - ArgData { - value: Value::String(help_text), - occurrences: 0, - }, - ); - Ok(matches) - } - ErrorKind::DisplayVersion => { - let mut matches = Matches::default(); - matches - .args - .insert("version".to_string(), Default::default()); - Ok(matches) - } - _ => Err(e.into()), - }, - } -} - -fn get_matches_internal(config: &CliConfig, matches: &ArgMatches) -> Matches { - let mut cli_matches = Matches::default(); - map_matches(config, matches, &mut cli_matches); - - if let Some((subcommand_name, subcommand_matches)) = matches.subcommand() { - if let Some(subcommand_config) = config - .subcommands - .as_ref() - .and_then(|s| s.get(subcommand_name)) - { - cli_matches.set_subcommand( - subcommand_name.to_string(), - get_matches_internal(subcommand_config, subcommand_matches), - ); - } - } - - cli_matches -} - -fn map_matches(config: &CliConfig, matches: &ArgMatches, cli_matches: &mut Matches) { - if let Some(args) = config.args() { - for arg in args { - #[allow(deprecated)] - let occurrences = matches.occurrences_of(arg.name.clone()); - let value = if occurrences == 0 || !arg.takes_value { - Value::Bool(occurrences > 0) - } else if arg.multiple { - #[allow(deprecated)] - matches - .values_of(arg.name.clone()) - .map(|v| { - let mut values = Vec::new(); - for value in v { - values.push(Value::String(value.to_string())); - } - Value::Array(values) - }) - .unwrap_or(Value::Null) - } else { - #[allow(deprecated)] - matches - .value_of(arg.name.clone()) - .map(|v| Value::String(v.to_string())) - .unwrap_or(Value::Null) - }; - - cli_matches.set_arg(arg.name.clone(), ArgData { value, occurrences }); - } - } -} - -fn get_app<'a>( - package_info: &'a PackageInfo, - version: &'a str, - command_name: &'a str, - about: Option<&'a String>, - config: &'a CliConfig, -) -> App<'a> { - let mut app = App::new(command_name) - .author(package_info.authors) - .version(version); - - if let Some(about) = about { - app = app.about(&**about); - } - if let Some(long_description) = config.long_description() { - app = app.long_about(&**long_description); - } - if let Some(before_help) = config.before_help() { - app = app.before_help(&**before_help); - } - if let Some(after_help) = config.after_help() { - app = app.after_help(&**after_help); - } - - if let Some(args) = config.args() { - for arg in args { - let arg_name = arg.name.as_ref(); - app = app.arg(get_arg(arg_name, arg)); - } - } - - if let Some(subcommands) = config.subcommands() { - for (subcommand_name, subcommand) in subcommands { - let clap_subcommand = get_app( - package_info, - version, - subcommand_name, - subcommand.description(), - subcommand, - ); - app = app.subcommand(clap_subcommand); - } - } - - app -} - -fn get_arg<'a>(arg_name: &'a str, arg: &'a CliArg) -> Arg<'a> { - let mut clap_arg = Arg::new(arg_name); - - if arg.index.is_none() { - clap_arg = clap_arg.long(arg_name); - if let Some(short) = arg.short { - clap_arg = clap_arg.short(short); - } - } - - clap_arg = bind_string_arg!(arg, clap_arg, description, help); - clap_arg = bind_string_arg!(arg, clap_arg, long_description, long_help); - clap_arg = clap_arg.takes_value(arg.takes_value); - clap_arg = clap_arg.multiple_values(arg.multiple); - #[allow(deprecated)] - { - clap_arg = clap_arg.multiple_occurrences(arg.multiple_occurrences); - } - clap_arg = bind_value_arg!(arg, clap_arg, number_of_values); - #[allow(deprecated)] - { - clap_arg = bind_string_slice_arg!(arg, clap_arg, possible_values); - } - clap_arg = bind_value_arg!(arg, clap_arg, min_values); - clap_arg = bind_value_arg!(arg, clap_arg, max_values); - clap_arg = clap_arg.required(arg.required); - clap_arg = bind_string_arg!( - arg, - clap_arg, - required_unless_present, - required_unless_present - ); - clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_present_all); - clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_present_any); - clap_arg = bind_string_arg!(arg, clap_arg, conflicts_with, conflicts_with); - if let Some(value) = &arg.conflicts_with_all { - let v: Vec<&str> = value.iter().map(|x| &**x).collect(); - clap_arg = clap_arg.conflicts_with_all(&v); - } - clap_arg = bind_string_arg!(arg, clap_arg, requires, requires); - if let Some(value) = &arg.requires_all { - let v: Vec<&str> = value.iter().map(|x| &**x).collect(); - clap_arg = clap_arg.requires_all(&v); - } - clap_arg = bind_if_arg!(arg, clap_arg, requires_if); - clap_arg = bind_if_arg!(arg, clap_arg, required_if_eq); - clap_arg = bind_value_arg!(arg, clap_arg, require_equals); - clap_arg = bind_value_arg!(arg, clap_arg, index); - - clap_arg -} diff --git a/core/tauri/src/api/cli/macros.rs b/core/tauri/src/api/cli/macros.rs deleted file mode 100644 index 87d82194b20..00000000000 --- a/core/tauri/src/api/cli/macros.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -macro_rules! bind_string_arg { - ($arg:expr, $clap_arg:expr, $arg_name:ident, $clap_field:ident) => {{ - let arg = $arg; - let mut clap_arg = $clap_arg; - if let Some(value) = &arg.$arg_name { - clap_arg = clap_arg.$clap_field(value.as_str()); - } - clap_arg - }}; -} - -macro_rules! bind_value_arg { - ($arg:expr, $clap_arg:expr, $field:ident) => {{ - let arg = $arg; - let mut clap_arg = $clap_arg; - if let Some(value) = arg.$field { - clap_arg = clap_arg.$field(value); - } - clap_arg - }}; -} - -macro_rules! bind_string_slice_arg { - ($arg:expr, $clap_arg:expr, $field:ident) => {{ - let arg = $arg; - let mut clap_arg = $clap_arg; - if let Some(value) = &arg.$field { - let v: Vec<&str> = value.iter().map(|x| &**x).collect(); - clap_arg = clap_arg.$field(v); - } - clap_arg - }}; -} - -macro_rules! bind_if_arg { - ($arg:expr, $clap_arg:expr, $field:ident) => {{ - let arg = $arg; - let mut clap_arg = $clap_arg; - if let Some(value) = &arg.$field { - let v: Vec<&str> = value.iter().map(|x| &**x).collect(); - clap_arg = clap_arg.$field(&v[0], &v[1]); - } - clap_arg - }}; -} diff --git a/core/tauri/src/api/dialog.rs b/core/tauri/src/api/dialog.rs deleted file mode 100644 index 7fc4c1477a7..00000000000 --- a/core/tauri/src/api/dialog.rs +++ /dev/null @@ -1,733 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! Use native message and file open/save dialogs. -//! -//! This module exposes non-blocking APIs on its root, relying on callback closures -//! to give results back. This is particularly useful when running dialogs from the main thread. -//! When using on asynchronous contexts such as async commands, the [`blocking`] APIs are recommended. - -pub use nonblocking::*; - -#[cfg(not(target_os = "linux"))] -macro_rules! run_dialog { - ($e:expr, $h: ident) => {{ - std::thread::spawn(move || { - let response = $e; - $h(response); - }); - }}; -} - -#[cfg(target_os = "linux")] -macro_rules! run_dialog { - ($e:expr, $h: ident) => {{ - std::thread::spawn(move || { - let context = glib::MainContext::default(); - context.invoke_with_priority(glib::PRIORITY_HIGH, move || { - let response = $e; - $h(response); - }); - }); - }}; -} - -#[cfg(not(target_os = "linux"))] -macro_rules! run_file_dialog { - ($e:expr, $h: ident) => {{ - std::thread::spawn(move || { - let response = crate::async_runtime::block_on($e); - $h(response); - }); - }}; -} - -#[cfg(target_os = "linux")] -macro_rules! run_file_dialog { - ($e:expr, $h: ident) => {{ - std::thread::spawn(move || { - let context = glib::MainContext::default(); - context.invoke_with_priority(glib::PRIORITY_HIGH, move || { - let response = $e; - $h(response); - }); - }); - }}; -} - -macro_rules! run_dialog_sync { - ($e:expr) => {{ - let (tx, rx) = sync_channel(0); - let cb = move |response| { - tx.send(response).unwrap(); - }; - run_file_dialog!($e, cb); - rx.recv().unwrap() - }}; -} - -macro_rules! file_dialog_builder { - () => { - #[cfg(target_os = "linux")] - type FileDialog = rfd::FileDialog; - #[cfg(not(target_os = "linux"))] - type FileDialog = rfd::AsyncFileDialog; - - /// The file dialog builder. - /// - /// Constructs file picker dialogs that can select single/multiple files or directories. - #[derive(Debug, Default)] - pub struct FileDialogBuilder(FileDialog); - - impl FileDialogBuilder { - /// Gets the default file dialog builder. - pub fn new() -> Self { - Default::default() - } - - /// Add file extension filter. Takes in the name of the filter, and list of extensions - #[must_use] - pub fn add_filter(mut self, name: impl AsRef, extensions: &[&str]) -> Self { - self.0 = self.0.add_filter(name.as_ref(), extensions); - self - } - - /// Set starting directory of the dialog. - #[must_use] - pub fn set_directory>(mut self, directory: P) -> Self { - self.0 = self.0.set_directory(directory); - self - } - - /// Set starting file name of the dialog. - #[must_use] - pub fn set_file_name(mut self, file_name: &str) -> Self { - self.0 = self.0.set_file_name(file_name); - self - } - - /// Sets the parent window of the dialog. - #[must_use] - pub fn set_parent(mut self, parent: &W) -> Self { - self.0 = self.0.set_parent(parent); - self - } - - /// Set the title of the dialog. - #[must_use] - pub fn set_title(mut self, title: &str) -> Self { - self.0 = self.0.set_title(title); - self - } - } - }; -} - -macro_rules! message_dialog_builder { - () => { - /// A builder for message dialogs. - pub struct MessageDialogBuilder(rfd::MessageDialog); - - impl MessageDialogBuilder { - /// Creates a new message dialog builder. - pub fn new(title: impl AsRef, message: impl AsRef) -> Self { - let title = title.as_ref().to_string(); - let message = message.as_ref().to_string(); - Self( - rfd::MessageDialog::new() - .set_title(&title) - .set_description(&message), - ) - } - - /// Set parent windows explicitly (optional) - /// - /// ## Platform-specific - /// - /// - **Linux:** Unsupported. - pub fn parent(mut self, parent: &W) -> Self { - self.0 = self.0.set_parent(parent); - self - } - - /// Set the set of button that will be displayed on the dialog. - pub fn buttons(mut self, buttons: MessageDialogButtons) -> Self { - self.0 = self.0.set_buttons(buttons.into()); - self - } - - /// Set type of a dialog. - /// - /// Depending on the system it can result in type specific icon to show up, - /// the will inform user it message is a error, warning or just information. - pub fn kind(mut self, kind: MessageDialogKind) -> Self { - self.0 = self.0.set_level(kind.into()); - self - } - } - }; -} - -/// Options for action buttons on message dialogs. -#[non_exhaustive] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum MessageDialogButtons { - /// Ok button. - Ok, - /// Ok and Cancel buttons. - 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 for rfd::MessageButtons { - fn from(kind: MessageDialogButtons) -> Self { - match kind { - 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) - } - } - } -} - -/// Types of message, ask and confirm dialogs. -#[non_exhaustive] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum MessageDialogKind { - /// Information dialog. - Info, - /// Warning dialog. - Warning, - /// Error dialog. - Error, -} - -impl From for rfd::MessageLevel { - fn from(kind: MessageDialogKind) -> Self { - match kind { - MessageDialogKind::Info => Self::Info, - MessageDialogKind::Warning => Self::Warning, - MessageDialogKind::Error => Self::Error, - } - } -} - -/// Blocking interfaces for the dialog APIs. -/// -/// The blocking APIs will block the current thread to execute instead of relying on callback closures, -/// which makes them easier to use. -/// -/// **NOTE:** You cannot block the main thread when executing the dialog APIs, so you must use the [`crate::api::dialog`] methods instead. -/// Examples of main thread context are the [`crate::App::run`] closure and non-async commands. -pub mod blocking { - use super::{MessageDialogButtons, MessageDialogKind}; - use crate::{Runtime, Window}; - use std::path::{Path, PathBuf}; - use std::sync::mpsc::sync_channel; - - file_dialog_builder!(); - message_dialog_builder!(); - - impl FileDialogBuilder { - /// Shows the dialog to select a single file. - /// This is a blocking operation, - /// and should *NOT* be used when running on the main thread context. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::blocking::FileDialogBuilder; - /// #[tauri::command] - /// async fn my_command() { - /// let file_path = FileDialogBuilder::new().pick_file(); - /// // do something with the optional file path here - /// // the file path is `None` if the user closed the dialog - /// } - /// ``` - pub fn pick_file(self) -> Option { - #[allow(clippy::let_and_return)] - let response = run_dialog_sync!(self.0.pick_file()); - #[cfg(not(target_os = "linux"))] - let response = response.map(|p| p.path().to_path_buf()); - response - } - - /// Shows the dialog to select multiple files. - /// This is a blocking operation, - /// and should *NOT* be used when running on the main thread context. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::blocking::FileDialogBuilder; - /// #[tauri::command] - /// async fn my_command() { - /// let file_path = FileDialogBuilder::new().pick_files(); - /// // do something with the optional file paths here - /// // the file paths value is `None` if the user closed the dialog - /// } - /// ``` - pub fn pick_files(self) -> Option> { - #[allow(clippy::let_and_return)] - let response = run_dialog_sync!(self.0.pick_files()); - #[cfg(not(target_os = "linux"))] - let response = - response.map(|paths| paths.into_iter().map(|p| p.path().to_path_buf()).collect()); - response - } - - /// Shows the dialog to select a single folder. - /// This is a blocking operation, - /// and should *NOT* be used when running on the main thread context. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::blocking::FileDialogBuilder; - /// #[tauri::command] - /// async fn my_command() { - /// let folder_path = FileDialogBuilder::new().pick_folder(); - /// // do something with the optional folder path here - /// // the folder path is `None` if the user closed the dialog - /// } - /// ``` - pub fn pick_folder(self) -> Option { - #[allow(clippy::let_and_return)] - let response = run_dialog_sync!(self.0.pick_folder()); - #[cfg(not(target_os = "linux"))] - let response = response.map(|p| p.path().to_path_buf()); - response - } - - /// Shows the dialog to select multiple folders. - /// This is a blocking operation, - /// and should *NOT* be used when running on the main thread context. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::blocking::FileDialogBuilder; - /// #[tauri::command] - /// async fn my_command() { - /// let folder_paths = FileDialogBuilder::new().pick_folders(); - /// // do something with the optional folder paths here - /// // the folder paths value is `None` if the user closed the dialog - /// } - /// ``` - pub fn pick_folders(self) -> Option> { - #[allow(clippy::let_and_return)] - let response = run_dialog_sync!(self.0.pick_folders()); - #[cfg(not(target_os = "linux"))] - let response = - response.map(|paths| paths.into_iter().map(|p| p.path().to_path_buf()).collect()); - response - } - - /// Shows the dialog to save a file. - /// This is a blocking operation, - /// and should *NOT* be used when running on the main thread context. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::blocking::FileDialogBuilder; - /// #[tauri::command] - /// async fn my_command() { - /// let file_path = FileDialogBuilder::new().save_file(); - /// // do something with the optional file path here - /// // the file path is `None` if the user closed the dialog - /// } - /// ``` - pub fn save_file(self) -> Option { - #[allow(clippy::let_and_return)] - let response = run_dialog_sync!(self.0.save_file()); - #[cfg(not(target_os = "linux"))] - let response = response.map(|p| p.path().to_path_buf()); - response - } - } - - impl MessageDialogBuilder { - //// Shows a message dialog. - /// - /// - In `Ok` dialog, it will return `true` when `OK` was pressed. - /// - In `OkCancel` dialog, it will return `true` when `OK` was pressed. - /// - In `YesNo` dialog, it will return `true` when `Yes` was pressed. - pub fn show(self) -> bool { - let (tx, rx) = sync_channel(1); - let f = move |response| { - tx.send(response).unwrap(); - }; - run_dialog!(self.0.show(), f); - rx.recv().unwrap() - } - } - - /// Displays a dialog with a message and an optional title with a "yes" and a "no" button and wait for it to be closed. - /// - /// This is a blocking operation, - /// and should *NOT* be used when running on the main thread context. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::blocking::ask; - /// # let app = tauri::Builder::default().build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")).unwrap(); - /// # let window = tauri::Manager::get_window(&app, "main").unwrap(); - /// let answer = ask(Some(&window), "Tauri", "Is Tauri awesome?"); - /// // do something with `answer` - /// ``` - #[allow(unused_variables)] - pub fn ask( - parent_window: Option<&Window>, - title: impl AsRef, - message: impl AsRef, - ) -> bool { - run_message_dialog(parent_window, title, message, rfd::MessageButtons::YesNo) - } - - /// Displays a dialog with a message and an optional title with an "ok" and a "cancel" button and wait for it to be closed. - /// - /// This is a blocking operation, - /// and should *NOT* be used when running on the main thread context. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::blocking::confirm; - /// # let app = tauri::Builder::default().build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")).unwrap(); - /// # let window = tauri::Manager::get_window(&app, "main").unwrap(); - /// let answer = confirm(Some(&window), "Tauri", "Are you sure?"); - /// // do something with `answer` - /// ``` - #[allow(unused_variables)] - pub fn confirm( - parent_window: Option<&Window>, - title: impl AsRef, - message: impl AsRef, - ) -> bool { - run_message_dialog(parent_window, title, message, rfd::MessageButtons::OkCancel) - } - - /// Displays a message dialog and wait for it to be closed. - /// - /// This is a blocking operation, - /// and should *NOT* be used when running on the main thread context. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::blocking::message; - /// # let app = tauri::Builder::default().build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")).unwrap(); - /// # let window = tauri::Manager::get_window(&app, "main").unwrap(); - /// message(Some(&window), "Tauri", "Tauri is awesome!"); - /// ``` - #[allow(unused_variables)] - pub fn message( - parent_window: Option<&Window>, - title: impl AsRef, - message: impl AsRef, - ) { - let _ = run_message_dialog(parent_window, title, message, rfd::MessageButtons::Ok); - } - - #[allow(unused_variables)] - fn run_message_dialog( - parent_window: Option<&Window>, - title: impl AsRef, - message: impl AsRef, - buttons: rfd::MessageButtons, - ) -> bool { - let (tx, rx) = sync_channel(1); - super::nonblocking::run_message_dialog( - parent_window, - title, - message, - buttons, - MessageDialogKind::Info, - move |response| { - tx.send(response).unwrap(); - }, - ); - rx.recv().unwrap() - } -} - -mod nonblocking { - use super::{MessageDialogButtons, MessageDialogKind}; - use crate::{Runtime, Window}; - use std::path::{Path, PathBuf}; - - file_dialog_builder!(); - message_dialog_builder!(); - - impl FileDialogBuilder { - /// Shows the dialog to select a single file. - /// This is not a blocking operation, - /// and should be used when running on the main thread to avoid deadlocks with the event loop. - /// - /// For usage in other contexts such as commands, prefer [`Self::pick_file`]. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::FileDialogBuilder; - /// tauri::Builder::default() - /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) - /// .expect("failed to build tauri app") - /// .run(|_app, _event| { - /// FileDialogBuilder::new().pick_file(|file_path| { - /// // do something with the optional file path here - /// // the file path is `None` if the user closed the dialog - /// }) - /// }) - /// ``` - pub fn pick_file) + Send + 'static>(self, f: F) { - #[cfg(not(target_os = "linux"))] - let f = |path: Option| f(path.map(|p| p.path().to_path_buf())); - run_file_dialog!(self.0.pick_file(), f) - } - - /// Shows the dialog to select multiple files. - /// This is not a blocking operation, - /// and should be used when running on the main thread to avoid deadlocks with the event loop. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::FileDialogBuilder; - /// tauri::Builder::default() - /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) - /// .expect("failed to build tauri app") - /// .run(|_app, _event| { - /// FileDialogBuilder::new().pick_files(|file_paths| { - /// // do something with the optional file paths here - /// // the file paths value is `None` if the user closed the dialog - /// }) - /// }) - /// ``` - pub fn pick_files>) + Send + 'static>(self, f: F) { - #[cfg(not(target_os = "linux"))] - let f = |paths: Option>| { - f(paths.map(|list| list.into_iter().map(|p| p.path().to_path_buf()).collect())) - }; - run_file_dialog!(self.0.pick_files(), f) - } - - /// Shows the dialog to select a single folder. - /// This is not a blocking operation, - /// and should be used when running on the main thread to avoid deadlocks with the event loop. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::FileDialogBuilder; - /// tauri::Builder::default() - /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) - /// .expect("failed to build tauri app") - /// .run(|_app, _event| { - /// FileDialogBuilder::new().pick_folder(|folder_path| { - /// // do something with the optional folder path here - /// // the folder path is `None` if the user closed the dialog - /// }) - /// }) - /// ``` - pub fn pick_folder) + Send + 'static>(self, f: F) { - #[cfg(not(target_os = "linux"))] - let f = |path: Option| f(path.map(|p| p.path().to_path_buf())); - run_file_dialog!(self.0.pick_folder(), f) - } - - /// Shows the dialog to select multiple folders. - /// This is not a blocking operation, - /// and should be used when running on the main thread to avoid deadlocks with the event loop. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::FileDialogBuilder; - /// tauri::Builder::default() - /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) - /// .expect("failed to build tauri app") - /// .run(|_app, _event| { - /// FileDialogBuilder::new().pick_folders(|file_paths| { - /// // do something with the optional folder paths here - /// // the folder paths value is `None` if the user closed the dialog - /// }) - /// }) - /// ``` - pub fn pick_folders>) + Send + 'static>(self, f: F) { - #[cfg(not(target_os = "linux"))] - let f = |paths: Option>| { - f(paths.map(|list| list.into_iter().map(|p| p.path().to_path_buf()).collect())) - }; - run_file_dialog!(self.0.pick_folders(), f) - } - - /// Shows the dialog to save a file. - /// - /// This is not a blocking operation, - /// and should be used when running on the main thread to avoid deadlocks with the event loop. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::FileDialogBuilder; - /// tauri::Builder::default() - /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) - /// .expect("failed to build tauri app") - /// .run(|_app, _event| { - /// FileDialogBuilder::new().save_file(|file_path| { - /// // do something with the optional file path here - /// // the file path is `None` if the user closed the dialog - /// }) - /// }) - /// ``` - pub fn save_file) + Send + 'static>(self, f: F) { - #[cfg(not(target_os = "linux"))] - let f = |path: Option| f(path.map(|p| p.path().to_path_buf())); - run_file_dialog!(self.0.save_file(), f) - } - } - - impl MessageDialogBuilder { - /// Shows a message dialog: - /// - /// - In `Ok` dialog, it will call the closure with `true` when `OK` was pressed - /// - In `OkCancel` dialog, it will call the closure with `true` when `OK` was pressed - /// - In `YesNo` dialog, it will call the closure with `true` when `Yes` was pressed - pub fn show(self, f: F) { - run_dialog!(self.0.show(), f); - } - } - - /// Displays a non-blocking dialog with a message and an optional title with a "yes" and a "no" button. - /// - /// This is not a blocking operation, - /// and should be used when running on the main thread to avoid deadlocks with the event loop. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::ask; - /// # let app = tauri::Builder::default().build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")).unwrap(); - /// # let window = tauri::Manager::get_window(&app, "main").unwrap(); - /// ask(Some(&window), "Tauri", "Is Tauri awesome?", |answer| { - /// // do something with `answer` - /// }); - /// ``` - #[allow(unused_variables)] - pub fn ask( - parent_window: Option<&Window>, - title: impl AsRef, - message: impl AsRef, - f: F, - ) { - run_message_dialog( - parent_window, - title, - message, - rfd::MessageButtons::YesNo, - MessageDialogKind::Info, - f, - ) - } - - /// Displays a non-blocking dialog with a message and an optional title with an "ok" and a "cancel" button. - /// - /// This is not a blocking operation, - /// and should be used when running on the main thread to avoid deadlocks with the event loop. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::confirm; - /// # let app = tauri::Builder::default().build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")).unwrap(); - /// # let window = tauri::Manager::get_window(&app, "main").unwrap(); - /// confirm(Some(&window), "Tauri", "Are you sure?", |answer| { - /// // do something with `answer` - /// }); - /// ``` - #[allow(unused_variables)] - pub fn confirm( - parent_window: Option<&Window>, - title: impl AsRef, - message: impl AsRef, - f: F, - ) { - run_message_dialog( - parent_window, - title, - message, - rfd::MessageButtons::OkCancel, - MessageDialogKind::Info, - f, - ) - } - - /// Displays a non-blocking message dialog. - /// - /// This is not a blocking operation, - /// and should be used when running on the main thread to avoid deadlocks with the event loop. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::dialog::message; - /// # let app = tauri::Builder::default().build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")).unwrap(); - /// # let window = tauri::Manager::get_window(&app, "main").unwrap(); - /// message(Some(&window), "Tauri", "Tauri is awesome!"); - /// ``` - #[allow(unused_variables)] - pub fn message( - parent_window: Option<&Window>, - title: impl AsRef, - message: impl AsRef, - ) { - run_message_dialog( - parent_window, - title, - message, - rfd::MessageButtons::Ok, - MessageDialogKind::Info, - |_| {}, - ) - } - - #[allow(unused_variables)] - pub(crate) fn run_message_dialog( - parent_window: Option<&Window>, - title: impl AsRef, - message: impl AsRef, - buttons: rfd::MessageButtons, - level: MessageDialogKind, - f: F, - ) { - let title = title.as_ref().to_string(); - let message = message.as_ref().to_string(); - #[allow(unused_mut)] - let mut builder = rfd::MessageDialog::new() - .set_title(&title) - .set_description(&message) - .set_buttons(buttons) - .set_level(level.into()); - - #[cfg(any(windows, target_os = "macos"))] - { - if let Some(window) = parent_window { - builder = builder.set_parent(window); - } - } - - run_dialog!(builder.show(), f) - } -} diff --git a/core/tauri/src/api/dir.rs b/core/tauri/src/api/dir.rs deleted file mode 100644 index 7a23eec70be..00000000000 --- a/core/tauri/src/api/dir.rs +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! Types and functions related to file system directory management. - -use serde::Serialize; -use std::{ - fs::{self, metadata, symlink_metadata}, - path::{Path, PathBuf}, -}; -use tempfile::{self, tempdir}; - -/// A disk entry which is either a file or a directory. -/// -/// This is the result of the [`read_dir`]. The `children` field is always `Some` if the entry is a directory. -#[derive(Debug, Serialize)] -#[non_exhaustive] -pub struct DiskEntry { - /// The path to the entry. - pub path: PathBuf, - /// The name of the entry (file name with extension or directory name). - pub name: Option, - /// The children of this entry if it's a directory. - #[serde(skip_serializing_if = "Option::is_none")] - pub children: Option>, -} - -/// Checks if the given path is a directory. -pub fn is_dir>(path: P) -> crate::api::Result { - metadata(path).map(|md| md.is_dir()).map_err(Into::into) -} - -fn is_symlink>(path: P) -> crate::api::Result { - // TODO: remove the different implementation once we raise tauri's MSRV to at least 1.58 - #[cfg(windows)] - let ret = symlink_metadata(path) - .map(|md| md.is_symlink()) - .map_err(Into::into); - - #[cfg(not(windows))] - let ret = symlink_metadata(path) - .map(|md| md.file_type().is_symlink()) - .map_err(Into::into); - - ret -} - -/// Reads a directory. Can perform recursive operations. -pub fn read_dir>(path: P, recursive: bool) -> crate::api::Result> { - read_dir_with_options(path, recursive, ReadDirOptions { scope: None }) -} - -#[derive(Clone, Copy)] -pub(crate) struct ReadDirOptions<'a> { - pub scope: Option<&'a crate::FsScope>, -} - -pub(crate) fn read_dir_with_options>( - path: P, - recursive: bool, - options: ReadDirOptions<'_>, -) -> crate::api::Result> { - let mut files_and_dirs: Vec = vec![]; - for entry in fs::read_dir(path)? { - let path = entry?.path(); - let path_as_string = path.display().to_string(); - - if let Ok(flag) = is_dir(&path_as_string) { - files_and_dirs.push(DiskEntry { - path: path.clone(), - children: if flag { - Some( - if recursive - && (!is_symlink(&path_as_string)? - || options.scope.map(|s| s.is_allowed(&path)).unwrap_or(true)) - { - read_dir_with_options(&path_as_string, true, options)? - } else { - vec![] - }, - ) - } else { - None - }, - name: path - .file_name() - .map(|name| name.to_string_lossy()) - .map(|name| name.to_string()), - }); - } - } - Result::Ok(files_and_dirs) -} - -/// Runs a closure with a temporary directory argument. -pub fn with_temp_dir(callback: F) -> crate::api::Result<()> { - let dir = tempdir()?; - callback(&dir); - dir.close()?; - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use quickcheck_macros::quickcheck; - use std::{ffi::OsStr, path::PathBuf}; - - // check is dir function by passing in arbitrary strings - #[quickcheck] - fn qc_is_dir(f: String) -> bool { - // if the string runs through is_dir and comes out as an OK result then it must be a DIR. - if is_dir(f.clone()).is_ok() { - PathBuf::from(f).is_dir() - } else { - true - } - } - - fn name_from_path(path: PathBuf) -> Option { - path - .file_name() - .map(|name| name.to_string_lossy()) - .map(|name| name.to_string()) - } - - #[test] - // check the read_dir function with recursive = true - fn check_read_dir_recursively() { - // define a relative directory string test/api/ - let dir = PathBuf::from("test/api/"); - // add the files to this directory - let mut file_one = dir.clone(); - file_one.push("test.txt"); - let mut file_two = dir.clone(); - file_two.push("test_binary"); - - // call walk_dir on the directory - let res = read_dir(dir, true); - - // assert that the result is Ok() - assert!(res.is_ok()); - - // destruct the OK into a vector of DiskEntry Structs - if let Ok(vec) = res { - // assert that the vector length is only 3 - assert_eq!(vec.len(), 2); - - // get the first DiskEntry - let first = &vec[0]; - // get the second DiskEntry - let second = &vec[1]; - - if first.path.extension() == Some(OsStr::new("txt")) { - // check the fields for the first DiskEntry - assert_eq!(first.path, file_one); - assert!(first.children.is_none()); - assert_eq!(first.name, name_from_path(file_one)); - - // check the fields for the third DiskEntry - assert_eq!(second.path, file_two); - assert!(second.children.is_none()); - assert_eq!(second.name, name_from_path(file_two)); - } else { - // check the fields for the second DiskEntry - assert_eq!(first.path, file_two); - assert!(first.children.is_none()); - assert_eq!(first.name, name_from_path(file_two)); - - // check the fields for the third DiskEntry - assert_eq!(second.path, file_one); - assert!(second.children.is_none()); - assert_eq!(second.name, name_from_path(file_one)); - } - } - } - - #[test] - // check the read_dir function with recursive = false - fn check_read_dir() { - // define a relative directory test/api/ - let dir = PathBuf::from("test/api/"); - - // call list_dir_contents on the dir - let res = read_dir(dir, false); - - // assert that the result is Ok() - assert!(res.is_ok()); - - // destruct the vector from the Ok() - if let Ok(vec) = res { - // assert the length of the vector is 2 - assert_eq!(vec.len(), 2); - - // get the two DiskEntry structs in this vector - let first = &vec[0]; - let second = &vec[1]; - - if first.path.extension() == Some(OsStr::new("txt")) { - // check the fields for the first DiskEntry - assert_eq!(first.path, PathBuf::from("test/api/test.txt")); - assert!(first.children.is_none()); - assert_eq!(first.name, Some("test.txt".to_string())); - - // check the fields for the second DiskEntry - assert_eq!(second.path, PathBuf::from("test/api/test_binary")); - assert!(second.children.is_none()); - assert_eq!(second.name, Some("test_binary".to_string())); - } else { - // check the fields for the first DiskEntry - assert_eq!(second.path, PathBuf::from("test/api/test.txt")); - assert!(second.children.is_none()); - assert_eq!(second.name, Some("test.txt".to_string())); - - // check the fields for the second DiskEntry - assert_eq!(first.path, PathBuf::from("test/api/test_binary")); - assert!(first.children.is_none()); - assert_eq!(first.name, Some("test_binary".to_string())); - } - } - } - - #[test] - // test the with_temp_dir function - fn check_test_dir() { - // create a callback closure that takes in a TempDir type and prints it. - let callback = |td: &tempfile::TempDir| { - println!("{td:?}"); - }; - - // execute the with_temp_dir function on the callback - let res = with_temp_dir(callback); - - // assert that the result is an OK type. - assert!(res.is_ok()); - } -} diff --git a/core/tauri/src/api/error.rs b/core/tauri/src/api/error.rs index 5b584b02aeb..fe9f3d1a4bf 100644 --- a/core/tauri/src/api/error.rs +++ b/core/tauri/src/api/error.rs @@ -2,93 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -/// The error types. +/// The result type of Tauri API module. +pub type Result = std::result::Result; + +/// The error type of Tauri API module. #[derive(thiserror::Error, Debug)] #[non_exhaustive] pub enum Error { - /// Command error. - #[error("Command Error: {0}")] - Command(String), - /// The path operation error. - #[error("Path Error: {0}")] - Path(String), - /// The path StripPrefixError error. - #[error("Path Error: {0}")] - PathPrefix(#[from] std::path::StripPrefixError), - /// Error showing the dialog. - #[error("Dialog Error: {0}")] - Dialog(String), - /// The dialog operation was cancelled by the user. - #[error("user cancelled the dialog")] - DialogCancelled, - /// The network error. - #[cfg(feature = "http-api")] - #[error("Network Error: {0}")] - Network(#[from] reqwest::Error), - /// HTTP method error. - #[error(transparent)] - HttpMethod(#[from] http::method::InvalidMethod), - /// Invalid HTTP header value. - #[error(transparent)] - HttpHeaderValue(#[from] http::header::InvalidHeaderValue), - /// Invalid HTTP header value. - #[error(transparent)] - HttpHeader(#[from] http::header::InvalidHeaderName), - /// Failed to serialize header value as string. - #[error(transparent)] - Utf8(#[from] std::string::FromUtf8Error), - /// HTTP form to must be an object. - #[error("http form must be an object")] - InvalidHttpForm, - /// Semver error. - #[error(transparent)] - Semver(#[from] semver::Error), /// JSON error. #[error(transparent)] Json(#[from] serde_json::Error), - /// IO error. - #[error(transparent)] - Io(#[from] std::io::Error), - /// Ignore error. - #[error("failed to walkdir: {0}")] - Ignore(#[from] ignore::Error), - /// ZIP error. - #[cfg(feature = "fs-extract-api")] - #[error(transparent)] - Zip(#[from] zip::result::ZipError), - /// Extract error. - #[cfg(feature = "fs-extract-api")] - #[error("Failed to extract: {0}")] - Extract(String), - /// Notification error. - #[cfg(notification_all)] - #[error(transparent)] - Notification(#[from] notify_rust::error::Error), - /// Url error. - #[error(transparent)] - Url(#[from] url::ParseError), - /// failed to detect the current platform. - #[error("failed to detect platform: {0}")] - FailedToDetectPlatform(String), - /// CLI argument parsing error. - #[cfg(feature = "cli")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "cli")))] - #[error("failed to parse CLI arguments: {0}")] - ParseCliArguments(String), - /// Shell error. - #[error("shell error: {0}")] - Shell(String), - /// Unknown program name. - #[error("unknown program name: {0}")] - UnknownProgramName(String), - /// HTTP error. - #[error(transparent)] - Http(#[from] http::Error), -} - -#[cfg(feature = "cli")] -impl From for Error { - fn from(error: clap::Error) -> Self { - Self::ParseCliArguments(error.to_string()) - } } diff --git a/core/tauri/src/api/file.rs b/core/tauri/src/api/file.rs deleted file mode 100644 index 9e19456fb7d..00000000000 --- a/core/tauri/src/api/file.rs +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! Types and functions related to file operations. - -#[cfg(feature = "fs-extract-api")] -mod extract; -mod file_move; - -use std::{ - fs, - path::{Display, Path}, -}; - -#[cfg(feature = "fs-extract-api")] -pub use extract::*; -pub use file_move::*; - -use serde::{de::Error as DeError, Deserialize, Deserializer}; - -#[derive(Clone, Debug)] -pub(crate) struct SafePathBuf(std::path::PathBuf); - -impl SafePathBuf { - pub fn new(path: std::path::PathBuf) -> Result { - if path - .components() - .any(|x| matches!(x, std::path::Component::ParentDir)) - { - Err("cannot traverse directory, rewrite the path without the use of `../`") - } else { - Ok(Self(path)) - } - } - - #[allow(dead_code)] - pub unsafe fn new_unchecked(path: std::path::PathBuf) -> Self { - Self(path) - } - - #[allow(dead_code)] - pub fn display(&self) -> Display<'_> { - self.0.display() - } -} - -impl AsRef for SafePathBuf { - fn as_ref(&self) -> &Path { - self.0.as_ref() - } -} - -impl<'de> Deserialize<'de> for SafePathBuf { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let path = std::path::PathBuf::deserialize(deserializer)?; - SafePathBuf::new(path).map_err(DeError::custom) - } -} - -/// Reads the entire contents of a file into a string. -pub fn read_string>(file: P) -> crate::api::Result { - fs::read_to_string(file).map_err(Into::into) -} - -/// Reads the entire contents of a file into a bytes vector. -pub fn read_binary>(file: P) -> crate::api::Result> { - fs::read(file).map_err(Into::into) -} - -#[cfg(test)] -mod test { - use super::*; - #[cfg(not(windows))] - use crate::api::Error; - use quickcheck::{Arbitrary, Gen}; - - use std::path::PathBuf; - - impl Arbitrary for super::SafePathBuf { - fn arbitrary(g: &mut Gen) -> Self { - Self(PathBuf::arbitrary(g)) - } - - fn shrink(&self) -> Box> { - Box::new(self.0.shrink().map(SafePathBuf)) - } - } - - #[test] - fn check_read_string() { - let file = String::from("test/api/test.txt"); - - let res = read_string(file); - - assert!(res.is_ok()); - - if let Ok(s) = res { - assert_eq!(s, "This is a test doc!".to_string()); - } - } - - #[test] - fn check_read_string_fail() { - let file = String::from("test/api/"); - - let res = read_string(file); - - assert!(res.is_err()); - - #[cfg(not(windows))] - if let Error::Io(e) = res.unwrap_err() { - #[cfg(not(windows))] - assert_eq!(e.to_string(), "Is a directory (os error 21)".to_string()); - } - } - - #[test] - fn check_read_binary() { - let file = String::from("test/api/test_binary"); - - let expected_vec = vec![ - 71, 73, 70, 56, 57, 97, 1, 0, 1, 0, 128, 0, 0, 255, 255, 255, 0, 0, 0, 33, 249, 4, 1, 0, 0, - 0, 0, 44, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, 68, 1, 0, 59, - ]; - - let res = read_binary(file); - - assert!(res.is_ok()); - - if let Ok(vec) = res { - assert_eq!(vec, expected_vec); - } - } - - #[test] - fn check_read_binary_fail() { - let file = String::from("test/api/"); - - let res = read_binary(file); - - assert!(res.is_err()); - - #[cfg(not(windows))] - if let Error::Io(e) = res.unwrap_err() { - #[cfg(not(windows))] - assert_eq!(e.to_string(), "Is a directory (os error 21)".to_string()); - } - } -} diff --git a/core/tauri/src/api/file/extract.rs b/core/tauri/src/api/file/extract.rs deleted file mode 100644 index 572a994f35d..00000000000 --- a/core/tauri/src/api/file/extract.rs +++ /dev/null @@ -1,328 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use std::{ - borrow::Cow, - fs, - io::{self, Cursor, Read, Seek}, - path::{self, Path, PathBuf}, -}; - -/// The archive reader. -#[derive(Debug)] -pub enum ArchiveReader { - /// A plain reader. - Plain(R), - /// A GZ- compressed reader (decoder). - GzCompressed(Box>), -} - -impl Read for ArchiveReader { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - match self { - Self::Plain(r) => r.read(buf), - Self::GzCompressed(decoder) => decoder.read(buf), - } - } -} - -impl ArchiveReader { - #[allow(dead_code)] - fn get_mut(&mut self) -> &mut R { - match self { - Self::Plain(r) => r, - Self::GzCompressed(decoder) => decoder.get_mut(), - } - } -} - -/// The supported archive formats. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[non_exhaustive] -pub enum ArchiveFormat { - /// Tar archive. - Tar(Option), - /// Zip archive. - Zip, -} - -/// The supported compression types. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[non_exhaustive] -pub enum Compression { - /// Gz compression (e.g. `.tar.gz` archives) - Gz, -} - -/// The zip entry. -pub struct ZipEntry { - path: PathBuf, - is_dir: bool, - file_contents: Vec, -} - -/// A read-only view into an entry of an archive. -#[non_exhaustive] -pub enum Entry<'a, R: Read> { - /// An entry of a tar archive. - #[non_exhaustive] - Tar(Box>), - /// An entry of a zip archive. - #[non_exhaustive] - Zip(ZipEntry), -} - -impl<'a, R: Read> Entry<'a, R> { - /// The entry path. - pub fn path(&self) -> crate::api::Result> { - match self { - Self::Tar(e) => e.path().map_err(Into::into), - Self::Zip(e) => Ok(Cow::Borrowed(&e.path)), - } - } - - /// Extract this entry into `into_path`. - /// If it's a directory, the target will be created, if it's a file, it'll be extracted at this location. - /// Note: You need to include the complete path, with file name and extension. - pub fn extract(self, into_path: &path::Path) -> crate::api::Result<()> { - match self { - Self::Tar(mut entry) => { - // determine if it's a file or a directory - if entry.header().entry_type() == tar::EntryType::Directory { - // this is a directory, lets create it - match fs::create_dir_all(into_path) { - Ok(_) => (), - Err(e) => { - if e.kind() != io::ErrorKind::AlreadyExists { - return Err(e.into()); - } - } - } - } else { - let mut out_file = fs::File::create(into_path)?; - io::copy(&mut entry, &mut out_file)?; - - // make sure we set permissions - if let Ok(mode) = entry.header().mode() { - set_perms(into_path, Some(&mut out_file), mode, true)?; - } - } - } - Self::Zip(entry) => { - if entry.is_dir { - // this is a directory, lets create it - match fs::create_dir_all(into_path) { - Ok(_) => (), - Err(e) => { - if e.kind() != io::ErrorKind::AlreadyExists { - return Err(e.into()); - } - } - } - } else { - let mut out_file = fs::File::create(into_path)?; - io::copy(&mut Cursor::new(entry.file_contents), &mut out_file)?; - } - } - } - - Ok(()) - } -} - -/// The extract manager to retrieve files from archives. -pub struct Extract<'a, R: Read + Seek> { - reader: ArchiveReader, - archive_format: ArchiveFormat, - tar_archive: Option>>, -} - -impl<'a, R: std::fmt::Debug + Read + Seek> std::fmt::Debug for Extract<'a, R> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Extract") - .field("reader", &self.reader) - .field("archive_format", &self.archive_format) - .finish() - } -} - -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.rewind().is_err() { - #[cfg(debug_assertions)] - eprintln!("Could not seek to start of the file"); - } - let compression = if let ArchiveFormat::Tar(compression) = archive_format { - compression - } else { - None - }; - Extract { - reader: match compression { - Some(Compression::Gz) => { - ArchiveReader::GzCompressed(Box::new(flate2::read::GzDecoder::new(reader))) - } - _ => ArchiveReader::Plain(reader), - }, - archive_format, - tar_archive: None, - } - } - - /// Reads the archive content. - pub fn with_files< - E: Into, - F: FnMut(Entry<'_, &mut ArchiveReader>) -> std::result::Result, - >( - &'a mut self, - mut f: F, - ) -> crate::api::Result<()> { - match self.archive_format { - ArchiveFormat::Tar(_) => { - let archive = tar::Archive::new(&mut self.reader); - self.tar_archive.replace(archive); - for entry in self.tar_archive.as_mut().unwrap().entries()? { - let entry = entry?; - if entry.path().is_ok() { - let stop = f(Entry::Tar(Box::new(entry))).map_err(Into::into)?; - if stop { - break; - } - } - } - } - - ArchiveFormat::Zip => { - #[cfg(feature = "fs-extract-api")] - { - let mut archive = zip::ZipArchive::new(self.reader.get_mut())?; - let file_names = archive - .file_names() - .map(|f| f.to_string()) - .collect::>(); - for path in file_names { - let mut zip_file = archive.by_name(&path)?; - let is_dir = zip_file.is_dir(); - let mut file_contents = Vec::new(); - zip_file.read_to_end(&mut file_contents)?; - let stop = f(Entry::Zip(ZipEntry { - path: path.into(), - is_dir, - file_contents, - })) - .map_err(Into::into)?; - if stop { - break; - } - } - } - } - } - - Ok(()) - } - - /// Extract an entire source archive into a specified path. If the source is a single compressed - /// file and not an archive, it will be extracted into a file with the same name inside of - /// `into_dir`. - pub fn extract_into(&mut self, into_dir: &path::Path) -> crate::api::Result<()> { - match self.archive_format { - ArchiveFormat::Tar(_) => { - let mut archive = tar::Archive::new(&mut self.reader); - archive.unpack(into_dir)?; - } - - ArchiveFormat::Zip => { - #[cfg(feature = "fs-extract-api")] - { - let mut archive = zip::ZipArchive::new(self.reader.get_mut())?; - for i in 0..archive.len() { - let mut file = archive.by_index(i)?; - // Decode the file name from raw bytes instead of using file.name() directly. - // file.name() uses String::from_utf8_lossy() which may return messy characters - // such as: τê▒Σ║ñµÿô.app/, that does not work as expected. - // Here we require the file name must be a valid UTF-8. - let file_name = String::from_utf8(file.name_raw().to_vec())?; - let out_path = into_dir.join(file_name); - if file.is_dir() { - fs::create_dir_all(&out_path)?; - } else { - if let Some(out_path_parent) = out_path.parent() { - fs::create_dir_all(out_path_parent)?; - } - let mut out_file = fs::File::create(&out_path)?; - io::copy(&mut file, &mut out_file)?; - } - // Get and Set permissions - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if let Some(mode) = file.unix_mode() { - fs::set_permissions(&out_path, fs::Permissions::from_mode(mode))?; - } - } - } - } - } - } - Ok(()) - } -} - -fn set_perms( - dst: &Path, - f: Option<&mut std::fs::File>, - mode: u32, - preserve: bool, -) -> crate::api::Result<()> { - _set_perms(dst, f, mode, preserve).map_err(|_| { - crate::api::Error::Extract(format!( - "failed to set permissions to {mode:o} \ - for `{}`", - dst.display() - )) - }) -} - -#[cfg(unix)] -fn _set_perms( - dst: &Path, - f: Option<&mut std::fs::File>, - mode: u32, - preserve: bool, -) -> io::Result<()> { - use std::os::unix::prelude::*; - - let mode = if preserve { mode } else { mode & 0o777 }; - let perm = fs::Permissions::from_mode(mode as _); - match f { - Some(f) => f.set_permissions(perm), - None => fs::set_permissions(dst, perm), - } -} - -#[cfg(windows)] -fn _set_perms( - dst: &Path, - f: Option<&mut std::fs::File>, - mode: u32, - _preserve: bool, -) -> io::Result<()> { - if mode & 0o200 == 0o200 { - return Ok(()); - } - match f { - Some(f) => { - let mut perm = f.metadata()?.permissions(); - perm.set_readonly(true); - f.set_permissions(perm) - } - None => { - let mut perm = fs::metadata(dst)?.permissions(); - perm.set_readonly(true); - fs::set_permissions(dst, perm) - } - } -} diff --git a/core/tauri/src/api/file/file_move.rs b/core/tauri/src/api/file/file_move.rs deleted file mode 100644 index c8b7e689177..00000000000 --- a/core/tauri/src/api/file/file_move.rs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use ignore::WalkBuilder; -use std::{fs, path}; - -/// Moves a file from the given path to the specified destination. -/// -/// `source` and `dest` must be on the same filesystem. -/// If `replace_using_temp` is specified, the destination file will be -/// replaced using the given temporary path. -/// -/// * Errors: -/// * Io - copying / renaming -#[derive(Debug)] -pub struct Move<'a> { - source: &'a path::Path, - temp: Option<&'a path::Path>, -} -impl<'a> Move<'a> { - /// Specify source file - pub fn from_source(source: &'a path::Path) -> Move<'a> { - Self { source, temp: None } - } - - /// If specified and the destination file already exists, the "destination" - /// file will be moved to the given temporary location before the "source" - /// file is moved to the "destination" file. - /// - /// In the event of an `io` error while renaming "source" to "destination", - /// the temporary file will be moved back to "destination". - /// - /// The `temp` dir must be explicitly provided since `rename` operations require - /// files to live on the same filesystem. - pub fn replace_using_temp(&mut self, temp: &'a path::Path) -> &mut Self { - self.temp = Some(temp); - self - } - - /// Move source file to specified destination (replace whole directory) - pub fn to_dest(&self, dest: &path::Path) -> crate::api::Result<()> { - match self.temp { - None => { - fs::rename(self.source, dest)?; - } - Some(temp) => { - if dest.exists() { - fs::rename(dest, temp)?; - if let Err(e) = fs::rename(self.source, dest) { - fs::rename(temp, dest)?; - return Err(e.into()); - } - } else { - fs::rename(self.source, dest)?; - } - } - }; - Ok(()) - } - - /// Walk in the source and copy all files and create directories if needed by - /// replacing existing elements. (equivalent to a cp -R) - pub fn walk_to_dest(&self, dest: &path::Path) -> crate::api::Result<()> { - match self.temp { - None => { - // got no temp -- no need to backup - walkdir_and_copy(self.source, dest)?; - } - Some(temp) => { - if dest.exists() { - // we got temp and our dest exist, lets make a backup - // of current files - walkdir_and_copy(dest, temp)?; - - if let Err(e) = walkdir_and_copy(self.source, dest) { - // if we got something wrong we reset the dest with our backup - fs::rename(temp, dest)?; - return Err(e); - } - } else { - // got temp but dest didnt exist - walkdir_and_copy(self.source, dest)?; - } - } - }; - Ok(()) - } -} -// Walk into the source and create directories, and copy files -// Overwriting existing items but keeping untouched the files in the dest -// not provided in the source. -fn walkdir_and_copy(source: &path::Path, dest: &path::Path) -> crate::api::Result<()> { - let walkdir = WalkBuilder::new(source).hidden(false).build(); - - for entry in walkdir { - // Check if it's a file - - let element = entry?; - let metadata = element.metadata()?; - let destination = dest.join(element.path().strip_prefix(source)?); - - // we make sure it's a directory and destination doesnt exist - if metadata.is_dir() && !&destination.exists() { - fs::create_dir_all(&destination)?; - } - - // we make sure it's a file - if metadata.is_file() { - fs::copy(element.path(), destination)?; - } - } - Ok(()) -} diff --git a/core/tauri/src/api/http.rs b/core/tauri/src/api/http.rs deleted file mode 100644 index 3f7b9d45f27..00000000000 --- a/core/tauri/src/api/http.rs +++ /dev/null @@ -1,542 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! Types and functions related to HTTP request. - -use http::Method; -pub use http::StatusCode; -use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::Value; -use serde_repr::{Deserialize_repr, Serialize_repr}; -use url::Url; - -use std::{collections::HashMap, path::PathBuf, time::Duration}; - -pub use reqwest::header; - -use header::{HeaderName, HeaderValue}; - -#[derive(Deserialize)] -#[serde(untagged)] -enum SerdeDuration { - Seconds(u64), - Duration(Duration), -} - -fn deserialize_duration<'de, D: Deserializer<'de>>( - deserializer: D, -) -> Result, D::Error> { - if let Some(duration) = Option::::deserialize(deserializer)? { - Ok(Some(match duration { - SerdeDuration::Seconds(s) => Duration::from_secs(s), - SerdeDuration::Duration(d) => d, - })) - } else { - Ok(None) - } -} - -/// The builder of [`Client`]. -#[derive(Debug, Clone, Default, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ClientBuilder { - /// Max number of redirections to follow. - pub max_redirections: Option, - /// Connect timeout for the request. - #[serde(deserialize_with = "deserialize_duration", default)] - pub connect_timeout: Option, -} - -impl ClientBuilder { - /// Creates a new client builder with the default options. - pub fn new() -> Self { - Default::default() - } - - /// Sets the maximum number of redirections. - #[must_use] - pub fn max_redirections(mut self, max_redirections: usize) -> Self { - self.max_redirections = Some(max_redirections); - self - } - - /// Sets the connection timeout. - #[must_use] - pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self { - self.connect_timeout.replace(connect_timeout); - self - } - - /// Builds the Client. - pub fn build(self) -> crate::api::Result { - let mut client_builder = reqwest::Client::builder(); - - if let Some(max_redirections) = self.max_redirections { - client_builder = client_builder.redirect(if max_redirections == 0 { - reqwest::redirect::Policy::none() - } else { - reqwest::redirect::Policy::limited(max_redirections) - }); - } - - if let Some(connect_timeout) = self.connect_timeout { - client_builder = client_builder.connect_timeout(connect_timeout); - } - - let client = client_builder.build()?; - Ok(Client(client)) - } -} - -/// The HTTP client based on [`reqwest`]. -#[derive(Debug, Clone)] -pub struct Client(reqwest::Client); - -impl Client { - /// Executes an HTTP request - /// - /// # Examples - pub async fn send(&self, mut request: HttpRequestBuilder) -> crate::api::Result { - let method = Method::from_bytes(request.method.to_uppercase().as_bytes())?; - - let mut request_builder = self.0.request(method, request.url.as_str()); - - if let Some(query) = request.query { - request_builder = request_builder.query(&query); - } - - if let Some(timeout) = request.timeout { - request_builder = request_builder.timeout(timeout); - } - - if let Some(body) = request.body { - request_builder = match body { - Body::Bytes(data) => request_builder.body(bytes::Bytes::from(data)), - Body::Text(text) => request_builder.body(bytes::Bytes::from(text)), - Body::Json(json) => request_builder.json(&json), - Body::Form(form_body) => { - #[allow(unused_variables)] - fn send_form( - request_builder: reqwest::RequestBuilder, - headers: &mut Option, - form_body: FormBody, - ) -> crate::api::Result { - #[cfg(feature = "http-multipart")] - if matches!( - headers - .as_ref() - .and_then(|h| h.0.get("content-type")) - .map(|v| v.as_bytes()), - Some(b"multipart/form-data") - ) { - // the Content-Type header will be set by reqwest in the `.multipart` call - headers.as_mut().map(|h| h.0.remove("content-type")); - let mut multipart = reqwest::multipart::Form::new(); - - for (name, part) in form_body.0 { - let part = match part { - FormPart::File { - file, - mime, - file_name, - } => { - let bytes: Vec = file.try_into()?; - let mut part = reqwest::multipart::Part::bytes(bytes); - if let Some(mime) = mime { - part = part.mime_str(&mime)?; - } - if let Some(file_name) = file_name { - part = part.file_name(file_name); - } - part - } - FormPart::Text(value) => reqwest::multipart::Part::text(value), - }; - - multipart = multipart.part(name, part); - } - - return Ok(request_builder.multipart(multipart)); - } - - let mut form = Vec::new(); - for (name, part) in form_body.0 { - match part { - FormPart::File { file, .. } => { - let bytes: Vec = file.try_into()?; - form.push((name, serde_json::to_string(&bytes)?)) - } - FormPart::Text(value) => form.push((name, value)), - } - } - Ok(request_builder.form(&form)) - } - send_form(request_builder, &mut request.headers, form_body)? - } - }; - } - - if let Some(headers) = request.headers { - request_builder = request_builder.headers(headers.0); - } - - let http_request = request_builder.build()?; - - let response = self.0.execute(http_request).await?; - - Ok(Response( - request.response_type.unwrap_or(ResponseType::Json), - response, - )) - } -} - -#[derive(Serialize_repr, Deserialize_repr, Clone, Debug)] -#[repr(u16)] -#[non_exhaustive] -/// The HTTP response type. -pub enum ResponseType { - /// Read the response as JSON - Json = 1, - /// Read the response as text - Text, - /// Read the response as binary - Binary, -} - -/// A file path or contents. -#[derive(Debug, Clone, Deserialize)] -#[serde(untagged)] -#[non_exhaustive] -pub enum FilePart { - /// File path. - Path(PathBuf), - /// File contents. - Contents(Vec), -} - -impl TryFrom for Vec { - type Error = crate::api::Error; - fn try_from(file: FilePart) -> crate::api::Result { - let bytes = match file { - FilePart::Path(path) => std::fs::read(path)?, - FilePart::Contents(bytes) => bytes, - }; - Ok(bytes) - } -} - -/// [`FormBody`] data types. -#[derive(Debug, Deserialize)] -#[serde(untagged)] -#[non_exhaustive] -pub enum FormPart { - /// A string value. - Text(String), - /// A file value. - #[serde(rename_all = "camelCase")] - File { - /// File path or content. - file: FilePart, - /// Mime type of this part. - /// Only used when the `Content-Type` header is set to `multipart/form-data`. - mime: Option, - /// File name. - /// Only used when the `Content-Type` header is set to `multipart/form-data`. - file_name: Option, - }, -} - -/// Form body definition. -#[derive(Debug, Deserialize)] -pub struct FormBody(pub(crate) HashMap); - -impl FormBody { - /// Creates a new form body. - pub fn new(data: HashMap) -> Self { - Self(data) - } -} - -/// A body for the request. -#[derive(Debug, Deserialize)] -#[serde(tag = "type", content = "payload")] -#[non_exhaustive] -pub enum Body { - /// A form body. - Form(FormBody), - /// A JSON body. - Json(Value), - /// A text string body. - Text(String), - /// A byte array body. - Bytes(Vec), -} - -/// A set of HTTP headers. -#[derive(Debug, Default)] -pub struct HeaderMap(header::HeaderMap); - -impl<'de> Deserialize<'de> for HeaderMap { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let map = HashMap::::deserialize(deserializer)?; - let mut headers = header::HeaderMap::default(); - for (key, value) in map { - if let (Ok(key), Ok(value)) = ( - header::HeaderName::from_bytes(key.as_bytes()), - header::HeaderValue::from_str(&value), - ) { - headers.insert(key, value); - } else { - return Err(serde::de::Error::custom(format!( - "invalid header `{key}` `{value}`" - ))); - } - } - Ok(Self(headers)) - } -} - -/// The builder for a HTTP request. -/// -/// # Examples -/// ```rust,no_run -/// use tauri::api::http::{HttpRequestBuilder, ResponseType, ClientBuilder}; -/// async fn run() { -/// let client = ClientBuilder::new() -/// .max_redirections(3) -/// .build() -/// .unwrap(); -/// let request = HttpRequestBuilder::new("GET", "http://example.com").unwrap() -/// .response_type(ResponseType::Text); -/// if let Ok(response) = client.send(request).await { -/// println!("got response"); -/// } else { -/// println!("Something Happened!"); -/// } -/// } -/// ``` -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct HttpRequestBuilder { - /// The request method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT or TRACE) - pub method: String, - /// The request URL - pub url: Url, - /// The request query params - pub query: Option>, - /// The request headers - pub headers: Option, - /// The request body - pub body: Option, - /// Timeout for the whole request - #[serde(deserialize_with = "deserialize_duration", default)] - pub timeout: Option, - /// The response type (defaults to Json) - pub response_type: Option, -} - -impl HttpRequestBuilder { - /// Initializes a new instance of the HttpRequestrequest_builder. - pub fn new(method: impl Into, url: impl AsRef) -> crate::api::Result { - Ok(Self { - method: method.into(), - url: Url::parse(url.as_ref())?, - query: None, - headers: None, - body: None, - timeout: None, - response_type: None, - }) - } - - /// Sets the request parameters. - #[must_use] - pub fn query(mut self, query: HashMap) -> Self { - self.query = Some(query); - self - } - - /// Adds a header. - pub fn header(mut self, key: K, value: V) -> crate::api::Result - where - HeaderName: TryFrom, - >::Error: Into, - HeaderValue: TryFrom, - >::Error: Into, - { - let key: Result = key.try_into().map_err(Into::into); - let value: Result = value.try_into().map_err(Into::into); - self - .headers - .get_or_insert_with(Default::default) - .0 - .insert(key?, value?); - Ok(self) - } - - /// Sets the request headers. - #[must_use] - pub fn headers(mut self, headers: header::HeaderMap) -> Self { - self.headers.replace(HeaderMap(headers)); - self - } - - /// Sets the request body. - #[must_use] - pub fn body(mut self, body: Body) -> Self { - self.body = Some(body); - self - } - - /// Sets the general request timeout. - #[must_use] - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout.replace(timeout); - self - } - - /// Sets the type of the response. Interferes with the way we read the response. - #[must_use] - pub fn response_type(mut self, response_type: ResponseType) -> Self { - self.response_type = Some(response_type); - self - } -} - -/// The HTTP response. -#[derive(Debug)] -pub struct Response(ResponseType, reqwest::Response); - -impl Response { - /// Get the [`StatusCode`] of this Response. - pub fn status(&self) -> StatusCode { - self.1.status() - } - - /// Get the headers of this Response. - pub fn headers(&self) -> &header::HeaderMap { - self.1.headers() - } - - /// Reads the response as raw bytes. - pub async fn bytes(self) -> crate::api::Result { - let status = self.status().as_u16(); - let data = self.1.bytes().await?.to_vec(); - Ok(RawResponse { status, data }) - } - - // Convert the response into a Stream of [`bytes::Bytes`] from the body. - // - // # Examples - // - // ```no_run - // use futures_util::StreamExt; - // - // # async fn run() -> Result<(), Box> { - // let client = tauri::api::http::ClientBuilder::new().build()?; - // let mut stream = client.send(tauri::api::http::HttpRequestBuilder::new("GET", "http://httpbin.org/ip")?) - // .await? - // .bytes_stream(); - // - // while let Some(item) = stream.next().await { - // println!("Chunk: {:?}", item?); - // } - // # Ok(()) - // # } - // ``` - #[allow(dead_code)] - pub(crate) fn bytes_stream( - self, - ) -> impl futures_util::Stream> { - use futures_util::StreamExt; - self.1.bytes_stream().map(|res| res.map_err(Into::into)) - } - - /// Reads the response. - /// - /// Note that the body is serialized to a [`Value`]. - pub async fn read(self) -> crate::api::Result { - let url = self.1.url().clone(); - - let mut headers = HashMap::new(); - let mut raw_headers = HashMap::new(); - for (name, value) in self.1.headers() { - headers.insert( - name.as_str().to_string(), - String::from_utf8(value.as_bytes().to_vec())?, - ); - raw_headers.insert( - name.as_str().to_string(), - self - .1 - .headers() - .get_all(name) - .into_iter() - .map(|v| String::from_utf8(v.as_bytes().to_vec()).map_err(Into::into)) - .collect::>>()?, - ); - } - let status = self.1.status().as_u16(); - - let data = match self.0 { - ResponseType::Json => self.1.json().await?, - ResponseType::Text => Value::String(self.1.text().await?), - ResponseType::Binary => serde_json::to_value(&self.1.bytes().await?)?, - }; - - Ok(ResponseData { - url, - status, - headers, - raw_headers, - data, - }) - } -} - -/// A response with raw bytes. -#[non_exhaustive] -#[derive(Debug)] -pub struct RawResponse { - /// Response status code. - pub status: u16, - /// Response bytes. - pub data: Vec, -} - -/// The response data. -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -#[non_exhaustive] -pub struct ResponseData { - /// Response URL. Useful if it followed redirects. - pub url: Url, - /// Response status code. - pub status: u16, - /// Response headers. - pub headers: HashMap, - /// Response raw headers. - pub raw_headers: HashMap>, - /// Response data. - pub data: Value, -} - -#[cfg(test)] -mod test { - use super::ClientBuilder; - use quickcheck::{Arbitrary, Gen}; - - impl Arbitrary for ClientBuilder { - fn arbitrary(g: &mut Gen) -> Self { - Self { - max_redirections: Option::arbitrary(g), - connect_timeout: Option::arbitrary(g), - } - } - } -} diff --git a/core/tauri/src/api/mod.rs b/core/tauri/src/api/mod.rs index cd296db7dad..10742b7baeb 100644 --- a/core/tauri/src/api/mod.rs +++ b/core/tauri/src/api/mod.rs @@ -4,44 +4,9 @@ //! The Tauri API interface. -#[cfg(all(desktop, feature = "dialog"))] -#[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "dialog"))))] -pub mod dialog; -pub mod dir; -pub mod file; -#[cfg(feature = "http-api")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "http-api")))] -pub mod http; -pub mod ipc; -#[cfg(feature = "os-api")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "os-api")))] -pub mod os; -pub mod path; -pub mod process; -#[cfg(feature = "shell-open-api")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "shell-open-api")))] -pub mod shell; -pub mod version; - -#[cfg(feature = "cli")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "cli")))] -pub mod cli; - -#[cfg(feature = "cli")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "cli")))] -pub use clap; - -#[cfg(all(desktop, feature = "notification"))] -#[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "notification"))))] -pub mod notification; - mod error; -/// The error type of Tauri API module. -pub use error::Error; -/// The result type of Tauri API module. -pub type Result = std::result::Result; - +pub use error::{Error, Result}; // Not public API #[doc(hidden)] pub mod private { diff --git a/core/tauri/src/api/notification.rs b/core/tauri/src/api/notification.rs deleted file mode 100644 index 8110f52ddf6..00000000000 --- a/core/tauri/src/api/notification.rs +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! Types and functions related to desktop notifications. - -#[cfg(windows)] -use std::path::MAIN_SEPARATOR as SEP; - -/// The desktop notification definition. -/// -/// Allows you to construct a Notification data and send it. -/// -/// # Examples -/// ```rust,no_run -/// use tauri::api::notification::Notification; -/// // first we build the application to access the Tauri configuration -/// let app = tauri::Builder::default() -/// // on an actual app, remove the string argument -/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) -/// .expect("error while building tauri application"); -/// -/// // shows a notification with the given title and body -/// Notification::new(&app.config().tauri.bundle.identifier) -/// .title("New message") -/// .body("You've got a new message.") -/// .show(); -/// -/// // run the app -/// app.run(|_app_handle, _event| {}); -/// ``` -#[allow(dead_code)] -#[derive(Debug, Default)] -pub struct Notification { - /// The notification body. - body: Option, - /// The notification title. - title: Option, - /// The notification icon. - icon: Option, - /// The notification identifier - identifier: String, -} - -impl Notification { - /// Initializes a instance of a Notification. - pub fn new(identifier: impl Into) -> Self { - Self { - identifier: identifier.into(), - ..Default::default() - } - } - - /// Sets the notification body. - #[must_use] - pub fn body(mut self, body: impl Into) -> Self { - self.body = Some(body.into()); - self - } - - /// Sets the notification title. - #[must_use] - pub fn title(mut self, title: impl Into) -> Self { - self.title = Some(title.into()); - self - } - - /// Sets the notification icon. - #[must_use] - pub fn icon(mut self, icon: impl Into) -> Self { - self.icon = Some(icon.into()); - self - } - - /// Shows the notification. - /// - /// # Examples - /// - /// ```no_run - /// use tauri::api::notification::Notification; - /// - /// // on an actual app, remove the string argument - /// let context = tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"); - /// Notification::new(&context.config().tauri.bundle.identifier) - /// .title("Tauri") - /// .body("Tauri is awesome!") - /// .show() - /// .unwrap(); - /// ``` - /// - /// ## Platform-specific - /// - /// - **Windows**: Not supported on Windows 7. If your app targets it, enable the `windows7-compat` feature and use [`Self::notify`]. - #[cfg_attr( - all(not(doc_cfg), feature = "windows7-compat"), - deprecated = "This function does not work on Windows 7. Use `Self::notify` instead." - )] - pub fn show(self) -> crate::api::Result<()> { - let mut notification = notify_rust::Notification::new(); - if let Some(body) = self.body { - notification.body(&body); - } - if let Some(title) = self.title { - notification.summary(&title); - } - if let Some(icon) = self.icon { - notification.icon(&icon); - } else { - notification.auto_icon(); - } - #[cfg(windows)] - { - let exe = tauri_utils::platform::current_exe()?; - let exe_dir = exe.parent().expect("failed to get exe directory"); - let curr_dir = exe_dir.display().to_string(); - // set the notification's System.AppUserModel.ID only when running the installed app - if !(curr_dir.ends_with(format!("{SEP}target{SEP}debug").as_str()) - || curr_dir.ends_with(format!("{SEP}target{SEP}release").as_str())) - { - notification.app_id(&self.identifier); - } - } - #[cfg(target_os = "macos")] - { - let _ = notify_rust::set_application(if cfg!(feature = "custom-protocol") { - &self.identifier - } else { - "com.apple.Terminal" - }); - } - - crate::async_runtime::spawn(async move { - let _ = notification.show(); - }); - - Ok(()) - } - - /// Shows the notification. This API is similar to [`Self::show`], but it also works on Windows 7. - /// - /// # Examples - /// - /// ```no_run - /// use tauri::api::notification::Notification; - /// - /// // on an actual app, remove the string argument - /// let context = tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"); - /// let identifier = context.config().tauri.bundle.identifier.clone(); - /// - /// tauri::Builder::default() - /// .setup(move |app| { - /// Notification::new(&identifier) - /// .title("Tauri") - /// .body("Tauri is awesome!") - /// .notify(&app.handle()) - /// .unwrap(); - /// Ok(()) - /// }) - /// .run(context) - /// .expect("error while running tauri application"); - /// ``` - #[cfg(feature = "windows7-compat")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "windows7-compat")))] - #[allow(unused_variables)] - pub fn notify(self, app: &crate::AppHandle) -> crate::api::Result<()> { - #[cfg(windows)] - { - if crate::utils::platform::is_windows_7() { - self.notify_win7(app) - } else { - #[allow(deprecated)] - self.show() - } - } - #[cfg(not(windows))] - { - #[allow(deprecated)] - self.show() - } - } - - #[cfg(all(windows, feature = "windows7-compat"))] - fn notify_win7(self, app: &crate::AppHandle) -> crate::api::Result<()> { - let app = app.clone(); - let default_window_icon = app.manager.inner.default_window_icon.clone(); - let _ = app.run_on_main_thread(move || { - let mut notification = win7_notifications::Notification::new(); - if let Some(body) = self.body { - notification.body(&body); - } - if let Some(title) = self.title { - notification.summary(&title); - } - if let Some(crate::Icon::Rgba { - rgba, - width, - height, - }) = default_window_icon - { - notification.icon(rgba, width, height); - } - let _ = notification.show(); - }); - - Ok(()) - } -} diff --git a/core/tauri/src/api/path.rs b/core/tauri/src/api/path.rs deleted file mode 100644 index 7edd76c3e04..00000000000 --- a/core/tauri/src/api/path.rs +++ /dev/null @@ -1,604 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! Types and functions related to file system path operations. - -use std::{ - env::temp_dir, - path::{Component, Path, PathBuf}, -}; - -use crate::{Config, Env, PackageInfo}; - -use serde_repr::{Deserialize_repr, Serialize_repr}; - -// we have to wrap the BaseDirectory enum in a module for #[allow(deprecated)] -// to work, because the procedural macros on the enum prevent it from working directly -// TODO: remove this workaround in v2 along with deprecated variants -#[allow(deprecated)] -mod base_directory { - use super::*; - - /// A base directory to be used in [`resolve_path`]. - /// - /// The base directory is the optional root of a file system operation. - /// If informed by the API call, all paths will be relative to the path of the given directory. - /// - /// For more information, check the [`dirs_next` documentation](https://docs.rs/dirs_next/). - #[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)] - #[repr(u16)] - #[non_exhaustive] - pub enum BaseDirectory { - /// The Audio directory. - Audio = 1, - /// The Cache directory. - Cache, - /// The Config directory. - Config, - /// The Data directory. - Data, - /// The LocalData directory. - LocalData, - /// The Desktop directory. - Desktop, - /// The Document directory. - Document, - /// The Download directory. - Download, - /// The Executable directory. - Executable, - /// The Font directory. - Font, - /// The Home directory. - Home, - /// The Picture directory. - Picture, - /// The Public directory. - Public, - /// The Runtime directory. - Runtime, - /// The Template directory. - Template, - /// The Video directory. - Video, - /// The Resource directory. - Resource, - /// The default app config directory. - /// Resolves to [`BaseDirectory::Config`]`/{bundle_identifier}`. - #[deprecated( - since = "1.2.0", - note = "Will be removed in 2.0.0. Use `BaseDirectory::AppConfig` or BaseDirectory::AppData` instead." - )] - App, - /// The default app log directory. - /// Resolves to [`BaseDirectory::Home`]`/Library/Logs/{bundle_identifier}` on macOS - /// and [`BaseDirectory::Config`]`/{bundle_identifier}/logs` on linux and Windows. - #[deprecated( - since = "1.2.0", - note = "Will be removed in 2.0.0. Use `BaseDirectory::AppLog` instead." - )] - Log, - /// A temporary directory. - /// Resolves to [`temp_dir`]. - Temp, - /// The default app config directory. - /// Resolves to [`BaseDirectory::Config`]`/{bundle_identifier}`. - AppConfig, - /// The default app data directory. - /// Resolves to [`BaseDirectory::Data`]`/{bundle_identifier}`. - AppData, - /// The default app local data directory. - /// Resolves to [`BaseDirectory::LocalData`]`/{bundle_identifier}`. - AppLocalData, - /// The default app cache directory. - /// Resolves to [`BaseDirectory::Cache`]`/{bundle_identifier}`. - AppCache, - /// The default app log directory. - /// Resolves to [`BaseDirectory::Home`]`/Library/Logs/{bundle_identifier}` on macOS - /// and [`BaseDirectory::Config`]`/{bundle_identifier}/logs` on linux and Windows. - AppLog, - } -} -pub use base_directory::BaseDirectory; - -impl BaseDirectory { - /// Gets the variable that represents this [`BaseDirectory`] for string paths. - pub fn variable(self) -> &'static str { - match self { - Self::Audio => "$AUDIO", - Self::Cache => "$CACHE", - Self::Config => "$CONFIG", - Self::Data => "$DATA", - Self::LocalData => "$LOCALDATA", - Self::Desktop => "$DESKTOP", - Self::Document => "$DOCUMENT", - Self::Download => "$DOWNLOAD", - Self::Executable => "$EXE", - Self::Font => "$FONT", - Self::Home => "$HOME", - Self::Picture => "$PICTURE", - Self::Public => "$PUBLIC", - Self::Runtime => "$RUNTIME", - Self::Template => "$TEMPLATE", - Self::Video => "$VIDEO", - Self::Resource => "$RESOURCE", - #[allow(deprecated)] - Self::App => "$APP", - #[allow(deprecated)] - Self::Log => "$LOG", - Self::Temp => "$TEMP", - Self::AppConfig => "$APPCONFIG", - Self::AppData => "$APPDATA", - Self::AppLocalData => "$APPLOCALDATA", - Self::AppCache => "$APPCACHE", - Self::AppLog => "$APPLOG", - } - } - - /// Gets the [`BaseDirectory`] associated with the given variable, or [`None`] if the variable doesn't match any. - pub fn from_variable(variable: &str) -> Option { - let res = match variable { - "$AUDIO" => Self::Audio, - "$CACHE" => Self::Cache, - "$CONFIG" => Self::Config, - "$DATA" => Self::Data, - "$LOCALDATA" => Self::LocalData, - "$DESKTOP" => Self::Desktop, - "$DOCUMENT" => Self::Document, - "$DOWNLOAD" => Self::Download, - "$EXE" => Self::Executable, - "$FONT" => Self::Font, - "$HOME" => Self::Home, - "$PICTURE" => Self::Picture, - "$PUBLIC" => Self::Public, - "$RUNTIME" => Self::Runtime, - "$TEMPLATE" => Self::Template, - "$VIDEO" => Self::Video, - "$RESOURCE" => Self::Resource, - #[allow(deprecated)] - "$APP" => Self::App, - #[allow(deprecated)] - "$LOG" => Self::Log, - "$TEMP" => Self::Temp, - "$APPCONFIG" => Self::AppConfig, - "$APPDATA" => Self::AppData, - "$APPLOCALDATA" => Self::AppLocalData, - "$APPCACHE" => Self::AppCache, - "$APPLOG" => Self::AppLog, - _ => return None, - }; - Some(res) - } -} - -/// Parse the given path, resolving a [`BaseDirectory`] variable if the path starts with one. -/// -/// # Examples -/// -/// ```rust,no_run -/// use tauri::Manager; -/// tauri::Builder::default() -/// .setup(|app| { -/// let path = tauri::api::path::parse(&app.config(), app.package_info(), &app.env(), "$HOME/.bashrc")?; -/// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.bashrc"); -/// Ok(()) -/// }); -/// ``` -pub fn parse>( - config: &Config, - package_info: &PackageInfo, - env: &Env, - path: P, -) -> crate::api::Result { - let mut p = PathBuf::new(); - let mut components = path.as_ref().components(); - match components.next() { - Some(Component::Normal(str)) => { - if let Some(base_directory) = BaseDirectory::from_variable(&str.to_string_lossy()) { - p.push(resolve_path( - config, - package_info, - env, - "", - Some(base_directory), - )?); - } else { - p.push(str); - } - } - Some(component) => p.push(component), - None => (), - } - - for component in components { - if let Component::ParentDir = component { - continue; - } - p.push(component); - } - - Ok(p) -} - -/// Resolves the path with the optional base directory. -/// -/// This is a low level API. If the application has been built, -/// prefer the [path resolver API](`crate::AppHandle#method.path_resolver`). -/// -/// # Examples -/// -/// ## Before initializing the application -/// -/// ```rust,no_run -/// use tauri::{api::path::{BaseDirectory, resolve_path}, Env}; -/// // on an actual app, remove the string argument -/// let context = tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"); -/// let path = resolve_path( -/// context.config(), -/// context.package_info(), -/// &Env::default(), -/// "db/tauri.sqlite", -/// Some(BaseDirectory::AppData)) -/// .expect("failed to resolve path"); -/// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.config/com.tauri.app/db/tauri.sqlite"); -/// -/// tauri::Builder::default().run(context).expect("error while running tauri application"); -/// ``` -/// -/// ## With an initialized app -/// ```rust,no_run -/// use tauri::{api::path::{BaseDirectory, resolve_path}, Manager}; -/// tauri::Builder::default() -/// .setup(|app| { -/// let path = resolve_path( -/// &app.config(), -/// app.package_info(), -/// &app.env(), -/// "path/to/something", -/// Some(BaseDirectory::Config) -/// )?; -/// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.config/path/to/something"); -/// Ok(()) -/// }); -/// ``` -pub fn resolve_path>( - config: &Config, - package_info: &PackageInfo, - env: &Env, - path: P, - dir: Option, -) -> crate::api::Result { - if let Some(base_dir) = dir { - let resolve_resource = matches!(base_dir, BaseDirectory::Resource); - let base_dir_path = match base_dir { - BaseDirectory::Audio => audio_dir(), - BaseDirectory::Cache => cache_dir(), - BaseDirectory::Config => config_dir(), - BaseDirectory::Data => data_dir(), - BaseDirectory::LocalData => local_data_dir(), - BaseDirectory::Desktop => desktop_dir(), - BaseDirectory::Document => document_dir(), - BaseDirectory::Download => download_dir(), - BaseDirectory::Executable => executable_dir(), - BaseDirectory::Font => font_dir(), - BaseDirectory::Home => home_dir(), - BaseDirectory::Picture => picture_dir(), - BaseDirectory::Public => public_dir(), - BaseDirectory::Runtime => runtime_dir(), - BaseDirectory::Template => template_dir(), - BaseDirectory::Video => video_dir(), - BaseDirectory::Resource => resource_dir(package_info, env), - #[allow(deprecated)] - BaseDirectory::App => app_config_dir(config), - #[allow(deprecated)] - BaseDirectory::Log => app_log_dir(config), - BaseDirectory::Temp => Some(temp_dir()), - BaseDirectory::AppConfig => app_config_dir(config), - BaseDirectory::AppData => app_data_dir(config), - BaseDirectory::AppLocalData => app_local_data_dir(config), - BaseDirectory::AppCache => app_cache_dir(config), - BaseDirectory::AppLog => app_log_dir(config), - }; - if let Some(mut base_dir_path_value) = base_dir_path { - // use the same path resolution mechanism as the bundler's resource injection algorithm - if resolve_resource { - let mut resource_path = PathBuf::new(); - for component in path.as_ref().components() { - match component { - Component::Prefix(_) => {} - Component::RootDir => resource_path.push("_root_"), - Component::CurDir => {} - Component::ParentDir => resource_path.push("_up_"), - Component::Normal(p) => resource_path.push(p), - } - } - base_dir_path_value.push(resource_path); - } else { - base_dir_path_value.push(path); - } - Ok(base_dir_path_value) - } else { - Err(crate::api::Error::Path( - "unable to determine base dir path".to_string(), - )) - } - } else { - let mut dir_path = PathBuf::new(); - dir_path.push(path); - Ok(dir_path) - } -} - -/// Returns the path to the user's audio directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_MUSIC_DIR`. -/// - **macOS:** Resolves to `$HOME/Music`. -/// - **Windows:** Resolves to `{FOLDERID_Music}`. -pub fn audio_dir() -> Option { - dirs_next::audio_dir() -} - -/// Returns the path to the user's cache directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to `$XDG_CACHE_HOME` or `$HOME/.cache`. -/// - **macOS:** Resolves to `$HOME/Library/Caches`. -/// - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. -pub fn cache_dir() -> Option { - dirs_next::cache_dir() -} - -/// Returns the path to the user's config directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to `$XDG_CONFIG_HOME` or `$HOME/.config`. -/// - **macOS:** Resolves to `$HOME/Library/Application Support`. -/// - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. -pub fn config_dir() -> Option { - dirs_next::config_dir() -} - -/// Returns the path to the user's data directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. -/// - **macOS:** Resolves to `$HOME/Library/Application Support`. -/// - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. -pub fn data_dir() -> Option { - dirs_next::data_dir() -} - -/// Returns the path to the user's local data directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. -/// - **macOS:** Resolves to `$HOME/Library/Application Support`. -/// - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. -pub fn local_data_dir() -> Option { - dirs_next::data_local_dir() -} - -/// Returns the path to the user's desktop directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DESKTOP_DIR`. -/// - **macOS:** Resolves to `$HOME/Desktop`. -/// - **Windows:** Resolves to `{FOLDERID_Desktop}`. -pub fn desktop_dir() -> Option { - dirs_next::desktop_dir() -} - -/// Returns the path to the user's document directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOCUMENTS_DIR`. -/// - **macOS:** Resolves to `$HOME/Documents`. -/// - **Windows:** Resolves to `{FOLDERID_Documents}`. -pub fn document_dir() -> Option { - dirs_next::document_dir() -} - -/// Returns the path to the user's download directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOWNLOAD_DIR`. -/// - **macOS:** Resolves to `$HOME/Downloads`. -/// - **Windows:** Resolves to `{FOLDERID_Downloads}`. -pub fn download_dir() -> Option { - dirs_next::download_dir() -} - -/// Returns the path to the user's executable directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to `$XDG_BIN_HOME/../bin` or `$XDG_DATA_HOME/../bin` or `$HOME/.local/bin`. -/// - **macOS:** Not supported. -/// - **Windows:** Not supported. -pub fn executable_dir() -> Option { - dirs_next::executable_dir() -} - -/// Returns the path to the user's font directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to `$XDG_DATA_HOME/fonts` or `$HOME/.local/share/fonts`. -/// - **macOS:** Resolves to `$HOME/Library/Fonts`. -/// - **Windows:** Not supported. -pub fn font_dir() -> Option { - dirs_next::font_dir() -} - -/// Returns the path to the user's home directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to `$HOME`. -/// - **macOS:** Resolves to `$HOME`. -/// - **Windows:** Resolves to `{FOLDERID_Profile}`. -pub fn home_dir() -> Option { - dirs_next::home_dir() -} - -/// Returns the path to the user's picture directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PICTURES_DIR`. -/// - **macOS:** Resolves to `$HOME/Pictures`. -/// - **Windows:** Resolves to `{FOLDERID_Pictures}`. -pub fn picture_dir() -> Option { - dirs_next::picture_dir() -} - -/// Returns the path to the user's public directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PUBLICSHARE_DIR`. -/// - **macOS:** Resolves to `$HOME/Public`. -/// - **Windows:** Resolves to `{FOLDERID_Public}`. -pub fn public_dir() -> Option { - dirs_next::public_dir() -} - -/// Returns the path to the user's runtime directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to `$XDG_RUNTIME_DIR`. -/// - **macOS:** Not supported. -/// - **Windows:** Not supported. -pub fn runtime_dir() -> Option { - dirs_next::runtime_dir() -} - -/// Returns the path to the user's template directory. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_TEMPLATES_DIR`. -/// - **macOS:** Not supported. -/// - **Windows:** Resolves to `{FOLDERID_Templates}`. -pub fn template_dir() -> Option { - dirs_next::template_dir() -} - -/// Returns the path to the user's video dir -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_VIDEOS_DIR`. -/// - **macOS:** Resolves to `$HOME/Movies`. -/// - **Windows:** Resolves to `{FOLDERID_Videos}`. -pub fn video_dir() -> Option { - dirs_next::video_dir() -} - -/// Returns the path to the resource directory of this app. -/// -/// See [`PathResolver::resource_dir`](crate::PathResolver#method.resource_dir) for a more convenient helper function. -pub fn resource_dir(package_info: &PackageInfo, env: &Env) -> Option { - crate::utils::platform::resource_dir(package_info, env).ok() -} - -/// Returns the path to the suggested directory for your app's config files. -/// -/// Resolves to [`config_dir`]`/${bundle_identifier}`. -/// -/// See [`PathResolver::app_config_dir`](crate::PathResolver#method.app_config_dir) for a more convenient helper function. -pub fn app_config_dir(config: &Config) -> Option { - dirs_next::config_dir().map(|dir| dir.join(&config.tauri.bundle.identifier)) -} - -/// Returns the path to the suggested directory for your app's data files. -/// -/// Resolves to [`data_dir`]`/${bundle_identifier}`. -/// -/// See [`PathResolver::app_data_dir`](crate::PathResolver#method.app_data_dir) for a more convenient helper function. -pub fn app_data_dir(config: &Config) -> Option { - dirs_next::data_dir().map(|dir| dir.join(&config.tauri.bundle.identifier)) -} - -/// Returns the path to the suggested directory for your app's local data files. -/// -/// Resolves to [`local_data_dir`]`/${bundle_identifier}`. -/// -/// See [`PathResolver::app_local_data_dir`](crate::PathResolver#method.app_local_data_dir) for a more convenient helper function. -pub fn app_local_data_dir(config: &Config) -> Option { - dirs_next::data_local_dir().map(|dir| dir.join(&config.tauri.bundle.identifier)) -} - -/// Returns the path to the suggested directory for your app's cache files. -/// -/// Resolves to [`cache_dir`]`/${bundle_identifier}`. -/// -/// See [`PathResolver::app_cache_dir`](crate::PathResolver#method.app_cache_dir) for a more convenient helper function. -pub fn app_cache_dir(config: &Config) -> Option { - dirs_next::cache_dir().map(|dir| dir.join(&config.tauri.bundle.identifier)) -} - -/// Returns the path to the suggested directory for your app's log files. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to [`config_dir`]`/${bundle_identifier}/logs`. -/// - **macOS:** Resolves to [`home_dir`]`/Library/Logs/${bundle_identifier}` -/// - **Windows:** Resolves to [`config_dir`]`/${bundle_identifier}/logs`. -/// -/// See [`PathResolver::app_log_dir`](crate::PathResolver#method.app_log_dir) for a more convenient helper function. -pub fn app_log_dir(config: &Config) -> Option { - #[cfg(target_os = "macos")] - let path = dirs_next::home_dir().map(|dir| { - dir - .join("Library/Logs") - .join(&config.tauri.bundle.identifier) - }); - - #[cfg(not(target_os = "macos"))] - let path = - dirs_next::config_dir().map(|dir| dir.join(&config.tauri.bundle.identifier).join("logs")); - - path -} - -/// Returns the path to the suggested directory for your app's config files. -/// -/// Resolves to [`config_dir`]`/${bundle_identifier}`. -/// -/// See [`PathResolver::app_config_dir`](crate::PathResolver#method.app_config_dir) for a more convenient helper function. -#[deprecated( - since = "1.2.0", - note = "Will be removed in 2.0.0. Use `app_config_dir` or `app_data_dir` instead." -)] -pub fn app_dir(config: &Config) -> Option { - app_config_dir(config) -} - -/// Returns the path to the suggested directory for your app's log files. -/// -/// ## Platform-specific -/// -/// - **Linux:** Resolves to [`config_dir`]`/${bundle_identifier}`. -/// - **macOS:** Resolves to [`home_dir`]`/Library/Logs/${bundle_identifier}` -/// - **Windows:** Resolves to [`config_dir`]`/${bundle_identifier}`. -/// -/// See [`PathResolver::app_log_dir`](crate::PathResolver#method.app_log_dir) for a more convenient helper function. -#[deprecated( - since = "1.2.0", - note = "Will be removed in 2.0.0. Use `app_log_dir` instead." -)] -pub fn log_dir(config: &Config) -> Option { - app_log_dir(config) -} diff --git a/core/tauri/src/api/process/command.rs b/core/tauri/src/api/process/command.rs deleted file mode 100644 index fe4911e1a35..00000000000 --- a/core/tauri/src/api/process/command.rs +++ /dev/null @@ -1,478 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use std::{ - collections::HashMap, - io::{BufReader, Write}, - path::PathBuf, - process::{Command as StdCommand, Stdio}, - sync::{Arc, Mutex, RwLock}, - thread::spawn, -}; - -#[cfg(unix)] -use std::os::unix::process::ExitStatusExt; -#[cfg(windows)] -use std::os::windows::process::CommandExt; - -#[cfg(windows)] -const CREATE_NO_WINDOW: u32 = 0x0800_0000; - -use crate::async_runtime::{block_on as block_on_task, channel, Receiver, Sender}; -pub use encoding_rs::Encoding; -use os_pipe::{pipe, PipeReader, PipeWriter}; -use serde::Serialize; -use shared_child::SharedChild; -use tauri_utils::platform; - -type ChildStore = Arc>>>; - -fn commands() -> &'static ChildStore { - use once_cell::sync::Lazy; - static STORE: Lazy = Lazy::new(Default::default); - &STORE -} - -/// Kills all child processes created with [`Command`]. -/// By default it's called before the [`crate::App`] exits. -pub fn kill_children() { - let commands = commands().lock().unwrap(); - let children = commands.values(); - for child in children { - let _ = child.kill(); - } -} - -/// Payload for the [`CommandEvent::Terminated`] command event. -#[derive(Debug, Clone, Serialize)] -pub struct TerminatedPayload { - /// Exit code of the process. - pub code: Option, - /// If the process was terminated by a signal, represents that signal. - pub signal: Option, -} - -/// A event sent to the command callback. -#[derive(Debug, Clone, Serialize)] -#[serde(tag = "event", content = "payload")] -#[non_exhaustive] -pub enum CommandEvent { - /// Stderr bytes until a newline (\n) or carriage return (\r) is found. - Stderr(String), - /// Stdout bytes until a newline (\n) or carriage return (\r) is found. - Stdout(String), - /// An error happened waiting for the command to finish or converting the stdout/stderr bytes to an UTF-8 string. - Error(String), - /// Command process terminated. - Terminated(TerminatedPayload), -} - -/// The type to spawn commands. -#[derive(Debug)] -pub struct Command { - program: String, - args: Vec, - env_clear: bool, - env: HashMap, - current_dir: Option, - encoding: Option<&'static Encoding>, -} - -/// Spawned child process. -#[derive(Debug)] -pub struct CommandChild { - inner: Arc, - stdin_writer: PipeWriter, -} - -impl CommandChild { - /// Writes to process stdin. - pub fn write(&mut self, buf: &[u8]) -> crate::api::Result<()> { - self.stdin_writer.write_all(buf)?; - Ok(()) - } - - /// Sends a kill signal to the child. - pub fn kill(self) -> crate::api::Result<()> { - self.inner.kill()?; - Ok(()) - } - - /// Returns the process pid. - pub fn pid(&self) -> u32 { - self.inner.id() - } -} - -/// Describes the result of a process after it has terminated. -#[derive(Debug)] -pub struct ExitStatus { - code: Option, -} - -impl ExitStatus { - /// Returns the exit code of the process, if any. - pub fn code(&self) -> Option { - self.code - } - - /// Returns true if exit status is zero. Signal termination is not considered a success, and success is defined as a zero exit status. - pub fn success(&self) -> bool { - self.code == Some(0) - } -} - -/// The output of a finished process. -#[derive(Debug)] -pub struct Output { - /// The status (exit code) of the process. - pub status: ExitStatus, - /// The data that the process wrote to stdout. - pub stdout: String, - /// The data that the process wrote to stderr. - pub stderr: String, -} - -fn relative_command_path(command: String) -> crate::Result { - match platform::current_exe()?.parent() { - #[cfg(windows)] - Some(exe_dir) => Ok(format!("{}\\{command}.exe", exe_dir.display())), - #[cfg(not(windows))] - Some(exe_dir) => Ok(format!("{}/{command}", exe_dir.display())), - None => Err(crate::api::Error::Command("Could not evaluate executable dir".to_string()).into()), - } -} - -impl From for StdCommand { - fn from(cmd: Command) -> StdCommand { - let mut command = StdCommand::new(cmd.program); - command.args(cmd.args); - command.stdout(Stdio::piped()); - command.stdin(Stdio::piped()); - command.stderr(Stdio::piped()); - if cmd.env_clear { - command.env_clear(); - } - command.envs(cmd.env); - if let Some(current_dir) = cmd.current_dir { - command.current_dir(current_dir); - } - #[cfg(windows)] - command.creation_flags(CREATE_NO_WINDOW); - command - } -} - -impl Command { - /// Creates a new Command for launching the given program. - pub fn new>(program: S) -> Self { - Self { - program: program.into(), - args: Default::default(), - env_clear: false, - env: Default::default(), - current_dir: None, - encoding: None, - } - } - - /// Creates a new Command for launching the given sidecar program. - /// - /// A sidecar program is a embedded external binary in order to make your application work - /// or to prevent users having to install additional dependencies (e.g. Node.js, Python, etc). - pub fn new_sidecar>(program: S) -> crate::Result { - Ok(Self::new(relative_command_path(program.into())?)) - } - - /// Appends arguments to the command. - #[must_use] - pub fn args(mut self, args: I) -> Self - where - I: IntoIterator, - S: AsRef, - { - for arg in args { - self.args.push(arg.as_ref().to_string()); - } - self - } - - /// Clears the entire environment map for the child process. - #[must_use] - pub fn env_clear(mut self) -> Self { - self.env_clear = true; - self - } - - /// Adds or updates multiple environment variable mappings. - #[must_use] - pub fn envs(mut self, env: HashMap) -> Self { - self.env = env; - self - } - - /// Sets the working directory for the child process. - #[must_use] - pub fn current_dir(mut self, current_dir: PathBuf) -> Self { - self.current_dir.replace(current_dir); - self - } - - /// Sets the character encoding for stdout/stderr. - #[must_use] - pub fn encoding(mut self, encoding: &'static Encoding) -> Self { - self.encoding.replace(encoding); - self - } - - /// Spawns the command. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::process::{Command, CommandEvent}; - /// tauri::async_runtime::spawn(async move { - /// let (mut rx, mut child) = Command::new("cargo") - /// .args(["tauri", "dev"]) - /// .spawn() - /// .expect("Failed to spawn cargo"); - /// - /// let mut i = 0; - /// while let Some(event) = rx.recv().await { - /// if let CommandEvent::Stdout(line) = event { - /// println!("got: {}", line); - /// i += 1; - /// if i == 4 { - /// child.write("message from Rust\n".as_bytes()).unwrap(); - /// i = 0; - /// } - /// } - /// } - /// }); - /// ``` - pub fn spawn(self) -> crate::api::Result<(Receiver, CommandChild)> { - let encoding = self.encoding; - let mut command: StdCommand = self.into(); - let (stdout_reader, stdout_writer) = pipe()?; - let (stderr_reader, stderr_writer) = pipe()?; - let (stdin_reader, stdin_writer) = pipe()?; - command.stdout(stdout_writer); - command.stderr(stderr_writer); - command.stdin(stdin_reader); - - let shared_child = SharedChild::spawn(&mut command)?; - let child = Arc::new(shared_child); - let child_ = child.clone(); - let guard = Arc::new(RwLock::new(())); - - commands().lock().unwrap().insert(child.id(), child.clone()); - - let (tx, rx) = channel(1); - - spawn_pipe_reader( - tx.clone(), - guard.clone(), - stdout_reader, - CommandEvent::Stdout, - encoding, - ); - spawn_pipe_reader( - tx.clone(), - guard.clone(), - stderr_reader, - CommandEvent::Stderr, - encoding, - ); - - spawn(move || { - let _ = match child_.wait() { - Ok(status) => { - let _l = guard.write().unwrap(); - commands().lock().unwrap().remove(&child_.id()); - block_on_task(async move { - tx.send(CommandEvent::Terminated(TerminatedPayload { - code: status.code(), - #[cfg(windows)] - signal: None, - #[cfg(unix)] - signal: status.signal(), - })) - .await - }) - } - Err(e) => { - let _l = guard.write().unwrap(); - block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await }) - } - }; - }); - - Ok(( - rx, - CommandChild { - inner: child, - stdin_writer, - }, - )) - } - - /// Executes a command as a child process, waiting for it to finish and collecting its exit status. - /// Stdin, stdout and stderr are ignored. - /// - /// # Examples - /// ```rust,no_run - /// use tauri::api::process::Command; - /// let status = Command::new("which").args(["ls"]).status().unwrap(); - /// println!("`which` finished with status: {:?}", status.code()); - /// ``` - pub fn status(self) -> crate::api::Result { - let (mut rx, _child) = self.spawn()?; - let code = crate::async_runtime::safe_block_on(async move { - let mut code = None; - #[allow(clippy::collapsible_match)] - while let Some(event) = rx.recv().await { - if let CommandEvent::Terminated(payload) = event { - code = payload.code; - } - } - code - }); - Ok(ExitStatus { code }) - } - - /// Executes the command as a child process, waiting for it to finish and collecting all of its output. - /// Stdin is ignored. - /// - /// # Examples - /// - /// ```rust,no_run - /// use tauri::api::process::Command; - /// let output = Command::new("echo").args(["TAURI"]).output().unwrap(); - /// assert!(output.status.success()); - /// assert_eq!(output.stdout, "TAURI"); - /// ``` - pub fn output(self) -> crate::api::Result { - let (mut rx, _child) = self.spawn()?; - - let output = crate::async_runtime::safe_block_on(async move { - let mut code = None; - let mut stdout = String::new(); - let mut stderr = String::new(); - while let Some(event) = rx.recv().await { - match event { - CommandEvent::Terminated(payload) => { - code = payload.code; - } - CommandEvent::Stdout(line) => { - stdout.push_str(line.as_str()); - stdout.push('\n'); - } - CommandEvent::Stderr(line) => { - stderr.push_str(line.as_str()); - stderr.push('\n'); - } - CommandEvent::Error(_) => {} - } - } - Output { - status: ExitStatus { code }, - stdout, - stderr, - } - }); - - Ok(output) - } -} - -fn spawn_pipe_reader CommandEvent + Send + Copy + 'static>( - tx: Sender, - guard: Arc>, - pipe_reader: PipeReader, - wrapper: F, - character_encoding: Option<&'static Encoding>, -) { - spawn(move || { - let _lock = guard.read().unwrap(); - let mut reader = BufReader::new(pipe_reader); - - let mut buf = Vec::new(); - loop { - buf.clear(); - match tauri_utils::io::read_line(&mut reader, &mut buf) { - Ok(n) => { - if n == 0 { - break; - } - let tx_ = tx.clone(); - let line = match character_encoding { - Some(encoding) => Ok(encoding.decode_with_bom_removal(&buf).0.into()), - None => String::from_utf8(buf.clone()), - }; - block_on_task(async move { - let _ = match line { - Ok(line) => tx_.send(wrapper(line)).await, - Err(e) => tx_.send(CommandEvent::Error(e.to_string())).await, - }; - }); - } - Err(e) => { - let tx_ = tx.clone(); - let _ = block_on_task(async move { tx_.send(CommandEvent::Error(e.to_string())).await }); - } - } - } - }); -} - -// tests for the commands functions. -#[cfg(test)] -mod test { - #[cfg(not(windows))] - use super::*; - - #[cfg(not(windows))] - #[test] - fn test_cmd_output() { - // create a command to run cat. - let cmd = Command::new("cat").args(["test/api/test.txt"]); - let (mut rx, _) = cmd.spawn().unwrap(); - - crate::async_runtime::block_on(async move { - while let Some(event) = rx.recv().await { - match event { - CommandEvent::Terminated(payload) => { - assert_eq!(payload.code, Some(0)); - } - CommandEvent::Stdout(line) => { - assert_eq!(line, "This is a test doc!".to_string()); - } - _ => {} - } - } - }); - } - - #[cfg(not(windows))] - #[test] - // test the failure case - fn test_cmd_fail() { - let cmd = Command::new("cat").args(["test/api/"]); - let (mut rx, _) = cmd.spawn().unwrap(); - - crate::async_runtime::block_on(async move { - while let Some(event) = rx.recv().await { - match event { - CommandEvent::Terminated(payload) => { - assert_eq!(payload.code, Some(1)); - } - CommandEvent::Stderr(line) => { - assert_eq!(line, "cat: test/api/: Is a directory".to_string()); - } - _ => {} - } - } - }); - } -} diff --git a/core/tauri/src/api/shell.rs b/core/tauri/src/api/shell.rs deleted file mode 100644 index 0a22dd67912..00000000000 --- a/core/tauri/src/api/shell.rs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! Types and functions related to shell. - -use crate::ShellScope; -use std::str::FromStr; - -/// Program to use on the [`open()`] call. -pub enum Program { - /// Use the `open` program. - Open, - /// Use the `start` program. - Start, - /// Use the `xdg-open` program. - XdgOpen, - /// Use the `gio` program. - Gio, - /// Use the `gnome-open` program. - GnomeOpen, - /// Use the `kde-open` program. - KdeOpen, - /// Use the `wslview` program. - WslView, - /// Use the `Firefox` program. - Firefox, - /// Use the `Google Chrome` program. - Chrome, - /// Use the `Chromium` program. - Chromium, - /// Use the `Safari` program. - Safari, -} - -impl FromStr for Program { - type Err = super::Error; - - fn from_str(s: &str) -> Result { - let p = match s.to_lowercase().as_str() { - "open" => Self::Open, - "start" => Self::Start, - "xdg-open" => Self::XdgOpen, - "gio" => Self::Gio, - "gnome-open" => Self::GnomeOpen, - "kde-open" => Self::KdeOpen, - "wslview" => Self::WslView, - "firefox" => Self::Firefox, - "chrome" | "google chrome" => Self::Chrome, - "chromium" => Self::Chromium, - "safari" => Self::Safari, - _ => return Err(super::Error::UnknownProgramName(s.to_string())), - }; - Ok(p) - } -} - -impl Program { - pub(crate) fn name(self) -> &'static str { - match self { - Self::Open => "open", - Self::Start => "start", - Self::XdgOpen => "xdg-open", - Self::Gio => "gio", - Self::GnomeOpen => "gnome-open", - Self::KdeOpen => "kde-open", - Self::WslView => "wslview", - - #[cfg(target_os = "macos")] - Self::Firefox => "Firefox", - #[cfg(not(target_os = "macos"))] - Self::Firefox => "firefox", - - #[cfg(target_os = "macos")] - Self::Chrome => "Google Chrome", - #[cfg(not(target_os = "macos"))] - Self::Chrome => "google-chrome", - - #[cfg(target_os = "macos")] - Self::Chromium => "Chromium", - #[cfg(not(target_os = "macos"))] - Self::Chromium => "chromium", - - #[cfg(target_os = "macos")] - Self::Safari => "Safari", - #[cfg(not(target_os = "macos"))] - Self::Safari => "safari", - } - } -} - -/// 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 `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`. -/// A custom validation regex may be supplied in the config in `tauri > allowlist > scope > open`. -/// -/// # Examples -/// -/// ```rust,no_run -/// use tauri::{api::shell::open, Manager}; -/// tauri::Builder::default() -/// .setup(|app| { -/// // open the given URL on the system default browser -/// open(&app.shell_scope(), "https://github.com/tauri-apps/tauri", None)?; -/// Ok(()) -/// }); -/// ``` -pub fn open>( - scope: &ShellScope, - path: P, - with: Option, -) -> crate::api::Result<()> { - scope - .open(path.as_ref(), with) - .map_err(|err| crate::api::Error::Shell(format!("failed to open: {err}"))) -} diff --git a/core/tauri/src/api/version.rs b/core/tauri/src/api/version.rs deleted file mode 100644 index 3a192ae2e77..00000000000 --- a/core/tauri/src/api/version.rs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! Compare two semantic versions. -//! -//! [Semantic Versioning](https://semver.org) is a guideline for how version numbers are assigned and incremented. -//! The functions on this module are helpers around [semver](https://docs.rs/semver/latest/semver/). - -use semver::Version; -use std::cmp::Ordering; - -/// Compare two semver versions. -/// -/// If the `first` semver is greater, returns -1. -/// If the `second` semver is greater, returns 1. -/// If they are equal, returns 0. -/// -/// # Examples -/// -/// ``` -/// use tauri::api::version::compare; -/// assert_eq!(compare("0.15.0", "0.15.5").unwrap(), 1); -/// assert_eq!(compare("0.15.10", "0.15.9").unwrap(), -1); -/// assert_eq!(compare("0.15.10", "0.16.10").unwrap(), 1); -/// assert_eq!(compare("1.57.0", "2.17.4").unwrap(), 1); -/// assert_eq!(compare("0.0.0", "0.0.0").unwrap(), 0); -/// ``` -pub fn compare(first: &str, second: &str) -> crate::api::Result { - let v1 = Version::parse(first)?; - let v2 = Version::parse(second)?; - match v1.cmp(&v2) { - Ordering::Greater => Ok(-1), - Ordering::Less => Ok(1), - Ordering::Equal => Ok(0), - } -} - -/// Check if the "second" semver is compatible with the "first". -/// -/// # Examples -/// -/// ``` -/// use tauri::api::version::is_compatible; -/// assert!(is_compatible("0.15.0", "0.15.5").unwrap()); -/// assert!(!is_compatible("0.15.0", "0.16.5").unwrap()); -/// -/// assert!(is_compatible("1.5.0", "1.5.10").unwrap()); -/// assert!(is_compatible("1.54.0", "1.55.0").unwrap()); -/// assert!(!is_compatible("2.17.0", "3.17.0").unwrap()); -/// ``` -pub fn is_compatible(first: &str, second: &str) -> crate::api::Result { - let first = Version::parse(first)?; - let second = Version::parse(second)?; - Ok(if second.major == 0 && first.major == 0 { - first.minor == second.minor && second.patch > first.patch - } else if second.major > 0 { - first.major == second.major - && ((second.minor > first.minor) - || (first.minor == second.minor && second.patch > first.patch)) - } else { - false - }) -} - -/// Check if a the "other" version is a major bump from the "current". -/// -/// # Examples -/// -/// ``` -/// use tauri::api::version::is_major; -/// assert!(is_major("1.0.0", "2.0.0").unwrap()); -/// assert!(is_major("1.5.0", "2.17.10").unwrap()); -/// assert!(is_major("0.5.0", "2.17.10").unwrap()); -/// assert!(!is_major("1.1.5", "1.2.5").unwrap()); -/// assert!(!is_major("0.14.0", "0.15.0").unwrap()); -/// ``` -pub fn is_major(current: &str, other: &str) -> crate::api::Result { - let current = Version::parse(current)?; - let other = Version::parse(other)?; - Ok(other.major > current.major) -} - -/// Check if a the "other" version is a minor bump from the "current". -/// -/// # Examples -/// -/// ``` -/// use tauri::api::version::is_minor; -/// assert!(is_minor("0.15.10", "0.16.110").unwrap()); -/// assert!(is_minor("1.0.0", "1.1.1").unwrap()); -/// assert!(!is_minor("2.1.9", "3.2.0").unwrap()); -/// assert!(!is_minor("1.0.0", "1.0.10").unwrap()); -/// ``` -pub fn is_minor(current: &str, other: &str) -> crate::api::Result { - let current = Version::parse(current)?; - let other = Version::parse(other)?; - Ok(current.major == other.major && other.minor > current.minor) -} - -/// Check if a the "other" version is a patch bump from the "current". -/// -/// # Examples -/// -/// ``` -/// use tauri::api::version::is_patch; -/// assert!(is_patch("0.15.0", "0.15.1").unwrap()); -/// assert!(is_patch("1.0.0", "1.0.1").unwrap()); -/// assert!(!is_patch("2.2.0", "2.3.1").unwrap()); -/// assert!(!is_patch("2.2.1", "1.1.0").unwrap()); -/// ``` -pub fn is_patch(current: &str, other: &str) -> crate::api::Result { - let current = Version::parse(current)?; - let other = Version::parse(other)?; - Ok(current.major == other.major && current.minor == other.minor && other.patch > current.patch) -} - -/// Check if a version is greater than the current. -/// -/// # Examples -/// -/// ``` -/// use tauri::api::version::is_greater; -/// assert!(is_greater("0.15.10", "0.16.0").unwrap()); -/// assert!(is_greater("1.0.0", "1.0.1").unwrap()); -/// assert!(is_greater("1.1.9", "1.2.0").unwrap()); -/// assert!(is_greater("1.0.0", "2.0.0").unwrap()); -/// ``` -pub fn is_greater(current: &str, other: &str) -> crate::api::Result { - Ok(Version::parse(other)? > Version::parse(current)?) -} diff --git a/core/tauri/src/app.rs b/core/tauri/src/app.rs index dcb965cdaed..e6dc3299891 100644 --- a/core/tauri/src/app.rs +++ b/core/tauri/src/app.rs @@ -2,14 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -#[cfg(all(desktop, feature = "system-tray"))] -pub(crate) mod tray; - use crate::{ - api::ipc::CallbackFn, command::{CommandArg, CommandItem}, - hooks::{ - window_invoke_responder, InvokeHandler, InvokeResponder, OnPageLoad, PageLoadPayload, SetupHook, + ipc::{ + channel::ChannelDataIpcQueue, CallbackFn, Invoke, InvokeError, InvokeHandler, InvokeResponder, + InvokeResponse, }, manager::{Asset, CustomProtocol, WindowManager}, plugin::{Plugin, PluginStore}, @@ -19,45 +16,73 @@ use crate::{ window::{PendingWindow, WindowEvent as RuntimeWindowEvent}, ExitRequestedEventAction, RunEvent as RuntimeRunEvent, }, - scope::{FsScope, IpcScope}, + scope::IpcScope, sealed::{ManagerBase, RuntimeOrDispatch}, utils::config::Config, - utils::{assets::Assets, resources::resource_relpath, Env}, - Context, DeviceEventFilter, EventLoopMessage, Invoke, InvokeError, InvokeResponse, Manager, - Runtime, Scopes, StateManager, Theme, Window, + utils::{assets::Assets, Env}, + Context, DeviceEventFilter, EventLoopMessage, Icon, Manager, Monitor, Runtime, Scopes, + StateManager, Theme, Window, }; -#[cfg(shell_scope)] -use crate::scope::ShellScope; +#[cfg(feature = "protocol-asset")] +use crate::scope::FsScope; +#[cfg(desktop)] +use crate::menu::{Menu, MenuEvent}; +#[cfg(all(desktop, feature = "tray-icon"))] +use crate::tray::{TrayIcon, TrayIconBuilder, TrayIconEvent, TrayIconId}; +#[cfg(desktop)] +use crate::window::WindowMenu; use raw_window_handle::HasRawDisplayHandle; +use serde::Deserialize; +use serialize_to_javascript::{default_template, DefaultTemplate, Template}; use tauri_macros::default_runtime; -use tauri_runtime::window::{ - dpi::{PhysicalPosition, PhysicalSize}, - FileDropEvent, +#[cfg(desktop)] +use tauri_runtime::EventLoopProxy; +use tauri_runtime::{ + window::{ + dpi::{PhysicalPosition, PhysicalSize}, + FileDropEvent, + }, + RuntimeInitArgs, }; use tauri_utils::PackageInfo; use std::{ collections::HashMap, - path::{Path, PathBuf}, + fmt, sync::{mpsc::Sender, Arc, Weak}, }; -use crate::runtime::menu::{Menu, MenuId, MenuIdRef}; - use crate::runtime::RuntimeHandle; -#[cfg(updater)] -use crate::updater; - #[cfg(target_os = "macos")] use crate::ActivationPolicy; -pub(crate) type GlobalMenuEventListener = Box) + Send + Sync>; +#[cfg(desktop)] +pub(crate) type GlobalMenuEventListener = Box; +#[cfg(all(desktop, feature = "tray-icon"))] +pub(crate) type GlobalTrayIconEventListener = + Box; pub(crate) type GlobalWindowEventListener = Box) + Send + Sync>; -#[cfg(all(desktop, feature = "system-tray"))] -type SystemTrayEventListener = Box, tray::SystemTrayEvent) + Send + Sync>; +/// A closure that is run when the Tauri application is setting up. +pub type SetupHook = + Box) -> Result<(), Box> + Send>; +/// A closure that is run once every time a window is created and loaded. +pub type OnPageLoad = dyn Fn(Window, PageLoadPayload) + Send + Sync + 'static; + +/// The payload for the [`OnPageLoad`] hook. +#[derive(Debug, Clone, Deserialize)] +pub struct PageLoadPayload { + url: String, +} + +impl PageLoadPayload { + /// The page URL. + pub fn url(&self) -> &str { + &self.url + } +} /// Api exposed on the `ExitRequested` event. #[derive(Debug)] @@ -180,41 +205,34 @@ pub enum RunEvent { /// /// This event is useful as a place to put your code that should be run after all state-changing events have been handled and you want to do stuff (updating state, performing calculations, etc) that happens as the “main body” of your event loop. MainEventsCleared, - /// Updater event. - #[cfg(updater)] - #[cfg_attr(doc_cfg, doc(cfg(feature = "updater")))] - Updater(crate::UpdaterEvent), + /// Emitted when the user wants to open the specified resource with the app. + #[cfg(any(target_os = "macos", target_os = "ios"))] + #[cfg_attr(doc_cfg, doc(cfg(any(target_os = "macos", feature = "ios"))))] + Opened { + /// The URL of the resources that is being open. + urls: Vec, + }, + /// An event from a menu item, could be on the window menu bar, application menu bar (on macOS) or tray icon menu. + #[cfg(desktop)] + #[cfg_attr(doc_cfg, doc(cfg(desktop)))] + MenuEvent(crate::menu::MenuEvent), + /// An event from a tray icon. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))] + TrayIconEvent(crate::tray::TrayIconEvent), } impl From for RunEvent { fn from(event: EventLoopMessage) -> Self { match event { - #[cfg(updater)] - EventLoopMessage::Updater(event) => RunEvent::Updater(event), + #[cfg(desktop)] + EventLoopMessage::MenuEvent(e) => Self::MenuEvent(e), + #[cfg(all(desktop, feature = "tray-icon"))] + EventLoopMessage::TrayIconEvent(e) => Self::TrayIconEvent(e), } } } -/// A menu event that was triggered on a window. -#[default_runtime(crate::Wry, wry)] -#[derive(Debug)] -pub struct WindowMenuEvent { - pub(crate) menu_item_id: MenuId, - pub(crate) window: Window, -} - -impl WindowMenuEvent { - /// The menu item id. - pub fn menu_item_id(&self) -> MenuIdRef<'_> { - &self.menu_item_id - } - - /// The window that the menu belongs to. - pub fn window(&self) -> &Window { - &self.window - } -} - /// A window event that was triggered on the specified window. #[default_runtime(crate::Wry, wry)] #[derive(Debug)] @@ -235,117 +253,6 @@ impl GlobalWindowEvent { } } -#[cfg(updater)] -#[derive(Debug, Clone, Default)] -pub(crate) struct UpdaterSettings { - pub(crate) target: Option, -} - -/// The path resolver is a helper for the application-specific [`crate::api::path`] APIs. -#[derive(Debug, Clone)] -pub struct PathResolver { - env: Env, - config: Arc, - package_info: PackageInfo, -} - -impl PathResolver { - /// Returns the path to the resource directory of this app. - /// - /// Helper function for [`crate::api::path::resource_dir`]. - pub fn resource_dir(&self) -> Option { - crate::api::path::resource_dir(&self.package_info, &self.env) - } - - /// Resolves the path of the given resource. - /// Note that the path must be the same as provided in `tauri.conf.json`. - /// - /// This function is helpful when your resource path includes a root dir (`/`) or parent component (`..`), - /// because Tauri replaces them with a parent folder, so simply using [`Self::resource_dir`] and joining the path - /// won't work. - /// - /// # Examples - /// - /// `tauri.conf.json`: - /// ```json - /// { - /// "tauri": { - /// "bundle": { - /// "resources": ["../assets/*"] - /// } - /// } - /// } - /// ``` - /// - /// ```no_run - /// tauri::Builder::default() - /// .setup(|app| { - /// let resource_path = app.path_resolver() - /// .resolve_resource("../assets/logo.svg") - /// .expect("failed to resolve resource dir"); - /// Ok(()) - /// }); - /// ``` - pub fn resolve_resource>(&self, path: P) -> Option { - self - .resource_dir() - .map(|dir| dir.join(resource_relpath(path.as_ref()))) - } - - /// Returns the path to the suggested directory for your app's config files. - /// - /// Helper function for [`crate::api::path::app_config_dir`]. - pub fn app_config_dir(&self) -> Option { - crate::api::path::app_config_dir(&self.config) - } - - /// Returns the path to the suggested directory for your app's data files. - /// - /// Helper function for [`crate::api::path::app_data_dir`]. - pub fn app_data_dir(&self) -> Option { - crate::api::path::app_data_dir(&self.config) - } - - /// Returns the path to the suggested directory for your app's local data files. - /// - /// Helper function for [`crate::api::path::app_local_data_dir`]. - pub fn app_local_data_dir(&self) -> Option { - crate::api::path::app_local_data_dir(&self.config) - } - - /// Returns the path to the suggested directory for your app's cache files. - /// - /// Helper function for [`crate::api::path::app_cache_dir`]. - pub fn app_cache_dir(&self) -> Option { - crate::api::path::app_cache_dir(&self.config) - } - - /// Returns the path to the suggested directory for your app's log files. - /// - /// Helper function for [`crate::api::path::app_log_dir`]. - pub fn app_log_dir(&self) -> Option { - crate::api::path::app_log_dir(&self.config) - } - - /// Returns the path to the suggested directory for your app's config files. - #[deprecated( - since = "1.2.0", - note = "Will be removed in 2.0.0. Use `app_config_dir` or `app_data_dir` instead." - )] - pub fn app_dir(&self) -> Option { - self.app_config_dir() - } - - /// Returns the path to the suggested directory for your app's log files. - #[deprecated( - since = "1.2.0", - note = "Will be removed in 2.0.0. Use `app_log_dir` instead." - )] - pub fn log_dir(&self) -> Option { - self.app_log_dir() - } -} - /// The asset resolver is a helper to access the [`tauri_utils::assets::Assets`] interface. #[derive(Debug, Clone)] pub struct AssetResolver { @@ -365,23 +272,8 @@ impl AssetResolver { #[default_runtime(crate::Wry, wry)] #[derive(Debug)] pub struct AppHandle { - runtime_handle: R::Handle, + pub(crate) runtime_handle: R::Handle, pub(crate) manager: WindowManager, - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager: R::GlobalShortcutManager, - #[cfg(feature = "clipboard")] - clipboard_manager: R::ClipboardManager, - /// The updater configuration. - #[cfg(updater)] - pub(crate) updater_settings: UpdaterSettings, -} - -impl AppHandle { - // currently only used on the updater - #[allow(dead_code)] - pub(crate) fn create_proxy(&self) -> R::EventLoopProxy { - self.runtime_handle.create_proxy() - } } /// APIs specific to the wry runtime. @@ -418,12 +310,6 @@ impl Clone for AppHandle { Self { runtime_handle: self.runtime_handle.clone(), manager: self.manager.clone(), - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager: self.global_shortcut_manager.clone(), - #[cfg(feature = "clipboard")] - clipboard_manager: self.clipboard_manager.clone(), - #[cfg(updater)] - updater_settings: self.updater_settings.clone(), } } } @@ -461,7 +347,7 @@ impl AppHandle { /// /// tauri::Builder::default() /// .setup(move |app| { - /// let handle = app.handle(); + /// let handle = app.handle().clone(); /// std::thread::spawn(move || { /// handle.plugin(init_plugin()); /// }); @@ -509,7 +395,7 @@ impl AppHandle { /// tauri::Builder::default() /// .plugin(plugin) /// .setup(move |app| { - /// let handle = app.handle(); + /// let handle = app.handle().clone(); /// std::thread::spawn(move || { /// handle.remove_plugin(plugin_name); /// }); @@ -533,24 +419,10 @@ impl AppHandle { std::process::exit(exit_code); } - /// Restarts the app. This is the same as [`crate::api::process::restart`], but it performs cleanup on this application. + /// Restarts the app. This is the same as [`crate::process::restart`], but it performs cleanup on this application. pub fn restart(&self) { self.cleanup_before_exit(); - crate::api::process::restart(&self.env()); - } - - /// Runs necessary cleanup tasks before exiting the process - fn cleanup_before_exit(&self) { - #[cfg(any(shell_execute, shell_sidecar))] - { - crate::api::process::kill_children(); - } - #[cfg(all(windows, feature = "system-tray"))] - { - for tray in self.manager().trays().values() { - let _ = tray.destroy(); - } - } + crate::process::restart(&self.env()); } } @@ -564,8 +436,8 @@ impl ManagerBase for AppHandle { RuntimeOrDispatch::RuntimeHandle(self.runtime_handle.clone()) } - fn managed_app_handle(&self) -> AppHandle { - self.clone() + fn managed_app_handle(&self) -> &AppHandle { + self } } @@ -573,17 +445,24 @@ impl ManagerBase for AppHandle { /// /// This type implements [`Manager`] which allows for manipulation of global application items. #[default_runtime(crate::Wry, wry)] -#[derive(Debug)] pub struct App { runtime: Option, + pending_windows: Option>>, + setup: Option>, manager: WindowManager, - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager: R::GlobalShortcutManager, - #[cfg(feature = "clipboard")] - clipboard_manager: R::ClipboardManager, handle: AppHandle, } +impl fmt::Debug for App { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("App") + .field("runtime", &self.runtime) + .field("manager", &self.manager) + .field("handle", &self.handle) + .finish() + } +} + impl Manager for App {} impl ManagerBase for App { fn manager(&self) -> &WindowManager { @@ -591,10 +470,14 @@ impl ManagerBase for App { } fn runtime(&self) -> RuntimeOrDispatch<'_, R> { - RuntimeOrDispatch::Runtime(self.runtime.as_ref().unwrap()) + if let Some(runtime) = self.runtime.as_ref() { + RuntimeOrDispatch::Runtime(runtime) + } else { + self.handle.runtime() + } } - fn managed_app_handle(&self) -> AppHandle { + fn managed_app_handle(&self) -> &AppHandle { self.handle() } } @@ -607,129 +490,114 @@ impl App { /// # Stability /// /// This API is unstable. - pub fn wry_plugin + 'static>( + pub fn wry_plugin + Send + 'static>( &mut self, plugin: P, - ) { - self.runtime.as_mut().unwrap().plugin(plugin); + ) where +

>::Plugin: Send, + { + self.handle.runtime_handle.plugin(plugin); } } macro_rules! shared_app_impl { ($app: ty) => { impl $app { - #[cfg(updater)] - #[cfg_attr(doc_cfg, doc(cfg(feature = "updater")))] - /// Gets the updater builder to manually check if an update is available. - /// - /// # Examples - /// - /// ```no_run - /// tauri::Builder::default() - /// .setup(|app| { - /// let handle = app.handle(); - /// tauri::async_runtime::spawn(async move { - #[cfg_attr( - feature = "updater", - doc = r#" let response = handle.updater().check().await;"# - )] - /// }); - /// Ok(()) - /// }); - /// ``` - pub fn updater(&self) -> updater::UpdateBuilder { - updater::builder(self.app_handle()) + /// Registers a global menu event listener. + #[cfg(desktop)] + pub fn on_menu_event, MenuEvent) + Send + Sync + 'static>( + &self, + handler: F, + ) { + self + .manager + .inner + .menu_event_listeners + .lock() + .unwrap() + .push(Box::new(handler)); } - /// Gets a handle to the first system tray. - /// - /// Prefer [`Self::tray_handle_by_id`] when multiple system trays are created. - /// - /// # Examples - /// ``` - /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu}; - /// - /// tauri::Builder::default() - /// .setup(|app| { - /// let app_handle = app.handle(); - /// SystemTray::new() - /// .with_menu( - /// SystemTrayMenu::new() - /// .add_item(CustomMenuItem::new("quit", "Quit")) - /// .add_item(CustomMenuItem::new("open", "Open")) - /// ) - /// .on_event(move |event| { - /// let tray_handle = app_handle.tray_handle(); - /// }) - /// .build(app)?; - /// Ok(()) - /// }); - /// ``` - #[cfg(all(desktop, feature = "system-tray"))] - #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] - pub fn tray_handle(&self) -> tray::SystemTrayHandle { + /// Registers a global tray icon menu event listener. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))] + pub fn on_tray_icon_event, TrayIconEvent) + Send + Sync + 'static>( + &self, + handler: F, + ) { self - .manager() - .trays() - .values() - .next() - .cloned() - .expect("tray not configured; use the `Builder#system_tray`, `App#system_tray` or `AppHandle#system_tray` APIs first.") + .manager + .inner + .global_tray_event_listeners + .lock() + .unwrap() + .push(Box::new(handler)); } - - /// Gets a handle to a system tray by its id. - /// - /// ``` - /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu}; - /// - /// tauri::Builder::default() - /// .setup(|app| { - /// let app_handle = app.handle(); - /// let tray_id = "my-tray"; - /// SystemTray::new() - /// .with_id(tray_id) - /// .with_menu( - /// SystemTrayMenu::new() - /// .add_item(CustomMenuItem::new("quit", "Quit")) - /// .add_item(CustomMenuItem::new("open", "Open")) - /// ) - /// .on_event(move |event| { - /// let tray_handle = app_handle.tray_handle_by_id(tray_id).unwrap(); - /// }) - /// .build(app)?; - /// Ok(()) - /// }); - /// ``` - #[cfg(all(desktop, feature = "system-tray"))] - #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] - pub fn tray_handle_by_id(&self, id: &str) -> Option> { + /// Gets the first tray icon registerd, usually the one configured in + /// tauri config file. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))] + pub fn tray(&self) -> Option> { self - .manager() - .get_tray(id) + .manager + .inner + .tray_icons + .lock() + .unwrap() + .first() + .cloned() } - /// The path resolver for the application. - pub fn path_resolver(&self) -> PathResolver { - PathResolver { - env: self.state::().inner().clone(), - config: self.manager.config(), - package_info: self.manager.package_info().clone(), + /// Removes the first tray icon registerd, usually the one configured in + /// tauri config file, from tauri's internal state and returns it. + /// + /// Note that dropping the returned icon, will cause the tray icon to disappear. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))] + pub fn remove_tray(&self) -> Option> { + let mut tray_icons = self.manager.inner.tray_icons.lock().unwrap(); + if !tray_icons.is_empty() { + return Some(tray_icons.swap_remove(0)); } + None } - /// Gets a copy of the global shortcut manager instance. - #[cfg(all(desktop, feature = "global-shortcut"))] - #[cfg_attr(doc_cfg, doc(cfg(feature = "global-shortcut")))] - pub fn global_shortcut_manager(&self) -> R::GlobalShortcutManager { - self.global_shortcut_manager.clone() + /// Gets a tray icon using the provided id. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))] + pub fn tray_by_id<'a, I>(&self, id: &'a I) -> Option> + where + I: ?Sized, + TrayIconId: PartialEq<&'a I>, + { + self + .manager + .inner + .tray_icons + .lock() + .unwrap() + .iter() + .find(|t| t.id() == &id) + .cloned() } - /// Gets a copy of the clipboard manager instance. - #[cfg(feature = "clipboard")] - #[cfg_attr(doc_cfg, doc(cfg(feature = "clipboard")))] - pub fn clipboard_manager(&self) -> R::ClipboardManager { - self.clipboard_manager.clone() + /// Removes a tray icon using the provided id from tauri's internal state and returns it. + /// + /// Note that dropping the returned icon, will cause the tray icon to disappear. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))] + pub fn remove_tray_by_id<'a, I>(&self, id: &'a I) -> Option> + where + I: ?Sized, + TrayIconId: PartialEq<&'a I>, + { + let mut tray_icons = self.manager.inner.tray_icons.lock().unwrap(); + let idx = tray_icons.iter().position(|t| t.id() == &id); + if let Some(idx) = idx { + return Some(tray_icons.swap_remove(idx)); + } + None } /// Gets the app's configuration, defined on the `tauri.conf.json` file. @@ -749,6 +617,160 @@ macro_rules! shared_app_impl { } } + /// Returns the primary monitor of the system. + /// + /// Returns None if it can't identify any monitor as a primary one. + pub fn primary_monitor(&self) -> crate::Result> { + Ok(match self.runtime() { + RuntimeOrDispatch::Runtime(h) => h.primary_monitor().map(Into::into), + RuntimeOrDispatch::RuntimeHandle(h) => h.primary_monitor().map(Into::into), + _ => unreachable!(), + }) + } + + /// Returns the list of all the monitors available on the system. + pub fn available_monitors(&self) -> crate::Result> { + Ok(match self.runtime() { + RuntimeOrDispatch::Runtime(h) => { + h.available_monitors().into_iter().map(Into::into).collect() + } + RuntimeOrDispatch::RuntimeHandle(h) => { + h.available_monitors().into_iter().map(Into::into).collect() + } + _ => unreachable!(), + }) + } + /// Returns the default window icon. + pub fn default_window_icon(&self) -> Option<&Icon> { + self.manager.inner.default_window_icon.as_ref() + } + + /// Returns the app-wide menu. + #[cfg(desktop)] + pub fn menu(&self) -> Option> { + self.manager.menu_lock().clone() + } + + /// Sets the app-wide menu and returns the previous one. + /// + /// If a window was not created with an explicit menu or had one set explicitly, + /// this menu will be assigned to it. + #[cfg(desktop)] + pub fn set_menu(&self, menu: Menu) -> crate::Result>> { + let prev_menu = self.remove_menu()?; + + self.manager.insert_menu_into_stash(&menu); + + self.manager.menu_lock().replace(menu.clone()); + + // set it on all windows that don't have one or previously had the app-wide menu + #[cfg(not(target_os = "macos"))] + { + for window in self.manager.windows().values() { + let has_app_wide_menu = window.has_app_wide_menu() || window.menu().is_none(); + if has_app_wide_menu { + window.set_menu(menu.clone())?; + window.menu_lock().replace(WindowMenu { + is_app_wide: true, + menu: menu.clone(), + }); + } + } + } + + // set it app-wide for macos + #[cfg(target_os = "macos")] + { + let menu_ = menu.clone(); + self.run_on_main_thread(move || { + let _ = init_app_menu(&menu_); + })?; + } + + Ok(prev_menu) + } + + /// Remove the app-wide menu and returns it. + /// + /// If a window was not created with an explicit menu or had one set explicitly, + /// this will remove the menu from it. + #[cfg(desktop)] + pub fn remove_menu(&self) -> crate::Result>> { + let menu = self.manager.menu_lock().as_ref().cloned(); + #[allow(unused_variables)] + if let Some(menu) = menu { + // remove from windows that have the app-wide menu + #[cfg(not(target_os = "macos"))] + { + for window in self.manager.windows().values() { + let has_app_wide_menu = window.has_app_wide_menu(); + if has_app_wide_menu { + window.remove_menu()?; + *window.menu_lock() = None; + } + } + } + + // remove app-wide for macos + #[cfg(target_os = "macos")] + { + self.run_on_main_thread(move || { + menu.inner().remove_for_nsapp(); + })?; + } + } + + let prev_menu = self.manager.menu_lock().take(); + + self + .manager + .remove_menu_from_stash_by_id(prev_menu.as_ref().map(|m| m.id())); + + Ok(prev_menu) + } + + /// Hides the app-wide menu from windows that have it. + /// + /// If a window was not created with an explicit menu or had one set explicitly, + /// this will hide the menu from it. + #[cfg(desktop)] + pub fn hide_menu(&self) -> crate::Result<()> { + #[cfg(not(target_os = "macos"))] + { + let is_app_menu_set = self.manager.menu_lock().is_some(); + if is_app_menu_set { + for window in self.manager.windows().values() { + if window.has_app_wide_menu() { + window.hide_menu()?; + } + } + } + } + + Ok(()) + } + + /// Shows the app-wide menu for windows that have it. + /// + /// If a window was not created with an explicit menu or had one set explicitly, + /// this will show the menu for it. + #[cfg(desktop)] + pub fn show_menu(&self) -> crate::Result<()> { + #[cfg(not(target_os = "macos"))] + { + let is_app_menu_set = self.manager.menu_lock().is_some(); + if is_app_menu_set { + for window in self.manager.windows().values() { + if window.has_app_wide_menu() { + window.show_menu()?; + } + } + } + } + + Ok(()) + } + /// Shows the application, but does not automatically focus it. #[cfg(target_os = "macos")] pub fn show(&self) -> crate::Result<()> { @@ -770,6 +792,13 @@ macro_rules! shared_app_impl { } Ok(()) } + + /// Runs necessary cleanup tasks before exiting the process. + /// **You should always exit the tauri app immediately after this function returns and not use any tauri-related APIs.** + pub fn cleanup_before_exit(&self) { + #[cfg(all(desktop, feature = "tray-icon"))] + self.manager.inner.tray_icons.lock().unwrap().clear() + } } }; } @@ -778,9 +807,20 @@ shared_app_impl!(App); shared_app_impl!(AppHandle); impl App { + fn register_core_plugins(&self) -> crate::Result<()> { + self.handle.plugin(crate::path::init())?; + self.handle.plugin(crate::event::init())?; + Ok(()) + } + + /// Runs the given closure on the main thread. + pub fn run_on_main_thread(&self, f: F) -> crate::Result<()> { + self.app_handle().run_on_main_thread(f) + } + /// Gets a handle to the application instance. - pub fn handle(&self) -> AppHandle { - self.handle.clone() + pub fn handle(&self) -> &AppHandle { + &self.handle } /// Sets the activation policy for the application. It is set to `NSApplicationActivationPolicyRegular` by default. @@ -834,26 +874,6 @@ impl App { .set_device_event_filter(filter); } - /// Gets the argument matches of the CLI definition configured in `tauri.conf.json`. - /// - /// # Examples - /// - /// ``` - /// tauri::Builder::default() - /// .setup(|app| { - /// let matches = app.get_cli_matches()?; - /// Ok(()) - /// }); - /// ``` - #[cfg(cli)] - pub fn get_cli_matches(&self) -> crate::Result { - if let Some(cli) = &self.manager.config().tauri.cli { - crate::api::cli::get_matches(cli, self.manager.package_info()).map_err(Into::into) - } else { - Ok(Default::default()) - } - } - /// Runs the application. /// /// # Examples @@ -870,9 +890,20 @@ impl App { /// }); /// ``` pub fn run, RunEvent) + 'static>(mut self, mut callback: F) { - let app_handle = self.handle(); + let app_handle = self.handle().clone(); let manager = self.manager.clone(); self.runtime.take().unwrap().run(move |event| match event { + RuntimeRunEvent::Ready => { + if let Err(e) = setup(&mut self) { + panic!("Failed to setup app: {e}"); + } + on_event_loop_event( + &app_handle, + RuntimeRunEvent::Ready, + &manager, + Some(&mut callback), + ); + } RuntimeRunEvent::Exit => { on_event_loop_event( &app_handle, @@ -891,8 +922,7 @@ impl App { /// Runs a iteration of the runtime event loop and immediately return. /// /// Note that when using this API, app cleanup is not automatically done. - /// The cleanup calls [`crate::api::process::kill_children`] so you may want to call that function before exiting the application. - /// Additionally, the cleanup calls [AppHandle#remove_system_tray](`AppHandle#method.remove_system_tray`) (Windows only). + /// The cleanup calls [`App::cleanup_before_exit`] so you may want to call that function before exiting the application. /// /// # Examples /// ```no_run @@ -903,6 +933,7 @@ impl App { /// loop { /// let iteration = app.run_iteration(); /// if iteration.window_count == 0 { + /// app.cleanup_before_exit(); /// break; /// } /// } @@ -910,7 +941,7 @@ impl App { #[cfg(desktop)] pub fn run_iteration(&mut self) -> crate::runtime::RunIteration { let manager = self.manager.clone(); - let app_handle = self.handle(); + let app_handle = self.handle().clone(); self.runtime.as_mut().unwrap().run_iteration(move |event| { on_event_loop_event( &app_handle, @@ -922,55 +953,6 @@ impl App { } } -#[cfg(updater)] -impl App { - /// Runs the updater hook with built-in dialog. - fn run_updater_dialog(&self) { - let handle = self.handle(); - - crate::async_runtime::spawn(async move { updater::check_update_with_dialog(handle).await }); - } - - fn run_updater(&self) { - let handle = self.handle(); - let handle_ = handle.clone(); - let updater_config = self.manager.config().tauri.updater.clone(); - // check if updater is active or not - if updater_config.active { - if updater_config.dialog { - #[cfg(not(target_os = "linux"))] - let updater_enabled = true; - #[cfg(target_os = "linux")] - let updater_enabled = cfg!(dev) || self.state::().appimage.is_some(); - if updater_enabled { - // if updater dialog is enabled spawn a new task - self.run_updater_dialog(); - // When dialog is enabled, if user want to recheck - // if an update is available after first start - // invoke the Event `tauri://update` from JS or rust side. - handle.listen_global(updater::EVENT_CHECK_UPDATE, move |_msg| { - let handle = handle_.clone(); - // re-spawn task inside tokyo to launch the download - // we don't need to emit anything as everything is handled - // by the process (user is asked to restart at the end) - // and it's handled by the updater - crate::async_runtime::spawn( - async move { updater::check_update_with_dialog(handle).await }, - ); - }); - } - } else { - // we only listen for `tauri://update` - // once we receive the call, we check if an update is available or not - // if there is a new update we emit `tauri://update-available` with details - // this is the user responsibilities to display dialog and ask if user want to install - // to install the update you need to invoke the Event `tauri://update-install` - updater::listener(handle); - } - } - } -} - /// Builds a Tauri application. /// /// # Examples @@ -990,7 +972,7 @@ pub struct Builder { invoke_handler: Box>, /// The JS message responder. - pub(crate) invoke_responder: Arc>, + invoke_responder: Option>>, /// The script that initializes the `window.__TAURI_POST_MESSAGE__` function. invoke_initialization_script: String, @@ -1013,35 +995,32 @@ pub struct Builder { /// App state. state: StateManager, - /// The menu set to all windows. - menu: Option

, + /// A closure that returns the menu set to all windows. + #[cfg(desktop)] + menu: Option) -> crate::Result> + Send>>, /// Enable macOS default menu creation. #[allow(unused)] enable_macos_default_menu: bool, - /// Menu event handlers that listens to all windows. - menu_event_listeners: Vec>, - /// Window event handlers that listens to all windows. window_event_listeners: Vec>, - /// The app system tray. - #[cfg(all(desktop, feature = "system-tray"))] - system_tray: Option, - - /// System tray event handlers. - #[cfg(all(desktop, feature = "system-tray"))] - system_tray_event_listeners: Vec>, - - /// The updater configuration. - #[cfg(updater)] - updater_settings: UpdaterSettings, - /// The device event filter. device_event_filter: DeviceEventFilter, } +#[derive(Template)] +#[default_template("../scripts/ipc-protocol.js")] +struct InvokeInitializationScript<'a> { + /// The function that processes the IPC message. + #[raw] + process_ipc_message_fn: &'a str, + os_name: &'a str, + fetch_channel_data_command: &'a str, + use_custom_protocol: bool, +} + impl Builder { /// Creates a new App builder. pub fn new() -> Self { @@ -1049,25 +1028,26 @@ impl Builder { #[cfg(any(windows, target_os = "linux"))] runtime_any_thread: false, setup: Box::new(|_| Ok(())), - invoke_handler: Box::new(|invoke| invoke.resolver.reject("not implemented")), - invoke_responder: Arc::new(window_invoke_responder), - invoke_initialization_script: - format!("Object.defineProperty(window, '__TAURI_POST_MESSAGE__', {{ value: (message) => window.ipc.postMessage({}(message)) }})", crate::manager::STRINGIFY_IPC_MESSAGE_FN), + invoke_handler: Box::new(|_| false), + invoke_responder: None, + invoke_initialization_script: InvokeInitializationScript { + process_ipc_message_fn: crate::manager::PROCESS_IPC_MESSAGE_FN, + os_name: std::env::consts::OS, + fetch_channel_data_command: crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND, + use_custom_protocol: cfg!(ipc_custom_protocol), + } + .render_default(&Default::default()) + .unwrap() + .into_string(), on_page_load: Box::new(|_, _| ()), pending_windows: Default::default(), plugins: PluginStore::default(), uri_scheme_protocols: Default::default(), state: StateManager::new(), + #[cfg(desktop)] menu: None, enable_macos_default_menu: true, - menu_event_listeners: Vec::new(), window_event_listeners: Vec::new(), - #[cfg(all(desktop, feature = "system-tray"))] - system_tray: None, - #[cfg(all(desktop, feature = "system-tray"))] - system_tray_event_listeners: Vec::new(), - #[cfg(updater)] - updater_settings: Default::default(), device_event_filter: Default::default(), } } @@ -1102,7 +1082,7 @@ impl Builder { #[must_use] pub fn invoke_handler(mut self, invoke_handler: F) -> Self where - F: Fn(Invoke) + Send + Sync + 'static, + F: Fn(Invoke) -> bool + Send + Sync + 'static, { self.invoke_handler = Box::new(invoke_handler); self @@ -1113,14 +1093,14 @@ impl Builder { /// The `responder` is a function that will be called when a command has been executed and must send a response to the JS layer. /// /// The `initialization_script` is a script that initializes `window.__TAURI_POST_MESSAGE__`. - /// That function must take the `message: object` argument and send it to the backend. + /// That function must take the `(message: object, options: object)` arguments and send it to the backend. #[must_use] pub fn invoke_system(mut self, initialization_script: String, responder: F) -> Self where - F: Fn(Window, InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static, + F: Fn(Window, String, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static, { self.invoke_initialization_script = initialization_script; - self.invoke_responder = Arc::new(responder); + self.invoke_responder.replace(Arc::new(responder)); self } @@ -1132,10 +1112,7 @@ impl Builder { /// tauri::Builder::default() /// .setup(|app| { /// let main_window = app.get_window("main").unwrap(); - #[cfg_attr( - feature = "dialog", - doc = r#" tauri::api::dialog::blocking::message(Some(&main_window), "Hello", "Welcome back!");"# - )] + /// main_window.set_title("Tauri!"); /// Ok(()) /// }); /// ``` @@ -1176,7 +1153,7 @@ impl Builder { /// } /// pub fn init() -> TauriPlugin { /// PluginBuilder::new("window") - /// .setup(|app| { + /// .setup(|app, api| { /// // initialize the plugin here /// Ok(()) /// }) @@ -1214,6 +1191,7 @@ impl Builder { /// [`State`](crate::State) guard. In particular, if a value of type `T` /// is managed by Tauri, adding `State` to the list of arguments in a /// command handler instructs Tauri to retrieve the managed value. + /// Additionally, [`state`](crate::Manager#method.state) can be used to retrieve the value manually. /// /// # Panics /// @@ -1290,55 +1268,38 @@ impl Builder { let type_name = std::any::type_name::(); assert!( self.state.set(state), - "state for type '{type_name}' is already being managed" + "state for type '{type_name}' is already being managed", ); self } - /// Sets the given system tray to be built before the app runs. - /// - /// Prefer the [`SystemTray#method.build`](crate::SystemTray#method.build) method to create the tray at runtime instead. - /// - /// # Examples - /// ``` - /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu}; - /// - /// tauri::Builder::default() - /// .system_tray(SystemTray::new().with_menu( - /// SystemTrayMenu::new() - /// .add_item(CustomMenuItem::new("quit", "Quit")) - /// .add_item(CustomMenuItem::new("open", "Open")) - /// )); - /// ``` - #[cfg(all(desktop, feature = "system-tray"))] - #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] - #[must_use] - pub fn system_tray(mut self, system_tray: tray::SystemTray) -> Self { - self.system_tray.replace(system_tray); - self - } - /// Sets the menu to use on all windows. /// /// # Examples /// ``` - /// use tauri::{MenuEntry, Submenu, MenuItem, Menu, CustomMenuItem}; + /// use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu}; /// /// tauri::Builder::default() - /// .menu(Menu::with_items([ - /// MenuEntry::Submenu(Submenu::new( + /// .menu(|handle| Menu::with_items(handle, &[ + /// &Submenu::with_items( + /// handle, /// "File", - /// Menu::with_items([ - /// MenuItem::CloseWindow.into(), + /// true, + /// &[ + /// &PredefinedMenuItem::close_window(handle, None), /// #[cfg(target_os = "macos")] - /// CustomMenuItem::new("hello", "Hello").into(), - /// ]), - /// )), + /// &MenuItem::new(handle, "Hello", true, None), + /// ], + /// )? /// ])); /// ``` #[must_use] - pub fn menu(mut self, menu: Menu) -> Self { - self.menu.replace(menu); + #[cfg(desktop)] + pub fn menu) -> crate::Result> + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.menu.replace(Box::new(f)); self } @@ -1346,8 +1307,6 @@ impl Builder { /// /// # Examples /// ``` - /// use tauri::{MenuEntry, Submenu, MenuItem, Menu, CustomMenuItem}; - /// /// tauri::Builder::default() /// .enable_macos_default_menu(false); /// ``` @@ -1357,46 +1316,6 @@ impl Builder { self } - /// Registers a menu event handler for all windows. - /// - /// # Examples - /// ``` - /// use tauri::{Menu, MenuEntry, Submenu, CustomMenuItem, api, Manager}; - /// tauri::Builder::default() - /// .menu(Menu::with_items([ - /// MenuEntry::Submenu(Submenu::new( - /// "File", - /// Menu::with_items([ - /// CustomMenuItem::new("New", "New").into(), - /// CustomMenuItem::new("Learn More", "Learn More").into(), - /// ]), - /// )), - /// ])) - /// .on_menu_event(|event| { - /// match event.menu_item_id() { - /// "Learn More" => { - /// // open in browser (requires the `shell-open-api` feature) - #[cfg_attr( - feature = "shell-open-api", - doc = r#" api::shell::open(&event.window().shell_scope(), "https://github.com/tauri-apps/tauri".to_string(), None).unwrap();"# - )] - /// } - /// id => { - /// // do something with other events - /// println!("got menu event: {}", id); - /// } - /// } - /// }); - /// ``` - #[must_use] - pub fn on_menu_event) + Send + Sync + 'static>( - mut self, - handler: F, - ) -> Self { - self.menu_event_listeners.push(Box::new(handler)); - self - } - /// Registers a window event handler for all windows. /// /// # Examples @@ -1421,37 +1340,6 @@ impl Builder { self } - /// Registers a system tray event handler. - /// - /// Prefer the [`SystemTray#method.on_event`](crate::SystemTray#method.on_event) method when creating a tray at runtime instead. - /// - /// # Examples - /// ``` - /// use tauri::{Manager, SystemTrayEvent}; - /// tauri::Builder::default() - /// .on_system_tray_event(|app, event| match event { - /// // show window with id "main" when the tray is left clicked - /// SystemTrayEvent::LeftClick { .. } => { - /// let window = app.get_window("main").unwrap(); - /// window.show().unwrap(); - /// window.set_focus().unwrap(); - /// } - /// _ => {} - /// }); - /// ``` - #[cfg(all(desktop, feature = "system-tray"))] - #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] - #[must_use] - pub fn on_system_tray_event< - F: Fn(&AppHandle, tray::SystemTrayEvent) + Send + Sync + 'static, - >( - mut self, - handler: F, - ) -> Self { - self.system_tray_event_listeners.push(Box::new(handler)); - self - } - /// Registers a URI scheme protocol available to all webviews. /// Leverages [setURLSchemeHandler](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler) on macOS, /// [AddWebResourceRequestedFilter](https://docs.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.addwebresourcerequestedfilter?view=webview2-dotnet-1.0.774.44) on Windows @@ -1482,42 +1370,6 @@ impl Builder { self } - /// Sets the current platform's target name for the updater. - /// - /// See [`UpdateBuilder::target`](crate::updater::UpdateBuilder#method.target) for more information. - /// - /// # Examples - /// - /// - Use a macOS Universal binary target name: - /// - /// ``` - /// let mut builder = tauri::Builder::default(); - /// #[cfg(target_os = "macos")] - /// { - /// builder = builder.updater_target("darwin-universal"); - /// } - /// ``` - /// - /// - Append debug information to the target: - /// - /// ``` - /// let kind = if cfg!(debug_assertions) { "debug" } else { "release" }; - /// tauri::Builder::default() - /// .updater_target(format!("{}-{kind}", tauri::updater::target().unwrap())); - /// ``` - /// - /// - Use the platform's target triple: - /// - /// ``` - /// tauri::Builder::default() - /// .updater_target(tauri::utils::platform::target_triple().unwrap()); - /// ``` - #[cfg(updater)] - pub fn updater_target>(mut self, target: T) -> Self { - self.updater_settings.target.replace(target.into()); - self - } - /// Change the device event filter mode. /// /// Since the DeviceEvent capture can lead to high CPU usage for unfocused windows, [`tao`] @@ -1545,12 +1397,11 @@ impl Builder { pub fn build(mut self, context: Context) -> crate::Result> { #[cfg(target_os = "macos")] if self.menu.is_none() && self.enable_macos_default_menu { - self.menu = Some(Menu::os_default(&context.package_info().name)); + self.menu = Some(Box::new(|app_handle| { + crate::menu::Menu::default(app_handle) + })); } - #[cfg(shell_scope)] - let shell_scope = context.shell_scope.clone(); - let manager = WindowManager::with_handlers( context, self.plugins, @@ -1559,7 +1410,8 @@ impl Builder { self.uri_scheme_protocols, self.state, self.window_event_listeners, - (self.menu, self.menu_event_listeners), + #[cfg(desktop)] + HashMap::new(), (self.invoke_responder, self.invoke_initialization_script), ); @@ -1567,7 +1419,6 @@ impl Builder { for config in manager.config().tauri.windows.clone() { let label = config.label.clone(); let webview_attributes = WebviewAttributes::from(&config); - self.pending_windows.push(PendingWindow::with_config( config, webview_attributes, @@ -1575,66 +1426,98 @@ impl Builder { )?); } + let runtime_args = RuntimeInitArgs { + #[cfg(windows)] + msg_hook: { + let menus = manager.inner.menus.clone(); + Some(Box::new(move |msg| { + use windows::Win32::UI::WindowsAndMessaging::{TranslateAcceleratorW, HACCEL, MSG}; + unsafe { + let msg = msg as *const MSG; + for menu in menus.lock().unwrap().values() { + let translated = + TranslateAcceleratorW((*msg).hwnd, HACCEL(menu.inner().haccel()), msg); + if translated == 1 { + return true; + } + } + + false + } + })) + }, + }; + #[cfg(any(windows, target_os = "linux"))] let mut runtime = if self.runtime_any_thread { - R::new_any_thread()? + R::new_any_thread(runtime_args)? } else { - R::new()? + R::new(runtime_args)? }; #[cfg(not(any(windows, target_os = "linux")))] - let mut runtime = R::new()?; + let mut runtime = R::new(runtime_args)?; + + #[cfg(desktop)] + { + // setup menu event handler + let proxy = runtime.create_proxy(); + muda::MenuEvent::set_event_handler(Some(move |e: muda::MenuEvent| { + let _ = proxy.send_event(EventLoopMessage::MenuEvent(e.into())); + })); + + // setup tray event handler + #[cfg(feature = "tray-icon")] + { + let proxy = runtime.create_proxy(); + tray_icon::TrayIconEvent::set_event_handler(Some(move |e: tray_icon::TrayIconEvent| { + let _ = proxy.send_event(EventLoopMessage::TrayIconEvent(e.into())); + })); + } + } runtime.set_device_event_filter(self.device_event_filter); let runtime_handle = runtime.handle(); - #[cfg(all(desktop, feature = "global-shortcut"))] - let global_shortcut_manager = runtime.global_shortcut_manager(); - - #[cfg(feature = "clipboard")] - let clipboard_manager = runtime.clipboard_manager(); - + #[allow(unused_mut)] let mut app = App { runtime: Some(runtime), + pending_windows: Some(self.pending_windows), + setup: Some(self.setup), manager: manager.clone(), - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager: global_shortcut_manager.clone(), - #[cfg(feature = "clipboard")] - clipboard_manager: clipboard_manager.clone(), handle: AppHandle { runtime_handle, manager, - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager, - #[cfg(feature = "clipboard")] - clipboard_manager, - #[cfg(updater)] - updater_settings: self.updater_settings, }, }; + #[cfg(desktop)] + if let Some(menu) = self.menu { + let menu = menu(&app.handle)?; + app + .manager + .menus_stash_lock() + .insert(menu.id().clone(), menu.clone()); + + #[cfg(target_os = "macos")] + init_app_menu(&menu)?; + + app.manager.menu_lock().replace(menu); + } + + app.register_core_plugins()?; + let env = Env::default(); + app.manage(env); + app.manage(Scopes { ipc: IpcScope::new(&app.config()), - fs: FsScope::for_fs_api( - &app.manager.config(), - app.package_info(), - &env, - &app.config().tauri.allowlist.fs.scope, - )?, - #[cfg(protocol_asset)] - asset_protocol: FsScope::for_fs_api( - &app.manager.config(), - app.package_info(), - &env, - &app.config().tauri.allowlist.protocol.asset_scope, - )?, - #[cfg(http_request)] - http: crate::scope::HttpScope::for_http_api(&app.config().tauri.allowlist.http.scope), - #[cfg(shell_scope)] - shell: ShellScope::new(&app.manager.config(), app.package_info(), &env, shell_scope), + #[cfg(feature = "protocol-asset")] + asset_protocol: FsScope::for_fs_api(&app, &app.config().tauri.security.asset_protocol.scope)?, }); - app.manage(env); + + app.manage(ChannelDataIpcQueue::default()); + app.handle.plugin(crate::ipc::channel::plugin())?; #[cfg(windows)] { @@ -1646,7 +1529,7 @@ impl Builder { .windows .webview_install_mode { - if let Some(resource_dir) = app.path_resolver().resource_dir() { + if let Ok(resource_dir) = app.path().resource_dir() { std::env::set_var( "WEBVIEW2_BROWSER_EXECUTABLE_FOLDER", resource_dir.join(path), @@ -1660,50 +1543,31 @@ impl Builder { } } - #[cfg(all(desktop, feature = "system-tray"))] - { - if let Some(tray) = self.system_tray { - tray.build(&app)?; - } + let handle = app.handle(); - for listener in self.system_tray_event_listeners { - let app_handle = app.handle(); - let listener = Arc::new(std::sync::Mutex::new(listener)); - app - .runtime - .as_mut() - .unwrap() - .on_system_tray_event(move |tray_id, event| { - if let Some((tray_id, tray)) = app_handle.manager().get_tray_by_runtime_id(tray_id) { - let app_handle = app_handle.clone(); - let event = tray::SystemTrayEvent::from_runtime_event(event, tray_id, &tray.ids); - let listener = listener.clone(); - listener.lock().unwrap()(&app_handle, event); - } - }); + // initialize default tray icon if defined + #[cfg(all(desktop, feature = "tray-icon"))] + { + let config = app.config(); + if let Some(tray_config) = &config.tauri.tray_icon { + let mut tray = TrayIconBuilder::new() + .icon_as_template(tray_config.icon_as_template) + .menu_on_left_click(tray_config.menu_on_left_click); + if let Some(icon) = &app.manager.inner.tray_icon { + tray = tray.icon(icon.clone()); + } + if let Some(title) = &tray_config.title { + tray = tray.title(title); + } + if let Some(tooltip) = &tray_config.tooltip { + tray = tray.tooltip(tooltip); + } + let tray = tray.build(handle)?; + app.manager.inner.tray_icons.lock().unwrap().push(tray); } } - app.manager.initialize_plugins(&app.handle())?; - - let window_labels = self - .pending_windows - .iter() - .map(|p| p.label.clone()) - .collect::>(); - - for pending in self.pending_windows { - let pending = app - .manager - .prepare_window(app.handle.clone(), pending, &window_labels)?; - let detached = app.runtime.as_ref().unwrap().create_window(pending)?; - let _window = app.manager.attach_window(app.handle(), detached); - } - - (self.setup)(&mut app).map_err(|e| crate::Error::Setup(e.into()))?; - - #[cfg(updater)] - app.run_updater(); + app.manager.initialize_plugins(handle)?; Ok(app) } @@ -1715,6 +1579,24 @@ impl Builder { } } +#[cfg(target_os = "macos")] +fn init_app_menu(menu: &Menu) -> crate::Result<()> { + menu.inner().init_for_nsapp(); + + if let Some(window_menu) = menu.get(crate::menu::WINDOW_SUBMENU_ID) { + if let Some(m) = window_menu.as_submenu() { + m.set_as_windows_menu_for_nsapp()?; + } + } + if let Some(help_menu) = menu.get(crate::menu::HELP_SUBMENU_ID) { + if let Some(m) = help_menu.as_submenu() { + m.set_as_help_menu_for_nsapp()?; + } + } + + Ok(()) +} + unsafe impl HasRawDisplayHandle for AppHandle { fn raw_display_handle(&self) -> raw_window_handle::RawDisplayHandle { self.runtime_handle.raw_display_handle() @@ -1727,6 +1609,59 @@ unsafe impl HasRawDisplayHandle for App { } } +fn setup(app: &mut App) -> crate::Result<()> { + let pending_windows = app.pending_windows.take(); + if let Some(pending_windows) = pending_windows { + let window_labels = pending_windows + .iter() + .map(|p| p.label.clone()) + .collect::>(); + + let app_handle = app.handle(); + let manager = app.manager(); + + for pending in pending_windows { + let pending = manager.prepare_window(app_handle.clone(), pending, &window_labels)?; + + #[cfg(desktop)] + let window_menu = app.manager.menu_lock().as_ref().map(|m| WindowMenu { + is_app_wide: true, + menu: m.clone(), + }); + + #[cfg(desktop)] + let handler = manager.prepare_window_menu_creation_handler(window_menu.as_ref()); + #[cfg(not(desktop))] + #[allow(clippy::type_complexity)] + let handler: Option) + Send>> = None; + + let window_effects = pending.webview_attributes.window_effects.clone(); + let detached = if let RuntimeOrDispatch::RuntimeHandle(runtime) = app_handle.runtime() { + runtime.create_window(pending, handler)? + } else { + // the AppHandle's runtime is always RuntimeOrDispatch::RuntimeHandle + unreachable!() + }; + let window = manager.attach_window( + app_handle.clone(), + detached, + #[cfg(desktop)] + None, + ); + + if let Some(effects) = window_effects { + crate::vibrancy::set_window_effects(&window, Some(effects))?; + } + } + } + + if let Some(setup) = app.setup.take() { + (setup)(app).map_err(|e| crate::Error::Setup(e.into()))?; + } + + Ok(()) +} + fn on_event_loop_event, RunEvent) + 'static>( app_handle: &AppHandle, event: RuntimeRunEvent, @@ -1775,7 +1710,64 @@ fn on_event_loop_event, RunEvent) + 'static>( } RuntimeRunEvent::Resumed => RunEvent::Resumed, RuntimeRunEvent::MainEventsCleared => RunEvent::MainEventsCleared, - RuntimeRunEvent::UserEvent(t) => t.into(), + RuntimeRunEvent::UserEvent(t) => { + match t { + #[cfg(desktop)] + EventLoopMessage::MenuEvent(ref e) => { + for listener in &*app_handle + .manager + .inner + .menu_event_listeners + .lock() + .unwrap() + { + listener(app_handle, e.clone()); + } + for (label, listener) in &*app_handle + .manager + .inner + .window_menu_event_listeners + .lock() + .unwrap() + { + if let Some(w) = app_handle.get_window(label) { + listener(&w, e.clone()); + } + } + } + #[cfg(all(desktop, feature = "tray-icon"))] + EventLoopMessage::TrayIconEvent(ref e) => { + for listener in &*app_handle + .manager + .inner + .global_tray_event_listeners + .lock() + .unwrap() + { + listener(app_handle, e.clone()); + } + + for (id, listener) in &*app_handle + .manager + .inner + .tray_event_listeners + .lock() + .unwrap() + { + if e.id == id { + if let Some(tray) = app_handle.tray_by_id(id) { + listener(&tray, e.clone()); + } + } + } + } + } + + #[allow(unreachable_code)] + t.into() + } + #[cfg(any(target_os = "macos", target_os = "ios"))] + RuntimeRunEvent::Opened { urls } => RunEvent::Opened { urls }, _ => unimplemented!(), }; @@ -1812,8 +1804,5 @@ mod tests { crate::test_utils::assert_send::>(); crate::test_utils::assert_sync::>(); } - - crate::test_utils::assert_send::(); - crate::test_utils::assert_sync::(); } } diff --git a/core/tauri/src/app/tray.rs b/core/tauri/src/app/tray.rs deleted file mode 100644 index b88b932c7e3..00000000000 --- a/core/tauri/src/app/tray.rs +++ /dev/null @@ -1,704 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -pub use crate::{ - runtime::{ - menu::{ - MenuHash, MenuId, MenuIdRef, MenuUpdate, SystemTrayMenu, SystemTrayMenuEntry, TrayHandle, - }, - window::dpi::{PhysicalPosition, PhysicalSize}, - RuntimeHandle, SystemTrayEvent as RuntimeSystemTrayEvent, - }, - Icon, Runtime, -}; -use crate::{sealed::RuntimeOrDispatch, Manager}; - -use rand::distributions::{Alphanumeric, DistString}; -use tauri_macros::default_runtime; -use tauri_runtime::TrayId; -use tauri_utils::debug_eprintln; - -use std::{ - collections::{hash_map::DefaultHasher, HashMap}, - fmt, - hash::{Hash, Hasher}, - sync::{Arc, Mutex}, -}; - -type TrayEventHandler = dyn Fn(SystemTrayEvent) + Send + Sync + 'static; - -pub(crate) fn get_menu_ids(map: &mut HashMap, menu: &SystemTrayMenu) { - for item in &menu.items { - match item { - SystemTrayMenuEntry::CustomItem(c) => { - map.insert(c.id, c.id_str.clone()); - } - SystemTrayMenuEntry::Submenu(s) => get_menu_ids(map, &s.inner), - _ => {} - } - } -} - -/// Represents a System Tray instance. -#[derive(Clone)] -#[non_exhaustive] -pub struct SystemTray { - /// The tray identifier. Defaults to a random string. - pub id: String, - /// The tray icon. - pub icon: Option, - /// The tray menu. - pub menu: Option, - /// Whether the icon is a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) icon or not. - #[cfg(target_os = "macos")] - pub icon_as_template: bool, - /// Whether the menu should appear when the tray receives a left click. Defaults to `true` - #[cfg(target_os = "macos")] - pub menu_on_left_click: bool, - on_event: Option>, - // TODO: icon_as_template and menu_on_left_click should be an Option instead :( - #[cfg(target_os = "macos")] - menu_on_left_click_set: bool, - #[cfg(target_os = "macos")] - icon_as_template_set: bool, - #[cfg(target_os = "macos")] - title: Option, - tooltip: Option, -} - -impl fmt::Debug for SystemTray { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut d = f.debug_struct("SystemTray"); - d.field("id", &self.id) - .field("icon", &self.icon) - .field("menu", &self.menu); - #[cfg(target_os = "macos")] - { - d.field("icon_as_template", &self.icon_as_template) - .field("menu_on_left_click", &self.menu_on_left_click); - } - d.finish() - } -} - -impl Default for SystemTray { - fn default() -> Self { - Self { - id: Alphanumeric.sample_string(&mut rand::thread_rng(), 16), - icon: None, - menu: None, - on_event: None, - #[cfg(target_os = "macos")] - icon_as_template: false, - #[cfg(target_os = "macos")] - menu_on_left_click: false, - #[cfg(target_os = "macos")] - icon_as_template_set: false, - #[cfg(target_os = "macos")] - menu_on_left_click_set: false, - #[cfg(target_os = "macos")] - title: None, - tooltip: None, - } - } -} - -impl SystemTray { - /// Creates a new system tray that only renders an icon. - /// - /// # Examples - /// - /// ``` - /// use tauri::SystemTray; - /// - /// tauri::Builder::default() - /// .setup(|app| { - /// let tray_handle = SystemTray::new().build(app)?; - /// Ok(()) - /// }); - /// ``` - pub fn new() -> Self { - Default::default() - } - - pub(crate) fn menu(&self) -> Option<&SystemTrayMenu> { - self.menu.as_ref() - } - - /// Sets the tray identifier, used to retrieve its handle and to identify a tray event source. - /// - /// # Examples - /// - /// ``` - /// use tauri::SystemTray; - /// - /// tauri::Builder::default() - /// .setup(|app| { - /// let tray_handle = SystemTray::new() - /// .with_id("tray-id") - /// .build(app)?; - /// Ok(()) - /// }); - /// ``` - #[must_use] - pub fn with_id>(mut self, id: I) -> Self { - self.id = id.into(); - self - } - - /// Sets the tray [`Icon`]. - /// - /// # Examples - /// - /// ``` - /// use tauri::{Icon, SystemTray}; - /// - /// tauri::Builder::default() - /// .setup(|app| { - /// let tray_handle = SystemTray::new() - /// // dummy and invalid Rgba icon; see the Icon documentation for more information - /// .with_icon(Icon::Rgba { rgba: Vec::new(), width: 0, height: 0 }) - /// .build(app)?; - /// Ok(()) - /// }); - /// ``` - #[must_use] - pub fn with_icon>(mut self, icon: I) -> Self - where - I::Error: std::error::Error, - { - match icon.try_into() { - Ok(icon) => { - self.icon.replace(icon); - } - Err(e) => { - debug_eprintln!("Failed to load tray icon: {}", e); - } - } - self - } - - /// Sets the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). - /// - /// Images you mark as template images should consist of only black and clear colors. - /// You can use the alpha channel in the image to adjust the opacity of black content. - /// - /// # Examples - /// - /// ``` - /// use tauri::SystemTray; - /// - /// tauri::Builder::default() - /// .setup(|app| { - /// let mut tray_builder = SystemTray::new(); - /// #[cfg(target_os = "macos")] - /// { - /// tray_builder = tray_builder.with_icon_as_template(true); - /// } - /// let tray_handle = tray_builder.build(app)?; - /// Ok(()) - /// }); - /// ``` - #[cfg(target_os = "macos")] - #[must_use] - pub fn with_icon_as_template(mut self, is_template: bool) -> Self { - self.icon_as_template_set = true; - self.icon_as_template = is_template; - self - } - - /// Sets whether the menu should appear when the tray receives a left click. Defaults to `true`. - /// - /// # Examples - /// - /// ``` - /// use tauri::SystemTray; - /// - /// tauri::Builder::default() - /// .setup(|app| { - /// let mut tray_builder = SystemTray::new(); - /// #[cfg(target_os = "macos")] - /// { - /// tray_builder = tray_builder.with_menu_on_left_click(false); - /// } - /// let tray_handle = tray_builder.build(app)?; - /// Ok(()) - /// }); - /// ``` - #[cfg(target_os = "macos")] - #[must_use] - pub fn with_menu_on_left_click(mut self, menu_on_left_click: bool) -> Self { - self.menu_on_left_click_set = true; - self.menu_on_left_click = menu_on_left_click; - self - } - - /// Sets the menu title` - /// - /// # Examples - /// - /// ``` - /// use tauri::SystemTray; - /// - /// tauri::Builder::default() - /// .setup(|app| { - /// let mut tray_builder = SystemTray::new(); - /// #[cfg(target_os = "macos")] - /// { - /// tray_builder = tray_builder.with_title("My App"); - /// } - /// let tray_handle = tray_builder.build(app)?; - /// Ok(()) - /// }); - /// ``` - #[cfg(target_os = "macos")] - #[must_use] - pub fn with_title(mut self, title: &str) -> Self { - self.title = Some(title.to_owned()); - 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 - /// - /// ``` - /// use tauri::{Icon, Manager, SystemTray, SystemTrayEvent}; - /// - /// tauri::Builder::default() - /// .setup(|app| { - /// let handle = app.handle(); - /// let id = "tray-id"; - /// SystemTray::new() - /// .with_id(id) - /// .on_event(move |event| { - /// let tray_handle = handle.tray_handle_by_id(id).unwrap(); - /// match event { - /// // show window with id "main" when the tray is left clicked - /// SystemTrayEvent::LeftClick { .. } => { - /// let window = handle.get_window("main").unwrap(); - /// window.show().unwrap(); - /// window.set_focus().unwrap(); - /// } - /// _ => {} - /// } - /// }) - /// .build(app)?; - /// Ok(()) - /// }); - /// ``` - #[must_use] - pub fn on_event(mut self, f: F) -> Self { - self.on_event.replace(Arc::new(f)); - self - } - - /// Sets the menu to show when the system tray is right clicked. - /// - /// # Examples - /// - /// ``` - /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu}; - /// - /// tauri::Builder::default() - /// .setup(|app| { - /// let tray_handle = SystemTray::new() - /// .with_menu( - /// SystemTrayMenu::new() - /// .add_item(CustomMenuItem::new("quit", "Quit")) - /// .add_item(CustomMenuItem::new("open", "Open")) - /// ) - /// .build(app)?; - /// Ok(()) - /// }); - /// ``` - #[must_use] - pub fn with_menu(mut self, menu: SystemTrayMenu) -> Self { - self.menu.replace(menu); - self - } - - /// Builds and shows the system tray. - /// - /// # Examples - /// - /// ``` - /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu}; - /// - /// tauri::Builder::default() - /// .setup(|app| { - /// let tray_handle = SystemTray::new() - /// .with_menu( - /// SystemTrayMenu::new() - /// .add_item(CustomMenuItem::new("quit", "Quit")) - /// .add_item(CustomMenuItem::new("open", "Open")) - /// ) - /// .build(app)?; - /// - /// tray_handle.get_item("quit").set_enabled(false); - /// Ok(()) - /// }); - /// ``` - pub fn build>( - mut self, - manager: &M, - ) -> crate::Result> { - let mut ids = HashMap::new(); - if let Some(menu) = self.menu() { - get_menu_ids(&mut ids, menu); - } - let ids = Arc::new(Mutex::new(ids)); - - if self.icon.is_none() { - if let Some(tray_icon) = &manager.manager().inner.tray_icon { - self = self.with_icon(tray_icon.clone()); - } - } - #[cfg(target_os = "macos")] - { - if !self.icon_as_template_set { - self.icon_as_template = manager - .config() - .tauri - .system_tray - .as_ref() - .map_or(false, |t| t.icon_as_template); - } - if !self.menu_on_left_click_set { - self.menu_on_left_click = manager - .config() - .tauri - .system_tray - .as_ref() - .map_or(false, |t| t.menu_on_left_click); - } - if self.title.is_none() { - self.title = manager - .config() - .tauri - .system_tray - .as_ref() - .and_then(|t| t.title.clone()) - } - } - - let tray_id = self.id.clone(); - - let mut runtime_tray = tauri_runtime::SystemTray::new(); - runtime_tray = runtime_tray.with_id(hash(&self.id)); - if let Some(i) = self.icon { - runtime_tray = runtime_tray.with_icon(i); - } - - if let Some(menu) = self.menu { - runtime_tray = runtime_tray.with_menu(menu); - } - - if let Some(on_event) = self.on_event { - let ids_ = ids.clone(); - let tray_id_ = tray_id.clone(); - runtime_tray = runtime_tray.on_event(move |event| { - on_event(SystemTrayEvent::from_runtime_event( - event, - tray_id_.clone(), - &ids_, - )) - }); - } - - #[cfg(target_os = "macos")] - { - runtime_tray = runtime_tray.with_icon_as_template(self.icon_as_template); - runtime_tray = runtime_tray.with_menu_on_left_click(self.menu_on_left_click); - if let Some(title) = self.title { - runtime_tray = runtime_tray.with_title(&title); - } - } - - 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), - RuntimeOrDispatch::RuntimeHandle(h) => h.system_tray(runtime_tray), - RuntimeOrDispatch::Dispatch(_) => manager - .app_handle() - .runtime_handle - .system_tray(runtime_tray), - }?; - - let tray_handle = SystemTrayHandle { - id, - ids, - inner: tray_handler, - }; - manager.manager().attach_tray(tray_id, tray_handle.clone()); - - Ok(tray_handle) - } -} - -fn hash(id: &str) -> MenuHash { - let mut hasher = DefaultHasher::new(); - id.hash(&mut hasher); - hasher.finish() as MenuHash -} - -/// System tray event. -#[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] -#[non_exhaustive] -pub enum SystemTrayEvent { - /// Tray context menu item was clicked. - #[non_exhaustive] - MenuItemClick { - /// The tray id. - tray_id: String, - /// The id of the menu item. - id: MenuId, - }, - /// Tray icon received a left click. - /// - /// ## Platform-specific - /// - /// - **Linux:** Unsupported - #[non_exhaustive] - LeftClick { - /// The tray id. - tray_id: String, - /// The position of the tray icon. - position: PhysicalPosition, - /// The size of the tray icon. - size: PhysicalSize, - }, - /// Tray icon received a right click. - /// - /// ## Platform-specific - /// - /// - **Linux:** Unsupported - /// - **macOS:** `Ctrl` + `Left click` fire this event. - #[non_exhaustive] - RightClick { - /// The tray id. - tray_id: String, - /// The position of the tray icon. - position: PhysicalPosition, - /// The size of the tray icon. - size: PhysicalSize, - }, - /// Fired when a menu item receive a `Double click` - /// - /// ## Platform-specific - /// - /// - **macOS / Linux:** Unsupported - /// - #[non_exhaustive] - DoubleClick { - /// The tray id. - tray_id: String, - /// The position of the tray icon. - position: PhysicalPosition, - /// The size of the tray icon. - size: PhysicalSize, - }, -} - -impl SystemTrayEvent { - pub(crate) fn from_runtime_event( - event: &RuntimeSystemTrayEvent, - tray_id: String, - menu_ids: &Arc>>, - ) -> Self { - match event { - RuntimeSystemTrayEvent::MenuItemClick(id) => Self::MenuItemClick { - tray_id, - id: menu_ids.lock().unwrap().get(id).unwrap().clone(), - }, - RuntimeSystemTrayEvent::LeftClick { position, size } => Self::LeftClick { - tray_id, - position: *position, - size: *size, - }, - RuntimeSystemTrayEvent::RightClick { position, size } => Self::RightClick { - tray_id, - position: *position, - size: *size, - }, - RuntimeSystemTrayEvent::DoubleClick { position, size } => Self::DoubleClick { - tray_id, - position: *position, - size: *size, - }, - } - } -} - -/// A handle to a system tray. Allows updating the context menu items. -#[default_runtime(crate::Wry, wry)] -#[derive(Debug)] -pub struct SystemTrayHandle { - pub(crate) id: TrayId, - pub(crate) ids: Arc>>, - pub(crate) inner: R::TrayHandler, -} - -impl Clone for SystemTrayHandle { - fn clone(&self) -> Self { - Self { - id: self.id, - ids: self.ids.clone(), - inner: self.inner.clone(), - } - } -} - -/// A handle to a system tray menu item. -#[default_runtime(crate::Wry, wry)] -#[derive(Debug)] -pub struct SystemTrayMenuItemHandle { - id: MenuHash, - tray_handler: R::TrayHandler, -} - -impl Clone for SystemTrayMenuItemHandle { - fn clone(&self) -> Self { - Self { - id: self.id, - tray_handler: self.tray_handler.clone(), - } - } -} - -impl SystemTrayHandle { - /// Gets a handle to the menu item that has the specified `id`. - pub fn get_item(&self, id: MenuIdRef<'_>) -> SystemTrayMenuItemHandle { - let ids = self.ids.lock().unwrap(); - let iter = ids.iter(); - for (raw, item_id) in iter { - if item_id == id { - return SystemTrayMenuItemHandle { - id: *raw, - tray_handler: self.inner.clone(), - }; - } - } - panic!("item id not found") - } - - /// Attempts to get a handle to the menu item that has the specified `id`, return an error if `id` is not found. - pub fn try_get_item(&self, id: MenuIdRef<'_>) -> Option> { - self - .ids - .lock() - .unwrap() - .iter() - .find(|i| i.1 == id) - .map(|i| SystemTrayMenuItemHandle { - id: *i.0, - tray_handler: self.inner.clone(), - }) - } - /// Updates the tray icon. - pub fn set_icon(&self, icon: Icon) -> crate::Result<()> { - self.inner.set_icon(icon.try_into()?).map_err(Into::into) - } - - /// Updates the tray menu. - pub fn set_menu(&self, menu: SystemTrayMenu) -> crate::Result<()> { - let mut ids = HashMap::new(); - get_menu_ids(&mut ids, &menu); - self.inner.set_menu(menu)?; - *self.ids.lock().unwrap() = ids; - Ok(()) - } - - /// Support [macOS tray icon template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) to adjust automatically based on taskbar color. - #[cfg(target_os = "macos")] - pub fn set_icon_as_template(&self, is_template: bool) -> crate::Result<()> { - self - .inner - .set_icon_as_template(is_template) - .map_err(Into::into) - } - - /// Adds the title to the tray menu - #[cfg(target_os = "macos")] - pub fn set_title(&self, title: &str) -> crate::Result<()> { - 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) - } -} - -impl SystemTrayMenuItemHandle { - /// Modifies the enabled state of the menu item. - pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { - self - .tray_handler - .update_item(self.id, MenuUpdate::SetEnabled(enabled)) - .map_err(Into::into) - } - - /// Modifies the title (label) of the menu item. - pub fn set_title>(&self, title: S) -> crate::Result<()> { - self - .tray_handler - .update_item(self.id, MenuUpdate::SetTitle(title.into())) - .map_err(Into::into) - } - - /// Modifies the selected state of the menu item. - pub fn set_selected(&self, selected: bool) -> crate::Result<()> { - self - .tray_handler - .update_item(self.id, MenuUpdate::SetSelected(selected)) - .map_err(Into::into) - } - - /// Sets the native image for this item. - #[cfg(target_os = "macos")] - #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))] - pub fn set_native_image(&self, image: crate::NativeImage) -> crate::Result<()> { - self - .tray_handler - .update_item(self.id, MenuUpdate::SetNativeImage(image)) - .map_err(Into::into) - } -} diff --git a/core/tauri/src/asset_protocol.rs b/core/tauri/src/asset_protocol.rs index 6181a486d76..ec961a9a116 100644 --- a/core/tauri/src/asset_protocol.rs +++ b/core/tauri/src/asset_protocol.rs @@ -2,9 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -#![cfg(protocol_asset)] - -use crate::api::file::SafePathBuf; +use crate::path::SafePathBuf; use crate::scope::FsScope; use rand::RngCore; use std::io::SeekFrom; diff --git a/core/tauri/src/command.rs b/core/tauri/src/command.rs index 7f392d48082..f3da5359dcf 100644 --- a/core/tauri/src/command.rs +++ b/core/tauri/src/command.rs @@ -7,11 +7,14 @@ //! You usually don't need to create these items yourself. These are created from [command](../attr.command.html) //! attribute macro along the way and used by [`crate::generate_handler`] macro. -use crate::hooks::InvokeError; -use crate::InvokeMessage; -use crate::Runtime; -use serde::de::Visitor; -use serde::{Deserialize, Deserializer}; +use crate::{ + ipc::{InvokeBody, InvokeError, InvokeMessage}, + Runtime, +}; +use serde::{ + de::{Error, Visitor}, + Deserialize, Deserializer, +}; /// Represents a custom command. pub struct CommandItem<'a, R: Runtime> { @@ -62,8 +65,6 @@ impl<'de, D: Deserialize<'de>, R: Runtime> CommandArg<'de, R> for D { macro_rules! pass { ($fn:ident, $($arg:ident: $argt:ty),+) => { fn $fn>(self, $($arg: $argt),*) -> Result { - use serde::de::Error; - if self.key.is_empty() { return Err(serde_json::Error::custom(format!( "command {} has an argument with no name with a non-optional value", @@ -71,14 +72,24 @@ macro_rules! pass { ))) } - match self.message.payload.get(self.key) { - Some(value) => value.$fn($($arg),*), - None => { + match &self.message.payload { + InvokeBody::Raw(_body) => { Err(serde_json::Error::custom(format!( - "command {} missing required key {}", + "command {} expected a value for key {} but the IPC call used a bytes payload", self.name, self.key ))) } + InvokeBody::Json(v) => { + match v.get(self.key) { + Some(value) => value.$fn($($arg),*), + None => { + Err(serde_json::Error::custom(format!( + "command {} missing required key {}", + self.name, self.key + ))) + } + } + } } } } @@ -111,9 +122,15 @@ impl<'de, R: Runtime> Deserializer<'de> for CommandItem<'de, R> { pass!(deserialize_byte_buf, visitor: V); fn deserialize_option>(self, visitor: V) -> Result { - match self.message.payload.get(self.key) { - Some(value) => value.deserialize_option(visitor), - None => visitor.visit_none(), + match &self.message.payload { + InvokeBody::Raw(_body) => Err(serde_json::Error::custom(format!( + "command {} expected a value for key {} but the IPC call used a bytes payload", + self.name, self.key + ))), + InvokeBody::Json(v) => match v.get(self.key) { + Some(value) => value.deserialize_option(visitor), + None => visitor.visit_none(), + }, } } @@ -155,46 +172,47 @@ impl<'de, R: Runtime> Deserializer<'de> for CommandItem<'de, R> { /// Nothing in this module is considered stable. #[doc(hidden)] pub mod private { - use crate::{InvokeError, InvokeResolver, Runtime}; + use crate::{ + ipc::{InvokeBody, InvokeError, InvokeResolver, IpcResponse}, + Runtime, + }; use futures_util::{FutureExt, TryFutureExt}; - use serde::Serialize; - use serde_json::Value; use std::future::Future; - // ===== impl Serialize ===== + // ===== impl IpcResponse ===== - pub struct SerializeTag; + pub struct ResponseTag; - pub trait SerializeKind { + pub trait ResponseKind { #[inline(always)] - fn blocking_kind(&self) -> SerializeTag { - SerializeTag + fn blocking_kind(&self) -> ResponseTag { + ResponseTag } #[inline(always)] - fn async_kind(&self) -> SerializeTag { - SerializeTag + fn async_kind(&self) -> ResponseTag { + ResponseTag } } - impl SerializeKind for &T {} + impl ResponseKind for &T {} - impl SerializeTag { + impl ResponseTag { #[inline(always)] pub fn block(self, value: T, resolver: InvokeResolver) where R: Runtime, - T: Serialize, + T: IpcResponse, { resolver.respond(Ok(value)) } #[inline(always)] - pub fn future(self, value: T) -> impl Future> + pub fn future(self, value: T) -> impl Future> where - T: Serialize, + T: IpcResponse, { - std::future::ready(serde_json::to_value(value).map_err(InvokeError::from_serde_json)) + std::future::ready(value.body().map_err(InvokeError::from_error)) } } @@ -214,14 +232,14 @@ pub mod private { } } - impl> ResultKind for Result {} + impl> ResultKind for Result {} impl ResultTag { #[inline(always)] pub fn block(self, value: Result, resolver: InvokeResolver) where R: Runtime, - T: Serialize, + T: IpcResponse, E: Into, { resolver.respond(value.map_err(Into::into)) @@ -231,20 +249,20 @@ pub mod private { pub fn future( self, value: Result, - ) -> impl Future> + ) -> impl Future> where - T: Serialize, + T: IpcResponse, E: Into, { std::future::ready( value .map_err(Into::into) - .and_then(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json)), + .and_then(|value| value.body().map_err(InvokeError::from_error)), ) } } - // ===== Future ===== + // ===== Future ===== pub struct FutureTag; @@ -254,16 +272,16 @@ pub mod private { FutureTag } } - impl> FutureKind for &F {} + impl> FutureKind for &F {} impl FutureTag { #[inline(always)] - pub fn future(self, value: F) -> impl Future> + pub fn future(self, value: F) -> impl Future> where - T: Serialize, + T: IpcResponse, F: Future + Send + 'static, { - value.map(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json)) + value.map(|value| value.body().map_err(InvokeError::from_error)) } } @@ -278,19 +296,22 @@ pub mod private { } } - impl, F: Future>> ResultFutureKind for F {} + impl, F: Future>> ResultFutureKind + for F + { + } impl ResultFutureTag { #[inline(always)] - pub fn future(self, value: F) -> impl Future> + pub fn future(self, value: F) -> impl Future> where - T: Serialize, + T: IpcResponse, E: Into, F: Future> + Send, { - value.err_into().map(|result| { - result.and_then(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json)) - }) + value + .err_into() + .map(|result| result.and_then(|value| value.body().map_err(InvokeError::from_error))) } } } diff --git a/core/tauri/src/endpoints.rs b/core/tauri/src/endpoints.rs deleted file mode 100644 index 5a942a0e956..00000000000 --- a/core/tauri/src/endpoints.rs +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use crate::{ - hooks::{InvokeError, InvokeMessage, InvokeResolver}, - Config, Invoke, PackageInfo, Runtime, Window, -}; -pub use anyhow::Result; -use serde::{Deserialize, Serialize}; -use serde_json::Value as JsonValue; - -use std::sync::Arc; - -mod app; -#[cfg(cli)] -mod cli; -#[cfg(clipboard_any)] -mod clipboard; -#[cfg(dialog_any)] -mod dialog; -mod event; -#[cfg(fs_any)] -mod file_system; -#[cfg(global_shortcut_any)] -mod global_shortcut; -#[cfg(http_any)] -mod http; -mod notification; -#[cfg(os_any)] -mod operating_system; -#[cfg(path_any)] -mod path; -#[cfg(process_any)] -mod process; -#[cfg(shell_any)] -mod shell; -mod window; - -/// The context passed to the invoke handler. -pub struct InvokeContext { - pub window: Window, - pub config: Arc, - pub package_info: PackageInfo, -} - -#[cfg(test)] -impl Clone for InvokeContext { - fn clone(&self) -> Self { - Self { - window: self.window.clone(), - config: self.config.clone(), - package_info: self.package_info.clone(), - } - } -} - -/// The response for a JS `invoke` call. -pub struct InvokeResponse { - json: Result, -} - -impl From for InvokeResponse { - fn from(value: T) -> Self { - Self { - json: serde_json::to_value(value).map_err(Into::into), - } - } -} - -#[derive(Deserialize)] -#[serde(tag = "module", content = "message")] -enum Module { - App(app::Cmd), - #[cfg(process_any)] - Process(process::Cmd), - #[cfg(fs_any)] - Fs(file_system::Cmd), - #[cfg(os_any)] - Os(operating_system::Cmd), - #[cfg(path_any)] - Path(path::Cmd), - Window(Box), - #[cfg(shell_any)] - Shell(shell::Cmd), - Event(event::Cmd), - #[cfg(dialog_any)] - Dialog(dialog::Cmd), - #[cfg(cli)] - Cli(cli::Cmd), - Notification(notification::Cmd), - #[cfg(http_any)] - Http(http::Cmd), - #[cfg(global_shortcut_any)] - GlobalShortcut(global_shortcut::Cmd), - #[cfg(clipboard_any)] - Clipboard(clipboard::Cmd), -} - -impl Module { - fn run( - self, - window: Window, - resolver: InvokeResolver, - config: Arc, - package_info: PackageInfo, - ) { - let context = InvokeContext { - window, - config, - package_info, - }; - match self { - Self::App(cmd) => resolver.respond_async(async move { - cmd - .run(context) - .and_then(|r| r.json) - .map_err(InvokeError::from_anyhow) - }), - #[cfg(process_any)] - Self::Process(cmd) => resolver.respond_async(async move { - cmd - .run(context) - .and_then(|r| r.json) - .map_err(InvokeError::from_anyhow) - }), - #[cfg(fs_any)] - Self::Fs(cmd) => resolver.respond_async(async move { - cmd - .run(context) - .and_then(|r| r.json) - .map_err(InvokeError::from_anyhow) - }), - #[cfg(path_any)] - Self::Path(cmd) => resolver.respond_async(async move { - cmd - .run(context) - .and_then(|r| r.json) - .map_err(InvokeError::from_anyhow) - }), - #[cfg(os_any)] - Self::Os(cmd) => resolver.respond_async(async move { - cmd - .run(context) - .and_then(|r| r.json) - .map_err(InvokeError::from_anyhow) - }), - Self::Window(cmd) => resolver.respond_async(async move { - cmd - .run(context) - .await - .and_then(|r| r.json) - .map_err(InvokeError::from_anyhow) - }), - #[cfg(shell_any)] - Self::Shell(cmd) => resolver.respond_async(async move { - cmd - .run(context) - .and_then(|r| r.json) - .map_err(InvokeError::from_anyhow) - }), - Self::Event(cmd) => resolver.respond_async(async move { - cmd - .run(context) - .and_then(|r| r.json) - .map_err(InvokeError::from_anyhow) - }), - #[cfg(dialog_any)] - Self::Dialog(cmd) => resolver.respond_async(async move { - cmd - .run(context) - .and_then(|r| r.json) - .map_err(InvokeError::from_anyhow) - }), - #[cfg(cli)] - Self::Cli(cmd) => resolver.respond_async(async move { - cmd - .run(context) - .and_then(|r| r.json) - .map_err(InvokeError::from_anyhow) - }), - Self::Notification(cmd) => resolver.respond_async(async move { - cmd - .run(context) - .and_then(|r| r.json) - .map_err(InvokeError::from_anyhow) - }), - #[cfg(http_any)] - Self::Http(cmd) => resolver.respond_async(async move { - cmd - .run(context) - .await - .and_then(|r| r.json) - .map_err(InvokeError::from_anyhow) - }), - #[cfg(global_shortcut_any)] - Self::GlobalShortcut(cmd) => resolver.respond_async(async move { - cmd - .run(context) - .and_then(|r| r.json) - .map_err(InvokeError::from_anyhow) - }), - #[cfg(clipboard_any)] - Self::Clipboard(cmd) => resolver.respond_async(async move { - cmd - .run(context) - .and_then(|r| r.json) - .map_err(InvokeError::from_anyhow) - }), - } - } -} - -pub(crate) fn handle( - module: String, - invoke: Invoke, - config: Arc, - package_info: &PackageInfo, -) { - let Invoke { message, resolver } = invoke; - let InvokeMessage { - mut payload, - window, - .. - } = message; - - if let JsonValue::Object(ref mut obj) = payload { - obj.insert("module".to_string(), JsonValue::String(module.clone())); - } - - match serde_json::from_value::(payload) { - Ok(module) => module.run(window, resolver, config, package_info.clone()), - Err(e) => { - let message = e.to_string(); - if message.starts_with("unknown variant") { - let mut s = message.split('`'); - s.next(); - if let Some(unknown_variant_name) = s.next() { - if unknown_variant_name == module { - return resolver.reject(format!( - "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()); - } - } - } - resolver.reject(message); - } - } -} diff --git a/core/tauri/src/endpoints/app.rs b/core/tauri/src/endpoints/app.rs deleted file mode 100644 index 32191eb6dd3..00000000000 --- a/core/tauri/src/endpoints/app.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use super::InvokeContext; -use crate::Runtime; -use serde::Deserialize; -use tauri_macros::{command_enum, module_command_handler, CommandModule}; - -/// The API descriptor. -#[command_enum] -#[derive(Deserialize, CommandModule)] -#[serde(tag = "cmd", rename_all = "camelCase")] -#[allow(clippy::enum_variant_names)] -pub enum Cmd { - /// Get Application Version - GetAppVersion, - /// Get Application Name - GetAppName, - /// Get Tauri Version - GetTauriVersion, - /// Shows the application on macOS. - #[cmd(app_show, "app > show")] - Show, - /// Hides the application on macOS. - #[cmd(app_hide, "app > hide")] - Hide, -} - -impl Cmd { - fn get_app_version(context: InvokeContext) -> super::Result { - Ok(context.package_info.version.to_string()) - } - - fn get_app_name(context: InvokeContext) -> super::Result { - Ok(context.package_info.name) - } - - fn get_tauri_version(_context: InvokeContext) -> super::Result<&'static str> { - Ok(crate::VERSION) - } - - #[module_command_handler(app_show)] - #[allow(unused_variables)] - fn show(context: InvokeContext) -> super::Result<()> { - #[cfg(target_os = "macos")] - context.window.app_handle.show()?; - Ok(()) - } - - #[module_command_handler(app_hide)] - #[allow(unused_variables)] - fn hide(context: InvokeContext) -> super::Result<()> { - #[cfg(target_os = "macos")] - context.window.app_handle.hide()?; - Ok(()) - } -} diff --git a/core/tauri/src/endpoints/cli.rs b/core/tauri/src/endpoints/cli.rs deleted file mode 100644 index 1c7c490a934..00000000000 --- a/core/tauri/src/endpoints/cli.rs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![allow(unused_imports)] - -use super::{InvokeContext, InvokeResponse}; -use crate::Runtime; -use serde::Deserialize; -use tauri_macros::{command_enum, module_command_handler, CommandModule}; - -/// The API descriptor. -#[command_enum] -#[derive(CommandModule, Deserialize)] -#[serde(tag = "cmd", rename_all = "camelCase")] -pub enum Cmd { - /// The get CLI matches API. - CliMatches, -} - -impl Cmd { - #[module_command_handler(cli)] - fn cli_matches(context: InvokeContext) -> super::Result { - if let Some(cli) = &context.config.tauri.cli { - crate::api::cli::get_matches(cli, &context.package_info) - .map(Into::into) - .map_err(Into::into) - } else { - Ok(crate::api::cli::Matches::default().into()) - } - } - - #[cfg(not(cli))] - fn cli_matches(_: InvokeContext) -> super::Result { - Err(crate::error::into_anyhow("CLI definition not set under tauri.conf.json > tauri > cli (https://tauri.app/docs/api/config#tauri.cli)")) - } -} - -#[cfg(test)] -mod tests { - #[tauri_macros::module_command_test(cli, "CLI definition not set under tauri.conf.json > tauri > cli (https://tauri.app/docs/api/config#tauri.cli)", runtime)] - #[quickcheck_macros::quickcheck] - fn cli_matches() { - let res = super::Cmd::cli_matches(crate::test::mock_invoke_context()); - crate::test_utils::assert_not_allowlist_error(res); - } -} diff --git a/core/tauri/src/endpoints/clipboard.rs b/core/tauri/src/endpoints/clipboard.rs deleted file mode 100644 index 0eb31b71229..00000000000 --- a/core/tauri/src/endpoints/clipboard.rs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![allow(unused_imports)] - -use super::InvokeContext; -#[cfg(any(clipboard_write_text, clipboard_read_text))] -use crate::runtime::ClipboardManager; -use crate::Runtime; -use serde::Deserialize; -use tauri_macros::{command_enum, module_command_handler, CommandModule}; - -/// The API descriptor. -#[command_enum] -#[derive(Deserialize, CommandModule)] -#[serde(tag = "cmd", content = "data", rename_all = "camelCase")] -pub enum Cmd { - /// Write a text string to the clipboard. - #[cmd(clipboard_write_text, "clipboard > writeText")] - WriteText(String), - /// Read clipboard content as text. - ReadText, -} - -impl Cmd { - #[module_command_handler(clipboard_write_text)] - fn write_text(context: InvokeContext, text: String) -> super::Result<()> { - context - .window - .app_handle - .clipboard_manager() - .write_text(text) - .map_err(crate::error::into_anyhow) - } - - #[module_command_handler(clipboard_read_text)] - fn read_text(context: InvokeContext) -> super::Result> { - context - .window - .app_handle - .clipboard_manager() - .read_text() - .map_err(crate::error::into_anyhow) - } - - #[cfg(not(clipboard_read_text))] - fn read_text(_: InvokeContext) -> super::Result<()> { - Err(crate::Error::ApiNotAllowlisted("clipboard > readText".into()).into_anyhow()) - } -} - -#[cfg(test)] -mod tests { - #[tauri_macros::module_command_test(clipboard_write_text, "clipboard > writeText")] - #[quickcheck_macros::quickcheck] - fn write_text(text: String) { - let ctx = crate::test::mock_invoke_context(); - super::Cmd::write_text(ctx.clone(), text.clone()).unwrap(); - #[cfg(clipboard_read_text)] - assert_eq!(super::Cmd::read_text(ctx).unwrap(), Some(text)); - } - - #[tauri_macros::module_command_test(clipboard_read_text, "clipboard > readText", runtime)] - #[quickcheck_macros::quickcheck] - fn read_text() { - let ctx = crate::test::mock_invoke_context(); - assert_eq!(super::Cmd::read_text(ctx.clone()).unwrap(), None); - #[cfg(clipboard_write_text)] - { - let text = "Tauri!".to_string(); - super::Cmd::write_text(ctx.clone(), text.clone()).unwrap(); - assert_eq!(super::Cmd::read_text(ctx).unwrap(), Some(text)); - } - } -} diff --git a/core/tauri/src/endpoints/dialog.rs b/core/tauri/src/endpoints/dialog.rs deleted file mode 100644 index 137c8434dd4..00000000000 --- a/core/tauri/src/endpoints/dialog.rs +++ /dev/null @@ -1,354 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![allow(unused_imports)] - -use super::{InvokeContext, InvokeResponse}; -use crate::Runtime; -#[cfg(any(dialog_open, dialog_save))] -use crate::{api::dialog::blocking::FileDialogBuilder, Manager, Scopes}; -use serde::{Deserialize, Deserializer}; -use tauri_macros::{command_enum, module_command_handler, CommandModule}; - -use std::path::PathBuf; - -macro_rules! message_dialog { - ($fn_name: ident, $allowlist: ident, $button_labels_type: ty, $buttons: expr) => { - #[module_command_handler($allowlist)] - fn $fn_name( - context: InvokeContext, - title: Option, - message: String, - level: Option, - button_labels: $button_labels_type, - ) -> super::Result { - 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(determine_button(button_labels)); - #[cfg(any(windows, target_os = "macos"))] - { - builder = builder.parent(&context.window); - } - if let Some(level) = level { - builder = builder.kind(level.into()); - } - Ok(builder.show()) - } - }; -} - -#[allow(dead_code)] -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DialogFilter { - name: String, - extensions: Vec, -} - -/// The options for the open dialog API. -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct OpenDialogOptions { - /// The title of the dialog window. - pub title: Option, - /// The filters of the dialog. - #[serde(default)] - pub filters: Vec, - /// Whether the dialog allows multiple selection or not. - #[serde(default)] - pub multiple: bool, - /// Whether the dialog is a directory selection (`true` value) or file selection (`false` value). - #[serde(default)] - pub directory: bool, - /// The initial path of the dialog. - pub default_path: Option, - /// If [`Self::directory`] is true, indicates that it will be read recursively later. - /// Defines whether subdirectories will be allowed on the scope or not. - #[serde(default)] - pub recursive: bool, -} - -/// The options for the save dialog API. -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SaveDialogOptions { - /// The title of the dialog window. - pub title: Option, - /// The filters of the dialog. - #[serde(default)] - pub filters: Vec, - /// The initial path of the dialog. - pub default_path: Option, -} - -/// Types of message, ask and confirm dialogs. -#[non_exhaustive] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum MessageDialogType { - /// Information dialog. - Info, - /// Warning dialog. - Warning, - /// Error dialog. - Error, -} - -impl<'de> Deserialize<'de> for MessageDialogType { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - Ok(match s.to_lowercase().as_str() { - "info" => MessageDialogType::Info, - "warning" => MessageDialogType::Warning, - "error" => MessageDialogType::Error, - _ => MessageDialogType::Info, - }) - } -} - -#[cfg(any(dialog_message, dialog_ask, dialog_confirm))] -impl From for crate::api::dialog::MessageDialogKind { - fn from(kind: MessageDialogType) -> Self { - match kind { - MessageDialogType::Info => Self::Info, - MessageDialogType::Warning => Self::Warning, - MessageDialogType::Error => Self::Error, - } - } -} - -/// The API descriptor. -#[command_enum] -#[derive(Deserialize, CommandModule)] -#[serde(tag = "cmd", rename_all = "camelCase")] -#[allow(clippy::enum_variant_names)] -pub enum Cmd { - /// The open dialog API. - #[cmd(dialog_open, "dialog > open")] - OpenDialog { options: OpenDialogOptions }, - /// The save dialog API. - #[cmd(dialog_save, "dialog > save")] - SaveDialog { options: SaveDialogOptions }, - #[cmd(dialog_message, "dialog > message")] - MessageDialog { - title: Option, - message: String, - #[serde(rename = "type")] - level: Option, - #[serde(rename = "buttonLabel")] - button_label: Option, - }, - #[cmd(dialog_ask, "dialog > ask")] - AskDialog { - title: Option, - message: String, - #[serde(rename = "type")] - level: Option, - #[serde(rename = "buttonLabels")] - button_label: Option<(String, String)>, - }, - #[cmd(dialog_confirm, "dialog > confirm")] - ConfirmDialog { - title: Option, - message: String, - #[serde(rename = "type")] - level: Option, - #[serde(rename = "buttonLabels")] - button_labels: Option<(String, String)>, - }, -} - -impl Cmd { - #[module_command_handler(dialog_open)] - #[allow(unused_variables)] - fn open_dialog( - context: InvokeContext, - options: OpenDialogOptions, - ) -> super::Result { - let mut dialog_builder = FileDialogBuilder::new(); - #[cfg(any(windows, target_os = "macos"))] - { - dialog_builder = dialog_builder.set_parent(&context.window); - } - if let Some(title) = options.title { - dialog_builder = dialog_builder.set_title(&title); - } - if let Some(default_path) = options.default_path { - dialog_builder = set_default_path(dialog_builder, default_path); - } - for filter in options.filters { - let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect(); - dialog_builder = dialog_builder.add_filter(filter.name, &extensions); - } - - let scopes = context.window.state::(); - - let res = if options.directory { - if options.multiple { - let folders = dialog_builder.pick_folders(); - if let Some(folders) = &folders { - for folder in folders { - scopes - .allow_directory(folder, options.recursive) - .map_err(crate::error::into_anyhow)?; - } - } - folders.into() - } else { - let folder = dialog_builder.pick_folder(); - if let Some(path) = &folder { - scopes - .allow_directory(path, options.recursive) - .map_err(crate::error::into_anyhow)?; - } - folder.into() - } - } else if options.multiple { - let files = dialog_builder.pick_files(); - if let Some(files) = &files { - for file in files { - scopes.allow_file(file).map_err(crate::error::into_anyhow)?; - } - } - files.into() - } else { - let file = dialog_builder.pick_file(); - if let Some(file) = &file { - scopes.allow_file(file).map_err(crate::error::into_anyhow)?; - } - file.into() - }; - - Ok(res) - } - - #[module_command_handler(dialog_save)] - #[allow(unused_variables)] - fn save_dialog( - context: InvokeContext, - options: SaveDialogOptions, - ) -> super::Result> { - let mut dialog_builder = FileDialogBuilder::new(); - #[cfg(any(windows, target_os = "macos"))] - { - dialog_builder = dialog_builder.set_parent(&context.window); - } - if let Some(title) = options.title { - dialog_builder = dialog_builder.set_title(&title); - } - if let Some(default_path) = options.default_path { - dialog_builder = set_default_path(dialog_builder, default_path); - } - for filter in options.filters { - let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect(); - dialog_builder = dialog_builder.add_filter(filter.name, &extensions); - } - - let scopes = context.window.state::(); - - let path = dialog_builder.save_file(); - if let Some(p) = &path { - scopes.allow_file(p).map_err(crate::error::into_anyhow)?; - } - - Ok(path) - } - - message_dialog!( - message_dialog, - dialog_message, - Option, - |label: Option| { - label - .map(crate::api::dialog::MessageDialogButtons::OkWithLabel) - .unwrap_or(crate::api::dialog::MessageDialogButtons::Ok) - } - ); - - message_dialog!( - ask_dialog, - dialog_ask, - 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, - 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) - } - ); -} - -#[cfg(any(dialog_open, dialog_save))] -fn set_default_path( - mut dialog_builder: FileDialogBuilder, - default_path: PathBuf, -) -> FileDialogBuilder { - if default_path.is_file() || !default_path.exists() { - if let (Some(parent), Some(file_name)) = (default_path.parent(), default_path.file_name()) { - if parent.components().count() > 0 { - dialog_builder = dialog_builder.set_directory(parent); - } - dialog_builder = dialog_builder.set_file_name(&file_name.to_string_lossy()); - } else { - dialog_builder = dialog_builder.set_directory(default_path); - } - dialog_builder - } else { - dialog_builder.set_directory(default_path) - } -} - -#[cfg(test)] -mod tests { - use super::{OpenDialogOptions, SaveDialogOptions}; - use quickcheck::{Arbitrary, Gen}; - - impl Arbitrary for OpenDialogOptions { - fn arbitrary(g: &mut Gen) -> Self { - Self { - filters: Vec::new(), - multiple: bool::arbitrary(g), - directory: bool::arbitrary(g), - default_path: Option::arbitrary(g), - title: Option::arbitrary(g), - recursive: bool::arbitrary(g), - } - } - } - - impl Arbitrary for SaveDialogOptions { - fn arbitrary(g: &mut Gen) -> Self { - Self { - filters: Vec::new(), - default_path: Option::arbitrary(g), - title: Option::arbitrary(g), - } - } - } - - #[tauri_macros::module_command_test(dialog_open, "dialog > open")] - #[quickcheck_macros::quickcheck] - fn open_dialog(_options: OpenDialogOptions) {} - - #[tauri_macros::module_command_test(dialog_save, "dialog > save")] - #[quickcheck_macros::quickcheck] - fn save_dialog(_options: SaveDialogOptions) {} -} diff --git a/core/tauri/src/endpoints/event.rs b/core/tauri/src/endpoints/event.rs deleted file mode 100644 index 471db9c3c08..00000000000 --- a/core/tauri/src/endpoints/event.rs +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![allow(unused_imports)] - -use super::InvokeContext; -use crate::{ - api::ipc::CallbackFn, - event::is_event_name_valid, - event::{listen_js, unlisten_js}, - runtime::window::is_label_valid, - sealed::ManagerBase, - Manager, Runtime, -}; -use serde::{de::Deserializer, Deserialize}; -use serde_json::Value as JsonValue; -use tauri_macros::{command_enum, CommandModule}; - -pub struct EventId(String); - -impl<'de> Deserialize<'de> for EventId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let event_id = String::deserialize(deserializer)?; - if is_event_name_valid(&event_id) { - Ok(EventId(event_id)) - } else { - Err(serde::de::Error::custom( - "Event name must include only alphanumeric characters, `-`, `/`, `:` and `_`.", - )) - } - } -} - -pub struct WindowLabel(String); - -impl<'de> Deserialize<'de> for WindowLabel { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let event_id = String::deserialize(deserializer)?; - if is_label_valid(&event_id) { - Ok(WindowLabel(event_id)) - } else { - Err(serde::de::Error::custom( - "Window label must include only alphanumeric characters, `-`, `/`, `:` and `_`.", - )) - } - } -} - -/// The API descriptor. -#[command_enum] -#[derive(Deserialize, CommandModule)] -#[serde(tag = "cmd", rename_all = "camelCase")] -pub enum Cmd { - /// Listen to an event. - #[serde(rename_all = "camelCase")] - Listen { - event: EventId, - window_label: Option, - handler: CallbackFn, - }, - /// Unlisten to an event. - #[serde(rename_all = "camelCase")] - Unlisten { event: EventId, event_id: u32 }, - /// Emit an event to the webview associated with the given window. - /// If the window_label is omitted, the event will be triggered on all listeners. - #[serde(rename_all = "camelCase")] - Emit { - event: EventId, - window_label: Option, - payload: Option, - }, -} - -impl Cmd { - fn listen( - context: InvokeContext, - event: EventId, - window_label: Option, - handler: CallbackFn, - ) -> super::Result { - let event_id = rand::random(); - - let window_label = window_label.map(|l| l.0); - - context - .window - .eval(&listen_js( - context.window.manager().event_listeners_object_name(), - format!("'{}'", event.0), - event_id, - window_label.clone(), - format!("window['_{}']", handler.0), - )) - .map_err(crate::error::into_anyhow)?; - - context - .window - .register_js_listener(window_label, event.0, event_id); - - Ok(event_id) - } - - fn unlisten( - context: InvokeContext, - event: EventId, - event_id: u32, - ) -> super::Result<()> { - context - .window - .eval(&unlisten_js( - context.window.manager().event_listeners_object_name(), - event.0, - event_id, - )) - .map_err(crate::error::into_anyhow)?; - context.window.unregister_js_listener(event_id); - Ok(()) - } - - fn emit( - context: InvokeContext, - event: EventId, - window_label: Option, - payload: Option, - ) -> super::Result<()> { - // dispatch the event to Rust listeners - context.window.trigger( - &event.0, - // TODO: dispatch any serializable value instead of a string in v2 - payload.as_ref().and_then(|p| { - serde_json::to_string(&p) - .map_err(|e| { - #[cfg(debug_assertions)] - eprintln!("{e}"); - e - }) - .ok() - }), - ); - - if let Some(target) = window_label { - context - .window - .emit_to(&target.0, &event.0, payload) - .map_err(crate::error::into_anyhow)?; - } else { - context - .window - .emit_all(&event.0, payload) - .map_err(crate::error::into_anyhow)?; - } - Ok(()) - } -} diff --git a/core/tauri/src/endpoints/file_system.rs b/core/tauri/src/endpoints/file_system.rs deleted file mode 100644 index ccfabde77b9..00000000000 --- a/core/tauri/src/endpoints/file_system.rs +++ /dev/null @@ -1,506 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![allow(unused_imports)] - -use crate::{ - api::{ - dir, - file::{self, SafePathBuf}, - path::BaseDirectory, - }, - scope::Scopes, - Config, Env, Manager, PackageInfo, Runtime, Window, -}; - -use super::InvokeContext; -#[allow(unused_imports)] -use anyhow::Context; -use serde::{ - de::{Deserializer, Error as DeError}, - Deserialize, Serialize, -}; -use tauri_macros::{command_enum, module_command_handler, CommandModule}; - -use std::fmt::{Debug, Formatter}; -use std::{ - fs, - fs::File, - io::Write, - path::{Component, Path}, - sync::Arc, -}; - -/// The options for the directory functions on the file system API. -#[derive(Debug, Clone, Deserialize)] -pub struct DirOperationOptions { - /// Whether the API should recursively perform the operation on the directory. - #[serde(default)] - pub recursive: bool, - /// The base directory of the operation. - /// The directory path of the BaseDirectory will be the prefix of the defined directory path. - pub dir: Option, -} - -/// The options for the file functions on the file system API. -#[derive(Debug, Clone, Deserialize)] -pub struct FileOperationOptions { - /// The base directory of the operation. - /// The directory path of the BaseDirectory will be the prefix of the defined file path. - pub dir: Option, -} - -/// The API descriptor. -#[command_enum] -#[derive(Deserialize, CommandModule)] -#[serde(tag = "cmd", rename_all = "camelCase")] -pub(crate) enum Cmd { - /// The read binary file API. - #[cmd(fs_read_file, "fs > readFile")] - ReadFile { - path: SafePathBuf, - options: Option, - }, - /// The read binary file API. - #[cmd(fs_read_file, "fs > readFile")] - ReadTextFile { - path: SafePathBuf, - options: Option, - }, - /// The write file API. - #[cmd(fs_write_file, "fs > writeFile")] - WriteFile { - path: SafePathBuf, - contents: Vec, - options: Option, - }, - /// The read dir API. - #[cmd(fs_read_dir, "fs > readDir")] - ReadDir { - path: SafePathBuf, - options: Option, - }, - /// The copy file API. - #[cmd(fs_copy_file, "fs > copyFile")] - CopyFile { - source: SafePathBuf, - destination: SafePathBuf, - options: Option, - }, - /// The create dir API. - #[cmd(fs_create_dir, "fs > createDir")] - CreateDir { - path: SafePathBuf, - options: Option, - }, - /// The remove dir API. - #[cmd(fs_remove_dir, "fs > removeDir")] - RemoveDir { - path: SafePathBuf, - options: Option, - }, - /// The remove file API. - #[cmd(fs_remove_file, "fs > removeFile")] - RemoveFile { - path: SafePathBuf, - options: Option, - }, - /// The rename file API. - #[cmd(fs_rename_file, "fs > renameFile")] - #[serde(rename_all = "camelCase")] - RenameFile { - old_path: SafePathBuf, - new_path: SafePathBuf, - options: Option, - }, - /// The exists API. - #[cmd(fs_exists, "fs > exists")] - Exists { - path: SafePathBuf, - options: Option, - }, -} - -impl Cmd { - #[module_command_handler(fs_read_file)] - fn read_file( - context: InvokeContext, - path: SafePathBuf, - options: Option, - ) -> super::Result> { - let resolved_path = resolve_path( - &context.config, - &context.package_info, - &context.window, - path, - options.and_then(|o| o.dir), - )?; - file::read_binary(&resolved_path) - .with_context(|| format!("path: {}", resolved_path.display())) - .map_err(Into::into) - } - - #[module_command_handler(fs_read_file)] - fn read_text_file( - context: InvokeContext, - path: SafePathBuf, - options: Option, - ) -> super::Result { - let resolved_path = resolve_path( - &context.config, - &context.package_info, - &context.window, - path, - options.and_then(|o| o.dir), - )?; - file::read_string(&resolved_path) - .with_context(|| format!("path: {}", resolved_path.display())) - .map_err(Into::into) - } - - #[module_command_handler(fs_write_file)] - fn write_file( - context: InvokeContext, - path: SafePathBuf, - contents: Vec, - options: Option, - ) -> super::Result<()> { - let resolved_path = resolve_path( - &context.config, - &context.package_info, - &context.window, - path, - options.and_then(|o| o.dir), - )?; - File::create(&resolved_path) - .with_context(|| format!("path: {}", resolved_path.display())) - .map_err(Into::into) - .and_then(|mut f| f.write_all(&contents).map_err(|err| err.into())) - } - - #[module_command_handler(fs_read_dir)] - fn read_dir( - context: InvokeContext, - path: SafePathBuf, - options: Option, - ) -> super::Result> { - let (recursive, dir) = if let Some(options_value) = options { - (options_value.recursive, options_value.dir) - } else { - (false, None) - }; - let resolved_path = resolve_path( - &context.config, - &context.package_info, - &context.window, - path, - dir, - )?; - dir::read_dir_with_options( - &resolved_path, - recursive, - dir::ReadDirOptions { - scope: Some(&context.window.state::().fs), - }, - ) - .with_context(|| format!("path: {}", resolved_path.display())) - .map_err(Into::into) - } - - #[module_command_handler(fs_copy_file)] - fn copy_file( - context: InvokeContext, - source: SafePathBuf, - destination: SafePathBuf, - options: Option, - ) -> super::Result<()> { - let (src, dest) = match options.and_then(|o| o.dir) { - Some(dir) => ( - resolve_path( - &context.config, - &context.package_info, - &context.window, - source, - Some(dir), - )?, - resolve_path( - &context.config, - &context.package_info, - &context.window, - destination, - Some(dir), - )?, - ), - None => (source, destination), - }; - fs::copy(src.clone(), dest.clone()) - .with_context(|| format!("source: {}, dest: {}", src.display(), dest.display()))?; - Ok(()) - } - - #[module_command_handler(fs_create_dir)] - fn create_dir( - context: InvokeContext, - path: SafePathBuf, - options: Option, - ) -> super::Result<()> { - let (recursive, dir) = if let Some(options_value) = options { - (options_value.recursive, options_value.dir) - } else { - (false, None) - }; - let resolved_path = resolve_path( - &context.config, - &context.package_info, - &context.window, - path, - dir, - )?; - if recursive { - fs::create_dir_all(&resolved_path) - .with_context(|| format!("path: {}", resolved_path.display()))?; - } else { - fs::create_dir(&resolved_path) - .with_context(|| format!("path: {} (non recursive)", resolved_path.display()))?; - } - - Ok(()) - } - - #[module_command_handler(fs_remove_dir)] - fn remove_dir( - context: InvokeContext, - path: SafePathBuf, - options: Option, - ) -> super::Result<()> { - let (recursive, dir) = if let Some(options_value) = options { - (options_value.recursive, options_value.dir) - } else { - (false, None) - }; - let resolved_path = resolve_path( - &context.config, - &context.package_info, - &context.window, - path, - dir, - )?; - if recursive { - fs::remove_dir_all(&resolved_path) - .with_context(|| format!("path: {}", resolved_path.display()))?; - } else { - fs::remove_dir(&resolved_path) - .with_context(|| format!("path: {} (non recursive)", resolved_path.display()))?; - } - - Ok(()) - } - - #[module_command_handler(fs_remove_file)] - fn remove_file( - context: InvokeContext, - path: SafePathBuf, - options: Option, - ) -> super::Result<()> { - let resolved_path = resolve_path( - &context.config, - &context.package_info, - &context.window, - path, - options.and_then(|o| o.dir), - )?; - fs::remove_file(&resolved_path) - .with_context(|| format!("path: {}", resolved_path.display()))?; - Ok(()) - } - - #[module_command_handler(fs_rename_file)] - fn rename_file( - context: InvokeContext, - old_path: SafePathBuf, - new_path: SafePathBuf, - options: Option, - ) -> super::Result<()> { - let (old, new) = match options.and_then(|o| o.dir) { - Some(dir) => ( - resolve_path( - &context.config, - &context.package_info, - &context.window, - old_path, - Some(dir), - )?, - resolve_path( - &context.config, - &context.package_info, - &context.window, - new_path, - Some(dir), - )?, - ), - None => (old_path, new_path), - }; - fs::rename(&old, &new) - .with_context(|| format!("old: {}, new: {}", old.display(), new.display())) - .map_err(Into::into) - } - - #[module_command_handler(fs_exists)] - fn exists( - context: InvokeContext, - path: SafePathBuf, - options: Option, - ) -> super::Result { - let resolved_path = resolve_path( - &context.config, - &context.package_info, - &context.window, - path, - options.and_then(|o| o.dir), - )?; - Ok(resolved_path.as_ref().exists()) - } -} - -#[allow(dead_code)] -fn resolve_path( - config: &Config, - package_info: &PackageInfo, - window: &Window, - path: SafePathBuf, - dir: Option, -) -> super::Result { - let env = window.state::().inner(); - match crate::api::path::resolve_path(config, package_info, env, &path, dir) { - Ok(path) => { - if window.state::().fs.is_allowed(&path) { - Ok( - // safety: the path is resolved by Tauri so it is safe - unsafe { SafePathBuf::new_unchecked(path) }, - ) - } else { - Err(anyhow::anyhow!( - crate::Error::PathNotAllowed(path).to_string() - )) - } - } - Err(e) => super::Result::::Err(e.into()) - .with_context(|| format!("path: {}, base dir: {dir:?}", path.display())), - } -} - -#[cfg(test)] -mod tests { - use super::{BaseDirectory, DirOperationOptions, FileOperationOptions, SafePathBuf}; - - use quickcheck::{Arbitrary, Gen}; - - impl Arbitrary for BaseDirectory { - fn arbitrary(g: &mut Gen) -> Self { - if bool::arbitrary(g) { - BaseDirectory::AppData - } else { - BaseDirectory::Resource - } - } - } - - impl Arbitrary for FileOperationOptions { - fn arbitrary(g: &mut Gen) -> Self { - Self { - dir: Option::arbitrary(g), - } - } - } - - impl Arbitrary for DirOperationOptions { - fn arbitrary(g: &mut Gen) -> Self { - Self { - recursive: bool::arbitrary(g), - dir: Option::arbitrary(g), - } - } - } - - #[tauri_macros::module_command_test(fs_read_file, "fs > readFile")] - #[quickcheck_macros::quickcheck] - fn read_file(path: SafePathBuf, options: Option) { - let res = super::Cmd::read_file(crate::test::mock_invoke_context(), path, options); - crate::test_utils::assert_not_allowlist_error(res); - } - - #[tauri_macros::module_command_test(fs_write_file, "fs > writeFile")] - #[quickcheck_macros::quickcheck] - fn write_file(path: SafePathBuf, contents: Vec, options: Option) { - let res = super::Cmd::write_file(crate::test::mock_invoke_context(), path, contents, options); - crate::test_utils::assert_not_allowlist_error(res); - } - - #[tauri_macros::module_command_test(fs_read_dir, "fs > readDir")] - #[quickcheck_macros::quickcheck] - fn read_dir(path: SafePathBuf, options: Option) { - let res = super::Cmd::read_dir(crate::test::mock_invoke_context(), path, options); - crate::test_utils::assert_not_allowlist_error(res); - } - - #[tauri_macros::module_command_test(fs_copy_file, "fs > copyFile")] - #[quickcheck_macros::quickcheck] - fn copy_file( - source: SafePathBuf, - destination: SafePathBuf, - options: Option, - ) { - let res = super::Cmd::copy_file( - crate::test::mock_invoke_context(), - source, - destination, - options, - ); - crate::test_utils::assert_not_allowlist_error(res); - } - - #[tauri_macros::module_command_test(fs_create_dir, "fs > createDir")] - #[quickcheck_macros::quickcheck] - fn create_dir(path: SafePathBuf, options: Option) { - let res = super::Cmd::create_dir(crate::test::mock_invoke_context(), path, options); - crate::test_utils::assert_not_allowlist_error(res); - } - - #[tauri_macros::module_command_test(fs_remove_dir, "fs > removeDir")] - #[quickcheck_macros::quickcheck] - fn remove_dir(path: SafePathBuf, options: Option) { - let res = super::Cmd::remove_dir(crate::test::mock_invoke_context(), path, options); - crate::test_utils::assert_not_allowlist_error(res); - } - - #[tauri_macros::module_command_test(fs_remove_file, "fs > removeFile")] - #[quickcheck_macros::quickcheck] - fn remove_file(path: SafePathBuf, options: Option) { - let res = super::Cmd::remove_file(crate::test::mock_invoke_context(), path, options); - crate::test_utils::assert_not_allowlist_error(res); - } - - #[tauri_macros::module_command_test(fs_rename_file, "fs > renameFile")] - #[quickcheck_macros::quickcheck] - fn rename_file( - old_path: SafePathBuf, - new_path: SafePathBuf, - options: Option, - ) { - let res = super::Cmd::rename_file( - crate::test::mock_invoke_context(), - old_path, - new_path, - options, - ); - crate::test_utils::assert_not_allowlist_error(res); - } - - #[tauri_macros::module_command_test(fs_exists, "fs > exists")] - #[quickcheck_macros::quickcheck] - fn exists(path: SafePathBuf, options: Option) { - let res = super::Cmd::exists(crate::test::mock_invoke_context(), path, options); - crate::test_utils::assert_not_allowlist_error(res); - } -} diff --git a/core/tauri/src/endpoints/global_shortcut.rs b/core/tauri/src/endpoints/global_shortcut.rs deleted file mode 100644 index eefb34f4dba..00000000000 --- a/core/tauri/src/endpoints/global_shortcut.rs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![allow(unused_imports)] - -use super::InvokeContext; -use crate::{api::ipc::CallbackFn, Runtime}; -use serde::Deserialize; -use tauri_macros::{command_enum, module_command_handler, CommandModule}; - -#[cfg(global_shortcut_all)] -use crate::runtime::GlobalShortcutManager; - -/// The API descriptor. -#[command_enum] -#[derive(Deserialize, CommandModule)] -#[serde(tag = "cmd", rename_all = "camelCase")] -pub enum Cmd { - /// Register a global shortcut. - #[cmd(global_shortcut_all, "globalShortcut > all")] - Register { - shortcut: String, - handler: CallbackFn, - }, - /// Register a list of global shortcuts. - #[cmd(global_shortcut_all, "globalShortcut > all")] - RegisterAll { - shortcuts: Vec, - handler: CallbackFn, - }, - /// Unregister a global shortcut. - #[cmd(global_shortcut_all, "globalShortcut > all")] - Unregister { shortcut: String }, - /// Unregisters all registered shortcuts. - UnregisterAll, - /// Determines whether the given hotkey is registered or not. - #[cmd(global_shortcut_all, "globalShortcut > all")] - IsRegistered { shortcut: String }, -} - -impl Cmd { - #[module_command_handler(global_shortcut_all)] - fn register( - context: InvokeContext, - shortcut: String, - handler: CallbackFn, - ) -> super::Result<()> { - let mut manager = context.window.app_handle.global_shortcut_manager(); - register_shortcut(context.window, &mut manager, shortcut, handler)?; - Ok(()) - } - - #[module_command_handler(global_shortcut_all)] - fn register_all( - context: InvokeContext, - shortcuts: Vec, - handler: CallbackFn, - ) -> super::Result<()> { - let mut manager = context.window.app_handle.global_shortcut_manager(); - for shortcut in shortcuts { - register_shortcut(context.window.clone(), &mut manager, shortcut, handler)?; - } - Ok(()) - } - - #[module_command_handler(global_shortcut_all)] - fn unregister(context: InvokeContext, shortcut: String) -> super::Result<()> { - context - .window - .app_handle - .global_shortcut_manager() - .unregister(&shortcut) - .map_err(crate::error::into_anyhow)?; - Ok(()) - } - - #[module_command_handler(global_shortcut_all)] - fn unregister_all(context: InvokeContext) -> super::Result<()> { - context - .window - .app_handle - .global_shortcut_manager() - .unregister_all() - .map_err(crate::error::into_anyhow)?; - Ok(()) - } - - #[cfg(not(global_shortcut_all))] - fn unregister_all(_: InvokeContext) -> super::Result<()> { - Err(crate::Error::ApiNotAllowlisted("globalShortcut > all".into()).into_anyhow()) - } - - #[module_command_handler(global_shortcut_all)] - fn is_registered(context: InvokeContext, shortcut: String) -> super::Result { - context - .window - .app_handle - .global_shortcut_manager() - .is_registered(&shortcut) - .map_err(crate::error::into_anyhow) - } -} - -#[cfg(global_shortcut_all)] -fn register_shortcut( - window: crate::Window, - manager: &mut R::GlobalShortcutManager, - shortcut: String, - handler: CallbackFn, -) -> super::Result<()> { - let accelerator = shortcut.clone(); - manager - .register(&shortcut, move || { - let callback_string = crate::api::ipc::format_callback(handler, &accelerator) - .expect("unable to serialize shortcut string to json"); - let _ = window.eval(callback_string.as_str()); - }) - .map_err(crate::error::into_anyhow)?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use crate::api::ipc::CallbackFn; - - #[tauri_macros::module_command_test(global_shortcut_all, "globalShortcut > all")] - #[quickcheck_macros::quickcheck] - fn register(shortcut: String, handler: CallbackFn) { - let ctx = crate::test::mock_invoke_context(); - super::Cmd::register(ctx.clone(), shortcut.clone(), handler).unwrap(); - assert!(super::Cmd::is_registered(ctx, shortcut).unwrap()); - } - - #[tauri_macros::module_command_test(global_shortcut_all, "globalShortcut > all")] - #[quickcheck_macros::quickcheck] - fn register_all(shortcuts: Vec, handler: CallbackFn) { - let ctx = crate::test::mock_invoke_context(); - super::Cmd::register_all(ctx.clone(), shortcuts.clone(), handler).unwrap(); - for shortcut in shortcuts { - assert!(super::Cmd::is_registered(ctx.clone(), shortcut).unwrap(),); - } - } - - #[tauri_macros::module_command_test(global_shortcut_all, "globalShortcut > all")] - #[quickcheck_macros::quickcheck] - fn unregister(shortcut: String) { - let ctx = crate::test::mock_invoke_context(); - super::Cmd::register(ctx.clone(), shortcut.clone(), CallbackFn(0)).unwrap(); - super::Cmd::unregister(ctx.clone(), shortcut.clone()).unwrap(); - assert!(!super::Cmd::is_registered(ctx, shortcut).unwrap()); - } - - #[tauri_macros::module_command_test(global_shortcut_all, "globalShortcut > all", runtime)] - #[quickcheck_macros::quickcheck] - fn unregister_all() { - let shortcuts = vec!["CTRL+X".to_string(), "SUPER+C".to_string(), "D".to_string()]; - let ctx = crate::test::mock_invoke_context(); - super::Cmd::register_all(ctx.clone(), shortcuts.clone(), CallbackFn(0)).unwrap(); - super::Cmd::unregister_all(ctx.clone()).unwrap(); - for shortcut in shortcuts { - assert!(!super::Cmd::is_registered(ctx.clone(), shortcut).unwrap(),); - } - } - - #[tauri_macros::module_command_test(global_shortcut_all, "globalShortcut > all")] - #[quickcheck_macros::quickcheck] - fn is_registered(shortcut: String) { - let ctx = crate::test::mock_invoke_context(); - assert!(!super::Cmd::is_registered(ctx.clone(), shortcut.clone()).unwrap(),); - super::Cmd::register(ctx.clone(), shortcut.clone(), CallbackFn(0)).unwrap(); - assert!(super::Cmd::is_registered(ctx, shortcut).unwrap()); - } -} diff --git a/core/tauri/src/endpoints/http.rs b/core/tauri/src/endpoints/http.rs deleted file mode 100644 index 4f6951791ea..00000000000 --- a/core/tauri/src/endpoints/http.rs +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![allow(unused_imports)] - -use super::InvokeContext; -use crate::Runtime; -use serde::Deserialize; -use tauri_macros::{command_enum, module_command_handler, CommandModule}; - -#[cfg(http_request)] -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, -}; - -#[cfg(http_request)] -use crate::api::http::{ClientBuilder, HttpRequestBuilder, ResponseData}; -#[cfg(not(http_request))] -type ClientBuilder = (); -#[cfg(not(http_request))] -type HttpRequestBuilder = (); -#[cfg(not(http_request))] -#[allow(dead_code)] -type ResponseData = (); - -type ClientId = u32; -#[cfg(http_request)] -type ClientStore = Arc>>; - -#[cfg(http_request)] -fn clients() -> &'static ClientStore { - use once_cell::sync::Lazy; - static STORE: Lazy = Lazy::new(Default::default); - &STORE -} - -/// The API descriptor. -#[command_enum] -#[derive(Deserialize, CommandModule)] -#[cmd(async)] -#[serde(tag = "cmd", rename_all = "camelCase")] -pub enum Cmd { - /// Create a new HTTP client. - #[cmd(http_request, "http > request")] - CreateClient { options: Option }, - /// Drop a HTTP client. - #[cmd(http_request, "http > request")] - DropClient { client: ClientId }, - /// The HTTP request API. - #[cmd(http_request, "http > request")] - HttpRequest { - client: ClientId, - options: Box, - }, -} - -impl Cmd { - #[module_command_handler(http_request)] - async fn create_client( - _context: InvokeContext, - options: Option, - ) -> super::Result { - let client = options.unwrap_or_default().build()?; - let mut store = clients().lock().unwrap(); - let id = rand::random::(); - store.insert(id, client); - Ok(id) - } - - #[module_command_handler(http_request)] - async fn drop_client( - _context: InvokeContext, - client: ClientId, - ) -> super::Result<()> { - let mut store = clients().lock().unwrap(); - store.remove(&client); - Ok(()) - } - - #[module_command_handler(http_request)] - async fn http_request( - context: InvokeContext, - client_id: ClientId, - options: Box, - ) -> super::Result { - use crate::Manager; - let scopes = context.window.state::(); - if scopes.http.is_allowed(&options.url) { - let client = clients() - .lock() - .unwrap() - .get(&client_id) - .ok_or_else(|| crate::Error::HttpClientNotInitialized.into_anyhow())? - .clone(); - let options = *options; - if let Some(crate::api::http::Body::Form(form)) = &options.body { - for value in form.0.values() { - if let crate::api::http::FormPart::File { - file: crate::api::http::FilePart::Path(path), - .. - } = value - { - if crate::api::file::SafePathBuf::new(path.clone()).is_err() - || !scopes.fs.is_allowed(path) - { - return Err(crate::Error::PathNotAllowed(path.clone()).into_anyhow()); - } - } - } - } - let response = client.send(options).await?; - Ok(response.read().await?) - } else { - Err(crate::Error::UrlNotAllowed(options.url).into_anyhow()) - } - } -} - -#[cfg(test)] -mod tests { - use super::{ClientBuilder, ClientId}; - - #[tauri_macros::module_command_test(http_request, "http > request")] - #[quickcheck_macros::quickcheck] - fn create_client(options: Option) { - assert!(crate::async_runtime::block_on(super::Cmd::create_client( - crate::test::mock_invoke_context(), - options - )) - .is_ok()); - } - - #[tauri_macros::module_command_test(http_request, "http > request")] - #[quickcheck_macros::quickcheck] - fn drop_client(client_id: ClientId) { - crate::async_runtime::block_on(async move { - assert!( - super::Cmd::drop_client(crate::test::mock_invoke_context(), client_id) - .await - .is_ok() - ); - let id = super::Cmd::create_client(crate::test::mock_invoke_context(), None) - .await - .unwrap(); - assert!( - super::Cmd::drop_client(crate::test::mock_invoke_context(), id) - .await - .is_ok() - ); - }); - } -} diff --git a/core/tauri/src/endpoints/notification.rs b/core/tauri/src/endpoints/notification.rs deleted file mode 100644 index 2399269a704..00000000000 --- a/core/tauri/src/endpoints/notification.rs +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![allow(unused_imports)] - -use super::InvokeContext; -use crate::Runtime; -use serde::Deserialize; -use tauri_macros::{command_enum, module_command_handler, CommandModule}; - -#[cfg(notification_all)] -use crate::{api::notification::Notification, Env, Manager}; - -// `Granted` response from `request_permission`. Matches the Web API return value. -const PERMISSION_GRANTED: &str = "granted"; -// `Denied` response from `request_permission`. Matches the Web API return value. -const PERMISSION_DENIED: &str = "denied"; - -/// The options for the notification API. -#[derive(Debug, Clone, Deserialize)] -pub struct NotificationOptions { - /// The notification title. - pub title: String, - /// The notification body. - pub body: Option, - /// The notification icon. - pub icon: Option, -} - -/// The API descriptor. -#[command_enum] -#[derive(Deserialize, CommandModule)] -#[serde(tag = "cmd", rename_all = "camelCase")] -pub enum Cmd { - /// The show notification API. - #[cmd(notification_all, "notification > all")] - Notification { options: NotificationOptions }, - /// The request notification permission API. - RequestNotificationPermission, - /// The notification permission check API. - IsNotificationPermissionGranted, -} - -impl Cmd { - #[module_command_handler(notification_all)] - fn notification( - context: InvokeContext, - options: NotificationOptions, - ) -> super::Result<()> { - let mut notification = - Notification::new(context.config.tauri.bundle.identifier.clone()).title(options.title); - if let Some(body) = options.body { - notification = notification.body(body); - } - if let Some(icon) = options.icon { - notification = notification.icon(icon); - } - #[cfg(feature = "windows7-compat")] - { - notification.notify(&context.window.app_handle)?; - } - #[cfg(not(feature = "windows7-compat"))] - notification.show()?; - Ok(()) - } - - fn request_notification_permission( - _context: InvokeContext, - ) -> super::Result<&'static str> { - Ok(if cfg!(notification_all) { - PERMISSION_GRANTED - } else { - PERMISSION_DENIED - }) - } - - fn is_notification_permission_granted( - _context: InvokeContext, - ) -> super::Result { - Ok(cfg!(notification_all)) - } -} - -#[cfg(test)] -mod tests { - use super::NotificationOptions; - - use quickcheck::{Arbitrary, Gen}; - - impl Arbitrary for NotificationOptions { - fn arbitrary(g: &mut Gen) -> Self { - Self { - title: String::arbitrary(g), - body: Option::arbitrary(g), - icon: Option::arbitrary(g), - } - } - } - - #[cfg(not(notification_all))] - #[test] - fn request_notification_permission() { - assert_eq!( - super::Cmd::request_notification_permission(crate::test::mock_invoke_context()).unwrap(), - if cfg!(notification_all) { - super::PERMISSION_GRANTED - } else { - super::PERMISSION_DENIED - } - ) - } - - #[cfg(not(notification_all))] - #[test] - fn is_notification_permission_granted() { - let expected = cfg!(notification_all); - assert_eq!( - super::Cmd::is_notification_permission_granted(crate::test::mock_invoke_context()).unwrap(), - expected, - ); - } - - #[tauri_macros::module_command_test(notification_all, "notification > all")] - #[quickcheck_macros::quickcheck] - fn notification(_options: NotificationOptions) {} -} diff --git a/core/tauri/src/endpoints/operating_system.rs b/core/tauri/src/endpoints/operating_system.rs deleted file mode 100644 index 61b11d45723..00000000000 --- a/core/tauri/src/endpoints/operating_system.rs +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![allow(unused_imports)] - -use super::InvokeContext; -use crate::Runtime; -use serde::Deserialize; -use std::path::PathBuf; -use tauri_macros::{command_enum, module_command_handler, CommandModule}; - -/// The API descriptor. -#[command_enum] -#[derive(Deserialize, CommandModule)] -#[serde(tag = "cmd", rename_all = "camelCase")] -pub enum Cmd { - Platform, - Version, - OsType, - Arch, - Tempdir, - Locale, -} - -#[cfg(os_all)] -impl Cmd { - fn platform(_context: InvokeContext) -> super::Result<&'static str> { - Ok(os_platform()) - } - - fn version(_context: InvokeContext) -> super::Result { - Ok(os_info::get().version().to_string()) - } - - fn os_type(_context: InvokeContext) -> super::Result<&'static str> { - Ok(os_type()) - } - - fn arch(_context: InvokeContext) -> super::Result<&'static str> { - Ok(std::env::consts::ARCH) - } - - fn tempdir(_context: InvokeContext) -> super::Result { - Ok(std::env::temp_dir()) - } - - fn locale(_context: InvokeContext) -> super::Result> { - Ok(crate::api::os::locale()) - } -} - -#[cfg(not(os_all))] -impl Cmd { - fn platform(_context: InvokeContext) -> super::Result<&'static str> { - Err(crate::Error::ApiNotAllowlisted("os > all".into()).into_anyhow()) - } - - fn version(_context: InvokeContext) -> super::Result { - Err(crate::Error::ApiNotAllowlisted("os > all".into()).into_anyhow()) - } - - fn os_type(_context: InvokeContext) -> super::Result<&'static str> { - Err(crate::Error::ApiNotAllowlisted("os > all".into()).into_anyhow()) - } - - fn arch(_context: InvokeContext) -> super::Result<&'static str> { - Err(crate::Error::ApiNotAllowlisted("os > all".into()).into_anyhow()) - } - - fn tempdir(_context: InvokeContext) -> super::Result { - Err(crate::Error::ApiNotAllowlisted("os > all".into()).into_anyhow()) - } - - fn locale(_context: InvokeContext) -> super::Result> { - Err(crate::Error::ApiNotAllowlisted("os > all".into()).into_anyhow()) - } -} - -#[cfg(os_all)] -fn os_type() -> &'static str { - #[cfg(target_os = "linux")] - return "Linux"; - #[cfg(target_os = "windows")] - return "Windows_NT"; - #[cfg(target_os = "macos")] - return "Darwin"; - #[cfg(target_os = "ios")] - return "iOS"; -} - -#[cfg(os_all)] -fn os_platform() -> &'static str { - match std::env::consts::OS { - "windows" => "win32", - "macos" => "darwin", - _ => std::env::consts::OS, - } -} - -#[cfg(test)] -mod tests { - #[tauri_macros::module_command_test(os_all, "os > all", runtime)] - #[quickcheck_macros::quickcheck] - fn platform() {} - - #[tauri_macros::module_command_test(os_all, "os > all", runtime)] - #[quickcheck_macros::quickcheck] - fn version() {} - - #[tauri_macros::module_command_test(os_all, "os > all", runtime)] - #[quickcheck_macros::quickcheck] - fn os_type() {} - - #[tauri_macros::module_command_test(os_all, "os > all", runtime)] - #[quickcheck_macros::quickcheck] - fn arch() {} - - #[tauri_macros::module_command_test(os_all, "os > all", runtime)] - #[quickcheck_macros::quickcheck] - fn tempdir() {} - - #[tauri_macros::module_command_test(os_all, "os > all", runtime)] - #[quickcheck_macros::quickcheck] - fn locale() {} -} diff --git a/core/tauri/src/endpoints/path.rs b/core/tauri/src/endpoints/path.rs deleted file mode 100644 index 66771c75529..00000000000 --- a/core/tauri/src/endpoints/path.rs +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![allow(unused_imports)] - -use crate::{api::path::BaseDirectory, Runtime}; -#[cfg(path_all)] -use crate::{Env, Manager}; -use std::path::PathBuf; -#[cfg(path_all)] -use std::path::{Component, Path, MAIN_SEPARATOR}; - -use super::InvokeContext; -use serde::Deserialize; -use tauri_macros::{command_enum, module_command_handler, CommandModule}; - -/// The API descriptor. -#[command_enum] -#[derive(Deserialize, CommandModule)] -#[serde(tag = "cmd", rename_all = "camelCase")] -pub enum Cmd { - #[cmd(path_all, "path > all")] - ResolvePath { - path: String, - directory: Option, - }, - #[cmd(path_all, "path > all")] - Resolve { paths: Vec }, - #[cmd(path_all, "path > all")] - Normalize { path: String }, - #[cmd(path_all, "path > all")] - Join { paths: Vec }, - #[cmd(path_all, "path > all")] - Dirname { path: String }, - #[cmd(path_all, "path > all")] - Extname { path: String }, - #[cmd(path_all, "path > all")] - Basename { path: String, ext: Option }, - #[cmd(path_all, "path > all")] - IsAbsolute { path: String }, -} - -impl Cmd { - #[module_command_handler(path_all)] - fn resolve_path( - context: InvokeContext, - path: String, - directory: Option, - ) -> super::Result { - crate::api::path::resolve_path( - &context.config, - &context.package_info, - context.window.state::().inner(), - path, - directory, - ) - .map_err(Into::into) - } - - #[module_command_handler(path_all)] - fn resolve(_context: InvokeContext, paths: Vec) -> super::Result { - // Start with current directory then start adding paths from the vector one by one using `PathBuf.push()` which - // will ensure that if an absolute path is encountered in the iteration, it will be used as the current full path. - // - // examples: - // 1. `vec!["."]` or `vec![]` will be equal to `std::env::current_dir()` - // 2. `vec!["/foo/bar", "/tmp/file", "baz"]` will be equal to `PathBuf::from("/tmp/file/baz")` - let mut path = std::env::current_dir()?; - for p in paths { - path.push(p); - } - Ok(normalize_path(&path)) - } - - #[module_command_handler(path_all)] - fn normalize(_context: InvokeContext, path: String) -> super::Result { - let mut p = normalize_path_no_absolute(Path::new(&path)) - .to_string_lossy() - .to_string(); - Ok( - // Node.js behavior is to return `".."` for `normalize("..")` - // and `"."` for `normalize("")` or `normalize(".")` - if p.is_empty() && path == ".." { - "..".into() - } else if p.is_empty() && path == "." { - ".".into() - } else { - // Add a trailing separator if the path passed to this functions had a trailing separator. That's how Node.js behaves. - if (path.ends_with('/') || path.ends_with('\\')) - && (!p.ends_with('/') || !p.ends_with('\\')) - { - p.push(MAIN_SEPARATOR); - } - p - }, - ) - } - - #[module_command_handler(path_all)] - fn join(_context: InvokeContext, mut paths: Vec) -> super::Result { - let path = PathBuf::from( - paths - .iter_mut() - .map(|p| { - // Add a `MAIN_SEPARATOR` if it doesn't already have one. - // Doing this to ensure that the vector elements are separated in - // the resulting string so path.components() can work correctly when called - // in `normalize_path_no_absolute()` later on. - if !p.ends_with('/') && !p.ends_with('\\') { - p.push(MAIN_SEPARATOR); - } - p.to_string() - }) - .collect::(), - ); - - let p = normalize_path_no_absolute(&path) - .to_string_lossy() - .to_string(); - Ok(if p.is_empty() { ".".into() } else { p }) - } - - #[module_command_handler(path_all)] - fn dirname(_context: InvokeContext, path: String) -> super::Result { - match Path::new(&path).parent() { - Some(p) => Ok(p.to_path_buf()), - None => Err(crate::error::into_anyhow(crate::api::Error::Path( - "Couldn't get the parent directory".into(), - ))), - } - } - - #[module_command_handler(path_all)] - fn extname(_context: InvokeContext, path: String) -> super::Result { - match Path::new(&path) - .extension() - .and_then(std::ffi::OsStr::to_str) - { - Some(p) => Ok(p.to_string()), - None => Err(crate::error::into_anyhow(crate::api::Error::Path( - "Couldn't get the extension of the file".into(), - ))), - } - } - - #[module_command_handler(path_all)] - fn basename( - _context: InvokeContext, - path: String, - ext: Option, - ) -> super::Result { - match Path::new(&path) - .file_name() - .and_then(std::ffi::OsStr::to_str) - { - Some(p) => Ok(if let Some(ext) = ext { - p.replace(ext.as_str(), "") - } else { - p.to_string() - }), - None => Err(crate::error::into_anyhow(crate::api::Error::Path( - "Couldn't get the basename".into(), - ))), - } - } - - #[module_command_handler(path_all)] - fn is_absolute(_context: InvokeContext, path: String) -> super::Result { - Ok(Path::new(&path).is_absolute()) - } -} - -/// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util. -/// https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106 -#[cfg(path_all)] -fn normalize_path(path: &Path) -> PathBuf { - let mut components = path.components().peekable(); - let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { - components.next(); - PathBuf::from(c.as_os_str()) - } else { - PathBuf::new() - }; - - for component in components { - match component { - Component::Prefix(..) => unreachable!(), - Component::RootDir => { - ret.push(component.as_os_str()); - } - Component::CurDir => {} - Component::ParentDir => { - ret.pop(); - } - Component::Normal(c) => { - ret.push(c); - } - } - } - ret -} - -/// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util but -/// slightly modified to not resolve absolute paths. -/// https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106 -#[cfg(path_all)] -fn normalize_path_no_absolute(path: &Path) -> PathBuf { - let mut components = path.components().peekable(); - let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { - components.next(); - PathBuf::from(c.as_os_str()) - } else { - PathBuf::new() - }; - - for component in components { - match component { - Component::Prefix(..) => unreachable!(), - Component::RootDir => { - ret.push(component.as_os_str()); - } - Component::CurDir => {} - Component::ParentDir => { - ret.pop(); - } - Component::Normal(c) => { - // Using PathBuf::push here will replace the whole path if an absolute path is encountered - // which is not the intended behavior, so instead of that, convert the current resolved path - // to a string and do simple string concatenation with the current component then convert it - // back to a PathBuf - let mut p = ret.to_string_lossy().to_string(); - // Only add a separator if it doesn't have one already or if current normalized path is empty, - // this ensures it won't have an unwanted leading separator - if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') { - p.push(MAIN_SEPARATOR); - } - if let Some(c) = c.to_str() { - p.push_str(c); - } - ret = PathBuf::from(p); - } - } - } - ret -} - -#[cfg(test)] -mod tests { - use crate::api::path::BaseDirectory; - - #[tauri_macros::module_command_test(path_all, "path > all")] - #[quickcheck_macros::quickcheck] - fn resolve_path(_path: String, _directory: Option) {} - - #[tauri_macros::module_command_test(path_all, "path > all")] - #[quickcheck_macros::quickcheck] - fn resolve(_paths: Vec) {} - - #[tauri_macros::module_command_test(path_all, "path > all")] - #[quickcheck_macros::quickcheck] - fn normalize(_path: String) {} - - #[tauri_macros::module_command_test(path_all, "path > all")] - #[quickcheck_macros::quickcheck] - fn join(_paths: Vec) {} - - #[tauri_macros::module_command_test(path_all, "path > all")] - #[quickcheck_macros::quickcheck] - fn dirname(_path: String) {} - - #[tauri_macros::module_command_test(path_all, "path > all")] - #[quickcheck_macros::quickcheck] - fn extname(_path: String) {} - - #[tauri_macros::module_command_test(path_all, "path > all")] - #[quickcheck_macros::quickcheck] - fn basename(_path: String, _ext: Option) {} - - #[tauri_macros::module_command_test(path_all, "path > all")] - #[quickcheck_macros::quickcheck] - fn is_absolute(_path: String) {} -} diff --git a/core/tauri/src/endpoints/process.rs b/core/tauri/src/endpoints/process.rs deleted file mode 100644 index b9e8b5364e0..00000000000 --- a/core/tauri/src/endpoints/process.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![allow(unused_imports)] - -use super::InvokeContext; -#[cfg(process_relaunch)] -use crate::Manager; -use crate::Runtime; -use serde::Deserialize; -use tauri_macros::{command_enum, module_command_handler, CommandModule}; - -/// The API descriptor. -#[command_enum] -#[derive(Deserialize, CommandModule)] -#[serde(tag = "cmd", rename_all = "camelCase")] -pub enum Cmd { - /// Relaunch application - Relaunch, - /// Close application with provided exit_code - #[cmd(process_exit, "process > exit")] - #[serde(rename_all = "camelCase")] - Exit { exit_code: i32 }, -} - -impl Cmd { - #[module_command_handler(process_relaunch)] - fn relaunch(context: InvokeContext) -> super::Result<()> { - context.window.app_handle().restart(); - Ok(()) - } - - #[cfg(not(process_relaunch))] - fn relaunch(_: InvokeContext) -> super::Result<()> { - Err(crate::Error::ApiNotAllowlisted("process > relaunch".into()).into_anyhow()) - } - - #[module_command_handler(process_exit)] - fn exit(context: InvokeContext, exit_code: i32) -> super::Result<()> { - // would be great if we can have a handler inside tauri - // who close all window and emit an event that user can catch - // if they want to process something before closing the app - context.window.app_handle.exit(exit_code); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - #[tauri_macros::module_command_test(process_relaunch, "process > relaunch", runtime)] - #[quickcheck_macros::quickcheck] - fn relaunch() {} - - #[tauri_macros::module_command_test(process_exit, "process > exit")] - #[quickcheck_macros::quickcheck] - fn exit(_exit_code: i32) {} -} diff --git a/core/tauri/src/endpoints/shell.rs b/core/tauri/src/endpoints/shell.rs deleted file mode 100644 index daf0d47b161..00000000000 --- a/core/tauri/src/endpoints/shell.rs +++ /dev/null @@ -1,280 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![allow(unused_imports)] - -use super::InvokeContext; -use crate::{api::ipc::CallbackFn, Runtime}; -#[cfg(shell_scope)] -use crate::{Manager, Scopes}; -use serde::Deserialize; -use tauri_macros::{command_enum, module_command_handler, CommandModule}; - -#[cfg(shell_scope)] -use crate::ExecuteArgs; -#[cfg(not(shell_scope))] -type ExecuteArgs = (); - -#[cfg(any(shell_execute, shell_sidecar))] -use std::sync::{Arc, Mutex}; -use std::{collections::HashMap, path::PathBuf}; - -type ChildId = u32; -#[cfg(any(shell_execute, shell_sidecar))] -type ChildStore = Arc>>; - -#[cfg(any(shell_execute, shell_sidecar))] -fn command_child_store() -> &'static ChildStore { - use once_cell::sync::Lazy; - static STORE: Lazy = Lazy::new(Default::default); - &STORE -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(untagged)] -pub enum Buffer { - Text(String), - Raw(Vec), -} - -#[allow(clippy::unnecessary_wraps)] -fn default_env() -> Option> { - Some(HashMap::default()) -} - -#[allow(dead_code)] -#[derive(Debug, Clone, Default, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CommandOptions { - #[serde(default)] - sidecar: bool, - cwd: Option, - // by default we don't add any env variables to the spawned process - // but the env is an `Option` so when it's `None` we clear the env. - #[serde(default = "default_env")] - env: Option>, - // Character encoding for stdout/stderr - encoding: Option, -} - -/// The API descriptor. -#[command_enum] -#[derive(Deserialize, CommandModule)] -#[serde(tag = "cmd", rename_all = "camelCase")] -pub enum Cmd { - /// The execute script API. - #[cmd(shell_script, "shell > execute or shell > sidecar")] - #[serde(rename_all = "camelCase")] - Execute { - program: String, - args: ExecuteArgs, - on_event_fn: CallbackFn, - #[serde(default)] - options: CommandOptions, - }, - #[cmd(shell_script, "shell > execute or shell > sidecar")] - StdinWrite { pid: ChildId, buffer: Buffer }, - #[cmd(shell_script, "shell > execute or shell > sidecar")] - KillChild { pid: ChildId }, - #[cmd(shell_open, "shell > open")] - Open { path: String, with: Option }, -} - -impl Cmd { - #[module_command_handler(shell_script)] - #[allow(unused_variables)] - fn execute( - context: InvokeContext, - program: String, - args: ExecuteArgs, - on_event_fn: CallbackFn, - options: CommandOptions, - ) -> super::Result { - let mut command = if options.sidecar { - #[cfg(not(shell_sidecar))] - return Err(crate::Error::ApiNotAllowlisted("shell > sidecar".to_string()).into_anyhow()); - #[cfg(shell_sidecar)] - { - let program = PathBuf::from(program); - let program_as_string = program.display().to_string(); - let program_no_ext_as_string = program.with_extension("").display().to_string(); - let configured_sidecar = context - .config - .tauri - .bundle - .external_bin - .as_ref() - .map(|bins| { - bins - .iter() - .find(|b| b == &&program_as_string || b == &&program_no_ext_as_string) - }) - .unwrap_or_default(); - if let Some(sidecar) = configured_sidecar { - context - .window - .state::() - .shell - .prepare_sidecar(&program.to_string_lossy(), sidecar, args) - .map_err(crate::error::into_anyhow)? - } else { - return Err(crate::Error::SidecarNotAllowed(program).into_anyhow()); - } - } - } else { - #[cfg(not(shell_execute))] - return Err(crate::Error::ApiNotAllowlisted("shell > execute".to_string()).into_anyhow()); - #[cfg(shell_execute)] - match context - .window - .state::() - .shell - .prepare(&program, args) - { - Ok(cmd) => cmd, - Err(e) => { - #[cfg(debug_assertions)] - eprintln!("{e}"); - return Err(crate::Error::ProgramNotAllowed(PathBuf::from(program)).into_anyhow()); - } - } - }; - #[cfg(any(shell_execute, shell_sidecar))] - { - if let Some(cwd) = options.cwd { - command = command.current_dir(cwd); - } - if let Some(env) = options.env { - command = command.envs(env); - } else { - command = command.env_clear(); - } - if let Some(encoding) = options.encoding { - 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}"))); - } - } - let (mut rx, child) = command.spawn()?; - - let pid = child.pid(); - command_child_store().lock().unwrap().insert(pid, child); - - crate::async_runtime::spawn(async move { - while let Some(event) = rx.recv().await { - if matches!(event, crate::api::process::CommandEvent::Terminated(_)) { - command_child_store().lock().unwrap().remove(&pid); - } - let js = crate::api::ipc::format_callback(on_event_fn, &event) - .expect("unable to serialize CommandEvent"); - - let _ = context.window.eval(js.as_str()); - } - }); - - Ok(pid) - } - } - - #[module_command_handler(shell_script)] - fn stdin_write( - _context: InvokeContext, - pid: ChildId, - buffer: Buffer, - ) -> super::Result<()> { - if let Some(child) = command_child_store().lock().unwrap().get_mut(&pid) { - match buffer { - Buffer::Text(t) => child.write(t.as_bytes())?, - Buffer::Raw(r) => child.write(&r)?, - } - } - Ok(()) - } - - #[module_command_handler(shell_script)] - fn kill_child(_context: InvokeContext, pid: ChildId) -> super::Result<()> { - if let Some(child) = command_child_store().lock().unwrap().remove(&pid) { - child.kill()?; - } - Ok(()) - } - - /// Open a (url) path with a default or specific browser opening program. - /// - /// See [`crate::api::shell::open`] for how it handles security-related measures. - #[module_command_handler(shell_open)] - fn open( - context: InvokeContext, - path: String, - with: Option, - ) -> super::Result<()> { - use std::str::FromStr; - - with - .as_deref() - // only allow pre-determined programs to be specified - .map(crate::api::shell::Program::from_str) - .transpose() - .map_err(Into::into) - // validate and open path - .and_then(|with| { - crate::api::shell::open(&context.window.state::().shell, path, with) - .map_err(Into::into) - }) - } -} - -#[cfg(test)] -mod tests { - use super::{Buffer, ChildId, CommandOptions, ExecuteArgs}; - use crate::api::ipc::CallbackFn; - use quickcheck::{Arbitrary, Gen}; - - impl Arbitrary for CommandOptions { - fn arbitrary(g: &mut Gen) -> Self { - Self { - sidecar: false, - cwd: Option::arbitrary(g), - env: Option::arbitrary(g), - encoding: Option::arbitrary(g), - } - } - } - - impl Arbitrary for Buffer { - fn arbitrary(g: &mut Gen) -> Self { - Buffer::Text(String::arbitrary(g)) - } - } - - #[cfg(shell_scope)] - impl Arbitrary for ExecuteArgs { - fn arbitrary(_: &mut Gen) -> Self { - ExecuteArgs::None - } - } - - #[tauri_macros::module_command_test(shell_execute, "shell > execute")] - #[quickcheck_macros::quickcheck] - fn execute( - _program: String, - _args: ExecuteArgs, - _on_event_fn: CallbackFn, - _options: CommandOptions, - ) { - } - - #[tauri_macros::module_command_test(shell_execute, "shell > execute or shell > sidecar")] - #[quickcheck_macros::quickcheck] - fn stdin_write(_pid: ChildId, _buffer: Buffer) {} - - #[tauri_macros::module_command_test(shell_execute, "shell > execute or shell > sidecar")] - #[quickcheck_macros::quickcheck] - fn kill_child(_pid: ChildId) {} - - #[tauri_macros::module_command_test(shell_open, "shell > open")] - #[quickcheck_macros::quickcheck] - fn open(_path: String, _with: Option) {} -} diff --git a/core/tauri/src/endpoints/window.rs b/core/tauri/src/endpoints/window.rs deleted file mode 100644 index 5762c40e157..00000000000 --- a/core/tauri/src/endpoints/window.rs +++ /dev/null @@ -1,377 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![allow(unused_imports)] - -use super::{InvokeContext, InvokeResponse}; -#[cfg(window_create)] -use crate::runtime::{webview::WindowBuilder, Dispatch}; -use crate::{ - runtime::{ - window::dpi::{Position, Size}, - UserAttentionType, - }, - utils::config::WindowConfig, - CursorIcon, Icon, Manager, Runtime, -}; -use serde::Deserialize; -use tauri_macros::{command_enum, module_command_handler, CommandModule}; - -#[derive(Deserialize)] -#[serde(untagged)] -pub enum IconDto { - #[cfg(any(feature = "icon-png", feature = "icon-ico"))] - File(std::path::PathBuf), - #[cfg(any(feature = "icon-png", feature = "icon-ico"))] - Raw(Vec), - Rgba { - rgba: Vec, - width: u32, - height: u32, - }, -} - -impl From for Icon { - fn from(icon: IconDto) -> Self { - match icon { - #[cfg(any(feature = "icon-png", feature = "icon-ico"))] - IconDto::File(path) => Self::File(path), - #[cfg(any(feature = "icon-png", feature = "icon-ico"))] - IconDto::Raw(raw) => Self::Raw(raw), - IconDto::Rgba { - rgba, - width, - height, - } => Self::Rgba { - rgba, - width, - height, - }, - } - } -} - -/// Window management API descriptor. -#[derive(Deserialize)] -#[serde(tag = "type", content = "payload", rename_all = "camelCase")] -pub enum WindowManagerCmd { - // Getters - ScaleFactor, - InnerPosition, - OuterPosition, - InnerSize, - OuterSize, - IsFullscreen, - IsMinimized, - IsMaximized, - IsFocused, - IsDecorated, - IsResizable, - IsMaximizable, - IsMinimizable, - IsClosable, - IsVisible, - Title, - CurrentMonitor, - PrimaryMonitor, - AvailableMonitors, - Theme, - // Setters - #[cfg(window_center)] - Center, - #[cfg(window_request_user_attention)] - RequestUserAttention(Option), - #[cfg(window_set_resizable)] - SetResizable(bool), - #[cfg(window_set_maximizable)] - SetMaximizable(bool), - #[cfg(window_set_minimizable)] - SetMinimizable(bool), - #[cfg(window_set_closable)] - SetClosable(bool), - #[cfg(window_set_title)] - SetTitle(String), - #[cfg(window_maximize)] - Maximize, - #[cfg(window_unmaximize)] - Unmaximize, - #[cfg(all(window_maximize, window_unmaximize))] - ToggleMaximize, - #[cfg(window_minimize)] - Minimize, - #[cfg(window_unminimize)] - Unminimize, - #[cfg(window_show)] - Show, - #[cfg(window_hide)] - Hide, - #[cfg(window_close)] - Close, - #[cfg(window_set_decorations)] - SetDecorations(bool), - #[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)] - SetMinSize(Option), - #[cfg(window_set_max_size)] - SetMaxSize(Option), - #[cfg(window_set_position)] - SetPosition(Position), - #[cfg(window_set_fullscreen)] - SetFullscreen(bool), - #[cfg(window_set_focus)] - SetFocus, - #[cfg(window_set_icon)] - SetIcon { - icon: IconDto, - }, - #[cfg(window_set_skip_taskbar)] - SetSkipTaskbar(bool), - #[cfg(window_set_cursor_grab)] - SetCursorGrab(bool), - #[cfg(window_set_cursor_visible)] - SetCursorVisible(bool), - #[cfg(window_set_cursor_icon)] - SetCursorIcon(CursorIcon), - #[cfg(window_set_cursor_position)] - SetCursorPosition(Position), - #[cfg(window_set_ignore_cursor_events)] - SetIgnoreCursorEvents(bool), - #[cfg(window_start_dragging)] - StartDragging, - #[cfg(window_print)] - Print, - // internals - #[cfg(all(window_maximize, window_unmaximize))] - #[serde(rename = "__toggleMaximize")] - InternalToggleMaximize, - #[cfg(any(debug_assertions, feature = "devtools"))] - #[serde(rename = "__toggleDevtools")] - InternalToggleDevtools, -} - -pub fn into_allowlist_error(variant: &str) -> crate::Error { - match variant { - "center" => crate::Error::ApiNotAllowlisted("window > center".to_string()), - "requestUserAttention" => { - crate::Error::ApiNotAllowlisted("window > requestUserAttention".to_string()) - } - "setResizable" => crate::Error::ApiNotAllowlisted("window > setResizable".to_string()), - "setTitle" => crate::Error::ApiNotAllowlisted("window > setTitle".to_string()), - "maximize" => crate::Error::ApiNotAllowlisted("window > maximize".to_string()), - "unmaximize" => crate::Error::ApiNotAllowlisted("window > unmaximize".to_string()), - "toggleMaximize" => { - crate::Error::ApiNotAllowlisted("window > maximize and window > unmaximize".to_string()) - } - "minimize" => crate::Error::ApiNotAllowlisted("window > minimize".to_string()), - "unminimize" => crate::Error::ApiNotAllowlisted("window > unminimize".to_string()), - "show" => crate::Error::ApiNotAllowlisted("window > show".to_string()), - "hide" => crate::Error::ApiNotAllowlisted("window > hide".to_string()), - "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()), - "setPosition" => crate::Error::ApiNotAllowlisted("window > setPosition".to_string()), - "setFullscreen" => crate::Error::ApiNotAllowlisted("window > setFullscreen".to_string()), - "setIcon" => crate::Error::ApiNotAllowlisted("window > setIcon".to_string()), - "setSkipTaskbar" => crate::Error::ApiNotAllowlisted("window > setSkipTaskbar".to_string()), - "setCursorGrab" => crate::Error::ApiNotAllowlisted("window > setCursorGrab".to_string()), - "setCursorVisible" => crate::Error::ApiNotAllowlisted("window > setCursorVisible".to_string()), - "setCursorIcon" => crate::Error::ApiNotAllowlisted("window > setCursorIcon".to_string()), - "setCursorPosition" => { - crate::Error::ApiNotAllowlisted("window > setCursorPosition".to_string()) - } - "setIgnoreCursorEvents" => { - crate::Error::ApiNotAllowlisted("window > setIgnoreCursorEvents".to_string()) - } - "startDragging" => crate::Error::ApiNotAllowlisted("window > startDragging".to_string()), - "print" => crate::Error::ApiNotAllowlisted("window > print".to_string()), - "__toggleMaximize" => { - crate::Error::ApiNotAllowlisted("window > maximize and window > unmaximize".to_string()) - } - "__toggleDevtools" => crate::Error::ApiNotAllowlisted("devtools".to_string()), - _ => crate::Error::ApiNotAllowlisted("window".to_string()), - } -} - -/// The API descriptor. -#[command_enum] -#[derive(Deserialize, CommandModule)] -#[cmd(async)] -#[serde(tag = "cmd", content = "data", rename_all = "camelCase")] -pub enum Cmd { - #[cmd(window_create, "window > create")] - CreateWebview { options: Box }, - Manage { - label: Option, - cmd: WindowManagerCmd, - }, -} - -impl Cmd { - #[module_command_handler(window_create)] - async fn create_webview( - context: InvokeContext, - mut options: Box, - ) -> super::Result<()> { - options.additional_browser_args = None; - crate::window::WindowBuilder::from_config(&context.window, *options) - .build() - .map_err(crate::error::into_anyhow)?; - Ok(()) - } - - async fn manage( - context: InvokeContext, - label: Option, - cmd: WindowManagerCmd, - ) -> super::Result { - Self::_manage(context, label, cmd) - .await - .map_err(crate::error::into_anyhow) - } - - async fn _manage( - context: InvokeContext, - label: Option, - cmd: WindowManagerCmd, - ) -> crate::Result { - let window = match label { - Some(l) if !l.is_empty() => context - .window - .get_window(&l) - .ok_or(crate::Error::WebviewNotFound)?, - _ => context.window, - }; - match cmd { - // Getters - WindowManagerCmd::ScaleFactor => return Ok(window.scale_factor()?.into()), - WindowManagerCmd::InnerPosition => return Ok(window.inner_position()?.into()), - WindowManagerCmd::OuterPosition => return Ok(window.outer_position()?.into()), - 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::IsFocused => return Ok(window.is_focused()?.into()), - WindowManagerCmd::IsDecorated => return Ok(window.is_decorated()?.into()), - WindowManagerCmd::IsResizable => return Ok(window.is_resizable()?.into()), - WindowManagerCmd::IsMaximizable => return Ok(window.is_maximizable()?.into()), - WindowManagerCmd::IsMinimizable => return Ok(window.is_minimizable()?.into()), - WindowManagerCmd::IsClosable => return Ok(window.is_closable()?.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()), - WindowManagerCmd::Theme => return Ok(window.theme()?.into()), - // Setters - #[cfg(window_center)] - WindowManagerCmd::Center => window.center()?, - #[cfg(window_request_user_attention)] - WindowManagerCmd::RequestUserAttention(request_type) => { - window.request_user_attention(request_type)? - } - #[cfg(window_set_resizable)] - WindowManagerCmd::SetResizable(resizable) => window.set_resizable(resizable)?, - #[cfg(window_set_maximizable)] - WindowManagerCmd::SetMaximizable(maximizable) => window.set_maximizable(maximizable)?, - #[cfg(window_set_minimizable)] - WindowManagerCmd::SetMinimizable(minimizable) => window.set_minimizable(minimizable)?, - #[cfg(window_set_closable)] - WindowManagerCmd::SetClosable(closable) => window.set_closable(closable)?, - #[cfg(window_set_title)] - WindowManagerCmd::SetTitle(title) => window.set_title(&title)?, - #[cfg(window_maximize)] - WindowManagerCmd::Maximize => window.maximize()?, - #[cfg(window_unmaximize)] - WindowManagerCmd::Unmaximize => window.unmaximize()?, - #[cfg(all(window_maximize, window_unmaximize))] - WindowManagerCmd::ToggleMaximize => match window.is_maximized()? { - true => window.unmaximize()?, - false => window.maximize()?, - }, - #[cfg(window_minimize)] - WindowManagerCmd::Minimize => window.minimize()?, - #[cfg(window_unminimize)] - WindowManagerCmd::Unminimize => window.unminimize()?, - #[cfg(window_show)] - WindowManagerCmd::Show => window.show()?, - #[cfg(window_hide)] - WindowManagerCmd::Hide => window.hide()?, - #[cfg(window_close)] - WindowManagerCmd::Close => window.close()?, - #[cfg(window_set_decorations)] - 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)] - WindowManagerCmd::SetMinSize(size) => window.set_min_size(size)?, - #[cfg(window_set_max_size)] - WindowManagerCmd::SetMaxSize(size) => window.set_max_size(size)?, - #[cfg(window_set_position)] - WindowManagerCmd::SetPosition(position) => window.set_position(position)?, - #[cfg(window_set_fullscreen)] - WindowManagerCmd::SetFullscreen(fullscreen) => window.set_fullscreen(fullscreen)?, - #[cfg(window_set_focus)] - WindowManagerCmd::SetFocus => window.set_focus()?, - #[cfg(window_set_icon)] - WindowManagerCmd::SetIcon { icon } => window.set_icon(icon.into())?, - #[cfg(window_set_skip_taskbar)] - WindowManagerCmd::SetSkipTaskbar(skip) => window.set_skip_taskbar(skip)?, - #[cfg(window_set_cursor_grab)] - WindowManagerCmd::SetCursorGrab(grab) => window.set_cursor_grab(grab)?, - #[cfg(window_set_cursor_visible)] - WindowManagerCmd::SetCursorVisible(visible) => window.set_cursor_visible(visible)?, - #[cfg(window_set_cursor_icon)] - WindowManagerCmd::SetCursorIcon(icon) => window.set_cursor_icon(icon)?, - #[cfg(window_set_cursor_position)] - WindowManagerCmd::SetCursorPosition(position) => window.set_cursor_position(position)?, - #[cfg(window_set_ignore_cursor_events)] - WindowManagerCmd::SetIgnoreCursorEvents(ignore_cursor) => { - window.set_ignore_cursor_events(ignore_cursor)? - } - #[cfg(window_start_dragging)] - WindowManagerCmd::StartDragging => window.start_dragging()?, - #[cfg(window_print)] - WindowManagerCmd::Print => window.print()?, - // internals - #[cfg(all(window_maximize, window_unmaximize))] - WindowManagerCmd::InternalToggleMaximize => { - if window.is_resizable()? { - match window.is_maximized()? { - true => window.unmaximize()?, - false => window.maximize()?, - } - } - } - #[cfg(any(debug_assertions, feature = "devtools"))] - WindowManagerCmd::InternalToggleDevtools => { - if window.is_devtools_open() { - window.close_devtools(); - } else { - window.open_devtools(); - } - } - } - #[allow(unreachable_code)] - Ok(().into()) - } -} diff --git a/core/tauri/src/error.rs b/core/tauri/src/error.rs index f9c1b3e1942..8de7cec0d01 100644 --- a/core/tauri/src/error.rs +++ b/core/tauri/src/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::{fmt, path::PathBuf}; +use std::fmt; /// A generic boxed error. #[derive(Debug)] @@ -34,57 +34,30 @@ pub enum Error { /// Runtime error. #[error("runtime error: {0}")] Runtime(#[from] tauri_runtime::Error), - /// Failed to create window. - #[error("failed to create window")] - CreateWindow, /// Window label must be unique. #[error("a window with label `{0}` already exists")] WindowLabelAlreadyExists(String), - /// Can't access webview dispatcher because the webview was closed or not found. - #[error("webview not found: invalid label or it was closed")] - WebviewNotFound, - /// Failed to send message to webview. - #[error("failed to send message to the webview")] - FailedToSendMessage, /// Embedded asset not found. #[error("asset not found: {0}")] AssetNotFound(String), /// Failed to serialize/deserialize. #[error("JSON error: {0}")] - Json(serde_json::Error), - /// Unknown API type. - #[error("unknown API: {0:?}")] - UnknownApi(Option), + Json(#[from] serde_json::Error), /// Failed to execute tauri API. #[error("failed to execute API: {0}")] FailedToExecuteApi(#[from] crate::api::Error), /// IO error. #[error("{0}")] Io(#[from] std::io::Error), - /// Failed to decode base64. - #[cfg(feature = "updater")] - #[error("Failed to decode base64 string: {0}")] - Base64Decode(#[from] base64::DecodeError), /// Failed to load window icon. #[error("invalid icon: {0}")] InvalidIcon(std::io::Error), - /// Client with specified ID not found. - #[error("http client dropped or not initialized")] - HttpClientNotInitialized, - /// API not whitelisted on tauri.conf.json - #[error("'{0}' not in the allowlist (https://tauri.app/docs/api/config#tauri.allowlist)")] - ApiNotAllowlisted(String), /// Invalid args when running a command. #[error("invalid args `{1}` for command `{0}`: {2}")] InvalidArgs(&'static str, &'static str, serde_json::Error), /// Encountered an error in the setup hook, #[error("error encountered during setup hook: {0}")] Setup(SetupError), - /// Tauri updater error. - #[cfg(updater)] - #[cfg_attr(doc_cfg, doc(cfg(feature = "updater")))] - #[error("Updater: {0}")] - TauriUpdater(#[from] crate::updater::Error), /// Error initializing plugin. #[error("failed to initialize plugin `{0}`: {1}")] PluginInitialization(String, String), @@ -95,25 +68,6 @@ pub enum Error { /// Task join error. #[error(transparent)] JoinError(#[from] tokio::task::JoinError), - /// Path not allowed by the scope. - #[error("path not allowed on the configured scope: {0}")] - PathNotAllowed(PathBuf), - /// The user did not allow sending notifications. - #[error("sending notification was not allowed by the user")] - NotificationNotAllowed, - /// URL not allowed by the scope. - #[error("url not allowed on the configured scope: {0}")] - UrlNotAllowed(url::Url), - /// Sidecar not allowed by the configuration. - #[error("sidecar not configured under `tauri.conf.json > tauri > bundle > externalBin`: {0}")] - SidecarNotAllowed(PathBuf), - /// Sidecar was not found by the configuration. - #[cfg(shell_scope)] - #[error("sidecar configuration found, but unable to create a path to it: {0}")] - SidecarNotFound(#[from] Box), - /// Program not allowed by the scope. - #[error("program not allowed on the configured shell scope: {0}")] - ProgramNotAllowed(PathBuf), /// An error happened inside the isolation pattern. #[cfg(feature = "isolation")] #[error("isolation pattern error: {0}")] @@ -131,25 +85,29 @@ pub enum Error { /// The Window's raw handle is invalid for the platform. #[error("Unexpected `raw_window_handle` for the current platform")] InvalidWindowHandle, -} - -pub(crate) fn into_anyhow(err: T) -> anyhow::Error { - anyhow::anyhow!(err.to_string()) -} - -impl Error { - #[allow(dead_code)] - pub(crate) fn into_anyhow(self) -> anyhow::Error { - anyhow::anyhow!(self.to_string()) - } -} - -impl From for Error { - fn from(error: serde_json::Error) -> Self { - if error.to_string().contains("unknown variant") { - Self::UnknownApi(Some(error)) - } else { - Self::Json(error) - } - } + /// JNI error. + #[cfg(target_os = "android")] + #[error("jni error: {0}")] + Jni(#[from] jni::errors::Error), + /// Failed to receive message . + #[error("failed to receive message")] + FailedToReceiveMessage, + /// Menu error. + #[error("menu error: {0}")] + #[cfg(desktop)] + Menu(#[from] muda::Error), + /// Bad menu icon error. + #[error(transparent)] + #[cfg(desktop)] + BadMenuIcon(#[from] muda::BadIcon), + /// Tray icon error. + #[error("tray icon error: {0}")] + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))] + Tray(#[from] tray_icon::Error), + /// Bad tray icon error. + #[error(transparent)] + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))] + BadTrayIcon(#[from] tray_icon::BadIcon), } diff --git a/core/tauri/src/event/commands.rs b/core/tauri/src/event/commands.rs new file mode 100644 index 00000000000..d710a84a027 --- /dev/null +++ b/core/tauri/src/event/commands.rs @@ -0,0 +1,90 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{command, ipc::CallbackFn, Manager, Result, Runtime, Window}; +use serde::{Deserialize, Deserializer}; +use serde_json::Value as JsonValue; +use tauri_runtime::window::is_label_valid; + +use super::is_event_name_valid; + +pub struct EventId(String); + +impl<'de> Deserialize<'de> for EventId { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let event_id = String::deserialize(deserializer)?; + if is_event_name_valid(&event_id) { + Ok(EventId(event_id)) + } else { + Err(serde::de::Error::custom( + "Event name must include only alphanumeric characters, `-`, `/`, `:` and `_`.", + )) + } + } +} + +pub struct WindowLabel(String); + +impl<'de> Deserialize<'de> for WindowLabel { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let event_id = String::deserialize(deserializer)?; + if is_label_valid(&event_id) { + Ok(WindowLabel(event_id)) + } else { + Err(serde::de::Error::custom( + "Window label must include only alphanumeric characters, `-`, `/`, `:` and `_`.", + )) + } + } +} + +#[command(root = "crate")] +pub fn listen( + window: Window, + event: EventId, + window_label: Option, + handler: CallbackFn, +) -> Result { + window.listen_js(window_label.map(|l| l.0), event.0, handler) +} + +#[command(root = "crate")] +pub fn unlisten(window: Window, event: EventId, event_id: usize) -> Result<()> { + window.unlisten_js(event.0, event_id) +} + +#[command(root = "crate")] +pub fn emit( + window: Window, + event: EventId, + window_label: Option, + payload: Option, +) -> Result<()> { + // dispatch the event to Rust listeners + window.trigger( + &event.0, + payload.as_ref().and_then(|p| { + serde_json::to_string(&p) + .map_err(|e| { + #[cfg(debug_assertions)] + eprintln!("{e}"); + e + }) + .ok() + }), + ); + + // emit event to JS + if let Some(target) = window_label { + window.emit_to(&target.0, &event.0, payload) + } else { + window.emit_all(&event.0, payload) + } +} diff --git a/core/tauri/src/event.rs b/core/tauri/src/event/listener.rs similarity index 73% rename from core/tauri/src/event.rs rename to core/tauri/src/event/listener.rs index 68d173f5a07..db1263cf3da 100644 --- a/core/tauri/src/event.rs +++ b/core/tauri/src/event/listener.rs @@ -2,59 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +use super::{Event, EventHandler}; + use std::{ boxed::Box, cell::Cell, collections::HashMap, - fmt, - hash::Hash, sync::{Arc, Mutex}, }; use uuid::Uuid; -/// Checks if an event name is valid. -pub fn is_event_name_valid(event: &str) -> bool { - event - .chars() - .all(|c| c.is_alphanumeric() || c == '-' || c == '/' || c == ':' || c == '_') -} - -pub fn assert_event_name_is_valid(event: &str) { - assert!( - is_event_name_valid(event), - "Event name must include only alphanumeric characters, `-`, `/`, `:` and `_`." - ); -} - -/// Represents an event handler. -#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub struct EventHandler(Uuid); - -impl fmt::Display for EventHandler { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -/// An event that was triggered. -#[derive(Debug, Clone)] -pub struct Event { - id: EventHandler, - data: Option, -} - -impl Event { - /// The [`EventHandler`] that was triggered. - pub fn id(&self) -> EventHandler { - self.id - } - - /// The event payload. - pub fn payload(&self) -> Option<&str> { - self.data.as_deref() - } -} - /// What to do with the pending handler when resolving it? enum Pending { Unlisten(EventHandler), @@ -77,7 +34,7 @@ struct InnerListeners { } /// A self-contained event manager. -pub(crate) struct Listeners { +pub struct Listeners { inner: Arc, } @@ -284,7 +241,6 @@ mod test { #[test] fn check_on_event(key in "[a-z]+", d in "[a-z]+") { let listeners: Listeners = Default::default(); - // call listen with e and the event_fn dummy func listeners.listen(key.clone(), None, event_fn); // call on event with e and d. @@ -298,54 +254,3 @@ mod test { } } } - -pub fn unlisten_js(listeners_object_name: String, event_name: String, event_id: u32) -> String { - format!( - " - (function () {{ - const listeners = (window['{listeners_object_name}'] || {{}})['{event_name}'] - if (listeners) {{ - const index = window['{listeners_object_name}']['{event_name}'].findIndex(e => e.id === {event_id}) - if (index > -1) {{ - window['{listeners_object_name}']['{event_name}'].splice(index, 1) - }} - }} - }})() - ", - ) -} - -pub fn listen_js( - listeners_object_name: String, - event: String, - event_id: u32, - window_label: Option, - handler: String, -) -> String { - format!( - " - (function () {{ - if (window['{listeners}'] === void 0) {{ - Object.defineProperty(window, '{listeners}', {{ value: Object.create(null) }}); - }} - if (window['{listeners}'][{event}] === void 0) {{ - Object.defineProperty(window['{listeners}'], {event}, {{ value: [] }}); - }} - const eventListeners = window['{listeners}'][{event}] - const listener = {{ - id: {event_id}, - windowLabel: {window_label}, - handler: {handler} - }}; - eventListeners.push(listener); - }})() - ", - listeners = listeners_object_name, - window_label = if let Some(l) = window_label { - crate::runtime::window::assert_label_is_valid(&l); - format!("'{l}'") - } else { - "null".to_owned() - }, - ) -} diff --git a/core/tauri/src/event/mod.rs b/core/tauri/src/event/mod.rs new file mode 100644 index 00000000000..ddc351d5486 --- /dev/null +++ b/core/tauri/src/event/mod.rs @@ -0,0 +1,120 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{fmt, hash::Hash}; +use uuid::Uuid; + +mod commands; +mod listener; +pub(crate) use listener::Listeners; + +use crate::{ + plugin::{Builder, TauriPlugin}, + Runtime, +}; + +/// Checks if an event name is valid. +pub fn is_event_name_valid(event: &str) -> bool { + event + .chars() + .all(|c| c.is_alphanumeric() || c == '-' || c == '/' || c == ':' || c == '_') +} + +pub fn assert_event_name_is_valid(event: &str) { + assert!( + is_event_name_valid(event), + "Event name must include only alphanumeric characters, `-`, `/`, `:` and `_`." + ); +} + +/// Represents an event handler. +#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct EventHandler(Uuid); + +impl fmt::Display for EventHandler { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +/// An event that was triggered. +#[derive(Debug, Clone)] +pub struct Event { + id: EventHandler, + data: Option, +} + +impl Event { + /// The [`EventHandler`] that was triggered. + pub fn id(&self) -> EventHandler { + self.id + } + + /// The event payload. + pub fn payload(&self) -> Option<&str> { + self.data.as_deref() + } +} + +/// Initializes the event plugin. +pub(crate) fn init() -> TauriPlugin { + Builder::new("event") + .invoke_handler(crate::generate_handler![ + commands::listen, + commands::unlisten, + commands::emit, + ]) + .build() +} + +pub fn unlisten_js(listeners_object_name: String, event_name: String, event_id: usize) -> String { + format!( + " + (function () {{ + const listeners = (window['{listeners_object_name}'] || {{}})['{event_name}'] + if (listeners) {{ + const index = window['{listeners_object_name}']['{event_name}'].findIndex(e => e.id === {event_id}) + if (index > -1) {{ + window['{listeners_object_name}']['{event_name}'].splice(index, 1) + }} + }} + }})() + ", + ) +} + +pub fn listen_js( + listeners_object_name: String, + event: String, + event_id: usize, + window_label: Option, + handler: String, +) -> String { + format!( + " + (function () {{ + if (window['{listeners}'] === void 0) {{ + Object.defineProperty(window, '{listeners}', {{ value: Object.create(null) }}); + }} + if (window['{listeners}'][{event}] === void 0) {{ + Object.defineProperty(window['{listeners}'], {event}, {{ value: [] }}); + }} + const eventListeners = window['{listeners}'][{event}] + const listener = {{ + id: {event_id}, + windowLabel: {window_label}, + handler: {handler} + }}; + eventListeners.push(listener); + }})() + ", + listeners = listeners_object_name, + window_label = if let Some(l) = window_label { + crate::runtime::window::assert_label_is_valid(&l); + format!("'{l}'") + } else { + "null".to_owned() + }, + ) +} diff --git a/core/tauri/src/hooks.rs b/core/tauri/src/hooks.rs deleted file mode 100644 index fb9dea03bca..00000000000 --- a/core/tauri/src/hooks.rs +++ /dev/null @@ -1,346 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use crate::{ - api::ipc::{format_callback, format_callback_result, CallbackFn}, - app::App, - Runtime, StateManager, Window, -}; -use serde::{Deserialize, Serialize}; -use serde_json::Value as JsonValue; -use serialize_to_javascript::{default_template, Template}; -use std::{future::Future, sync::Arc}; - -use tauri_macros::default_runtime; - -/// A closure that is run when the Tauri application is setting up. -pub type SetupHook = - Box) -> Result<(), Box> + Send>; - -/// A closure that is run every time Tauri receives a message it doesn't explicitly handle. -pub type InvokeHandler = dyn Fn(Invoke) + Send + Sync + 'static; - -/// A closure that is responsible for respond a JS message. -pub type InvokeResponder = - dyn Fn(Window, InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static; - -/// A closure that is run once every time a window is created and loaded. -pub type OnPageLoad = dyn Fn(Window, PageLoadPayload) + Send + Sync + 'static; - -// todo: why is this derive broken but the output works manually? -#[derive(Template)] -#[default_template("../scripts/ipc.js")] -pub(crate) struct IpcJavascript<'a> { - pub(crate) isolation_origin: &'a str, -} - -#[cfg(feature = "isolation")] -#[derive(Template)] -#[default_template("../scripts/isolation.js")] -pub(crate) struct IsolationJavascript<'a> { - pub(crate) isolation_src: &'a str, - pub(crate) style: &'a str, -} - -/// The payload for the [`OnPageLoad`] hook. -#[derive(Debug, Clone, Deserialize)] -pub struct PageLoadPayload { - url: String, -} - -impl PageLoadPayload { - /// The page URL. - pub fn url(&self) -> &str { - &self.url - } -} - -/// The payload used on the IPC invoke. -#[derive(Debug, Deserialize)] -pub struct InvokePayload { - /// The invoke command. - pub cmd: String, - #[serde(rename = "__tauriModule")] - #[doc(hidden)] - pub tauri_module: Option, - /// The success callback. - pub callback: CallbackFn, - /// The error callback. - pub error: CallbackFn, - /// The payload of the message. - #[serde(flatten)] - pub inner: JsonValue, -} - -/// The message and resolver given to a custom command. -#[default_runtime(crate::Wry, wry)] -#[derive(Debug)] -pub struct Invoke { - /// The message passed. - pub message: InvokeMessage, - - /// The resolver of the message. - pub resolver: InvokeResolver, -} - -/// Error response from an [`InvokeMessage`]. -#[derive(Debug)] -pub struct InvokeError(JsonValue); - -impl InvokeError { - /// Create an [`InvokeError`] as a string of the [`serde_json::Error`] message. - #[inline(always)] - pub fn from_serde_json(error: serde_json::Error) -> Self { - Self(JsonValue::String(error.to_string())) - } - - /// Create an [`InvokeError`] as a string of the [`anyhow::Error`] message. - #[inline(always)] - pub fn from_anyhow(error: anyhow::Error) -> Self { - Self(JsonValue::String(format!("{error:#}"))) - } -} - -impl From for InvokeError { - #[inline] - fn from(value: T) -> Self { - serde_json::to_value(value) - .map(Self) - .unwrap_or_else(Self::from_serde_json) - } -} - -impl From for InvokeError { - #[inline(always)] - fn from(error: crate::Error) -> Self { - Self(JsonValue::String(error.to_string())) - } -} - -/// Response from a [`InvokeMessage`] passed to the [`InvokeResolver`]. -#[derive(Debug)] -pub enum InvokeResponse { - /// Resolve the promise. - Ok(JsonValue), - /// Reject the promise. - Err(InvokeError), -} - -impl InvokeResponse { - /// Turn a [`InvokeResponse`] back into a serializable result. - #[inline(always)] - pub fn into_result(self) -> Result { - match self { - Self::Ok(v) => Ok(v), - Self::Err(e) => Err(e.0), - } - } -} - -impl From> for InvokeResponse { - #[inline] - fn from(result: Result) -> Self { - match result { - Ok(ok) => match serde_json::to_value(ok) { - Ok(value) => Self::Ok(value), - Err(err) => Self::Err(InvokeError::from_serde_json(err)), - }, - Err(err) => Self::Err(err), - } - } -} - -impl From for InvokeResponse { - fn from(error: InvokeError) -> Self { - Self::Err(error) - } -} - -/// Resolver of a invoke message. -#[default_runtime(crate::Wry, wry)] -#[derive(Debug)] -pub struct InvokeResolver { - window: Window, - pub(crate) callback: CallbackFn, - pub(crate) error: CallbackFn, -} - -impl InvokeResolver { - pub(crate) fn new(window: Window, callback: CallbackFn, error: CallbackFn) -> Self { - Self { - window, - callback, - error, - } - } - - /// Reply to the invoke promise with an async task. - pub fn respond_async(self, task: F) - where - T: Serialize, - F: Future> + Send + 'static, - { - crate::async_runtime::spawn(async move { - Self::return_task(self.window, task, self.callback, self.error).await; - }); - } - - /// Reply to the invoke promise with an async task which is already serialized. - pub fn respond_async_serialized(self, task: F) - where - F: Future> + Send + 'static, - { - crate::async_runtime::spawn(async move { - let response = match task.await { - Ok(ok) => InvokeResponse::Ok(ok), - Err(err) => InvokeResponse::Err(err), - }; - Self::return_result(self.window, response, self.callback, self.error) - }); - } - - /// Reply to the invoke promise with a serializable value. - pub fn respond(self, value: Result) { - Self::return_result(self.window, value.into(), self.callback, self.error) - } - - /// Resolve the invoke promise with a value. - pub fn resolve(self, value: T) { - Self::return_result(self.window, Ok(value).into(), self.callback, self.error) - } - - /// Reject the invoke promise with a value. - pub fn reject(self, value: T) { - Self::return_result( - self.window, - Result::<(), _>::Err(value.into()).into(), - self.callback, - self.error, - ) - } - - /// Reject the invoke promise with an [`InvokeError`]. - pub fn invoke_error(self, error: InvokeError) { - Self::return_result(self.window, error.into(), self.callback, self.error) - } - - /// Asynchronously executes the given task - /// and evaluates its Result to the JS promise described by the `success_callback` and `error_callback` function names. - /// - /// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value. - /// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value. - pub async fn return_task( - window: Window, - task: F, - success_callback: CallbackFn, - error_callback: CallbackFn, - ) where - T: Serialize, - F: Future> + Send + 'static, - { - let result = task.await; - Self::return_closure(window, || result, success_callback, error_callback) - } - - pub(crate) fn return_closure Result>( - window: Window, - f: F, - success_callback: CallbackFn, - error_callback: CallbackFn, - ) { - Self::return_result(window, f().into(), success_callback, error_callback) - } - - pub(crate) fn return_result( - window: Window, - response: InvokeResponse, - success_callback: CallbackFn, - error_callback: CallbackFn, - ) { - (window.invoke_responder())(window, response, success_callback, error_callback); - } -} - -pub fn window_invoke_responder( - window: Window, - response: InvokeResponse, - success_callback: CallbackFn, - error_callback: CallbackFn, -) { - let callback_string = - match format_callback_result(response.into_result(), success_callback, error_callback) { - Ok(callback_string) => callback_string, - Err(e) => format_callback(error_callback, &e.to_string()) - .expect("unable to serialize response string to json"), - }; - - let _ = window.eval(&callback_string); -} - -/// An invoke message. -#[default_runtime(crate::Wry, wry)] -#[derive(Debug)] -pub struct InvokeMessage { - /// The window that received the invoke message. - pub(crate) window: Window, - /// Application managed state. - pub(crate) state: Arc, - /// The IPC command. - pub(crate) command: String, - /// The JSON argument passed on the invoke message. - pub(crate) payload: JsonValue, -} - -impl InvokeMessage { - /// Create an new [`InvokeMessage`] from a payload send to a window. - pub(crate) fn new( - window: Window, - state: Arc, - command: String, - payload: JsonValue, - ) -> Self { - Self { - window, - state, - command, - payload, - } - } - - /// The invoke command. - #[inline(always)] - pub fn command(&self) -> &str { - &self.command - } - - /// The window that received the invoke. - #[inline(always)] - pub fn window(&self) -> Window { - self.window.clone() - } - - /// A reference to window that received the invoke. - #[inline(always)] - pub fn window_ref(&self) -> &Window { - &self.window - } - - /// A reference to the payload the invoke received. - #[inline(always)] - pub fn payload(&self) -> &JsonValue { - &self.payload - } - - /// The state manager associated with the application - #[inline(always)] - pub fn state(&self) -> Arc { - self.state.clone() - } - - /// A reference to the state manager associated with application. - #[inline(always)] - pub fn state_ref(&self) -> &StateManager { - &self.state - } -} diff --git a/core/tauri/src/ios.rs b/core/tauri/src/ios.rs new file mode 100644 index 00000000000..16bb3c6e5cf --- /dev/null +++ b/core/tauri/src/ios.rs @@ -0,0 +1,180 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use cocoa::base::{id, nil, NO, YES}; +use objc::*; +use serde_json::Value as JsonValue; +use swift_rs::{swift, SRString, SwiftArg}; + +use std::{ + ffi::c_void, + os::raw::{c_char, c_int, c_ulonglong}, +}; + +type PluginMessageCallbackFn = unsafe extern "C" fn(c_int, c_int, *const c_char); +pub struct PluginMessageCallback(pub PluginMessageCallbackFn); + +impl<'a> SwiftArg<'a> for PluginMessageCallback { + type ArgType = PluginMessageCallbackFn; + + unsafe fn as_arg(&'a self) -> Self::ArgType { + self.0 + } +} + +type ChannelSendDataCallbackFn = unsafe extern "C" fn(c_ulonglong, *const c_char); +pub struct ChannelSendDataCallback(pub ChannelSendDataCallbackFn); + +impl<'a> SwiftArg<'a> for ChannelSendDataCallback { + type ArgType = ChannelSendDataCallbackFn; + + unsafe fn as_arg(&'a self) -> Self::ArgType { + self.0 + } +} + +swift!(pub fn run_plugin_command( + id: i32, + name: &SRString, + method: &SRString, + data: *const c_void, + callback: PluginMessageCallback, + send_channel_data_callback: ChannelSendDataCallback +)); +swift!(pub fn register_plugin( + name: &SRString, + plugin: *const c_void, + config: *const c_void, + webview: *const c_void +)); +swift!(pub fn on_webview_created(webview: *const c_void, controller: *const c_void)); + +pub fn json_to_dictionary(json: &JsonValue) -> id { + if let serde_json::Value::Object(map) = json { + unsafe { + let dictionary: id = msg_send![class!(NSMutableDictionary), alloc]; + let data: id = msg_send![dictionary, init]; + for (key, value) in map { + add_json_entry_to_dictionary(data, key, value); + } + data + } + } else { + nil + } +} + +const UTF8_ENCODING: usize = 4; + +struct NSString(id); + +impl NSString { + fn new(s: &str) -> Self { + // Safety: objc runtime calls are unsafe + NSString(unsafe { + let ns_string: id = msg_send![class!(NSString), alloc]; + let ns_string: id = msg_send![ns_string, + initWithBytes:s.as_ptr() + length:s.len() + encoding:UTF8_ENCODING]; + + // The thing is allocated in rust, the thing must be set to autorelease in rust to relinquish control + // or it can not be released correctly in OC runtime + let _: () = msg_send![ns_string, autorelease]; + + ns_string + }) + } +} + +unsafe fn add_json_value_to_array(array: id, value: &JsonValue) { + match value { + JsonValue::Null => { + let null: id = msg_send![class!(NSNull), null]; + let () = msg_send![array, addObject: null]; + } + JsonValue::Bool(val) => { + let value = if *val { YES } else { NO }; + let v: id = msg_send![class!(NSNumber), numberWithBool: value]; + let () = msg_send![array, addObject: v]; + } + JsonValue::Number(val) => { + let number: id = if let Some(v) = val.as_i64() { + msg_send![class!(NSNumber), numberWithInteger: v] + } else if let Some(v) = val.as_u64() { + msg_send![class!(NSNumber), numberWithUnsignedLongLong: v] + } else if let Some(v) = val.as_f64() { + msg_send![class!(NSNumber), numberWithDouble: v] + } else { + unreachable!() + }; + let () = msg_send![array, addObject: number]; + } + JsonValue::String(val) => { + let () = msg_send![array, addObject: NSString::new(val)]; + } + JsonValue::Array(val) => { + let nsarray: id = msg_send![class!(NSMutableArray), alloc]; + let inner_array: id = msg_send![nsarray, init]; + for value in val { + add_json_value_to_array(inner_array, value); + } + let () = msg_send![array, addObject: inner_array]; + } + JsonValue::Object(val) => { + let dictionary: id = msg_send![class!(NSMutableDictionary), alloc]; + let data: id = msg_send![dictionary, init]; + for (key, value) in val { + add_json_entry_to_dictionary(data, key, value); + } + let () = msg_send![array, addObject: data]; + } + } +} + +unsafe fn add_json_entry_to_dictionary(data: id, key: &str, value: &JsonValue) { + let key = NSString::new(key); + match value { + JsonValue::Null => { + let null: id = msg_send![class!(NSNull), null]; + let () = msg_send![data, setObject:null forKey: key]; + } + JsonValue::Bool(val) => { + let flag = if *val { YES } else { NO }; + let value: id = msg_send![class!(NSNumber), numberWithBool: flag]; + let () = msg_send![data, setObject:value forKey: key]; + } + JsonValue::Number(val) => { + let number: id = if let Some(v) = val.as_i64() { + msg_send![class!(NSNumber), numberWithInteger: v] + } else if let Some(v) = val.as_u64() { + msg_send![class!(NSNumber), numberWithUnsignedLongLong: v] + } else if let Some(v) = val.as_f64() { + msg_send![class!(NSNumber), numberWithDouble: v] + } else { + unreachable!() + }; + let () = msg_send![data, setObject:number forKey: key]; + } + JsonValue::String(val) => { + let () = msg_send![data, setObject:NSString::new(val) forKey: key]; + } + JsonValue::Array(val) => { + let nsarray: id = msg_send![class!(NSMutableArray), alloc]; + let array: id = msg_send![nsarray, init]; + for value in val { + add_json_value_to_array(array, value); + } + let () = msg_send![data, setObject:array forKey: key]; + } + JsonValue::Object(val) => { + let dictionary: id = msg_send![class!(NSMutableDictionary), alloc]; + let inner_data: id = msg_send![dictionary, init]; + for (key, value) in val { + add_json_entry_to_dictionary(inner_data, key, value); + } + let () = msg_send![data, setObject:inner_data forKey: key]; + } + } +} diff --git a/core/tauri/src/ipc/channel.rs b/core/tauri/src/ipc/channel.rs new file mode 100644 index 00000000000..e09a8d922b5 --- /dev/null +++ b/core/tauri/src/ipc/channel.rs @@ -0,0 +1,151 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +use serde::{Deserialize, Serialize, Serializer}; + +use crate::{ + command, + command::{CommandArg, CommandItem}, + plugin::{Builder as PluginBuilder, TauriPlugin}, + Manager, Runtime, State, Window, +}; + +use super::{CallbackFn, InvokeBody, InvokeError, IpcResponse, Request, Response}; + +pub const IPC_PAYLOAD_PREFIX: &str = "__CHANNEL__:"; +pub const CHANNEL_PLUGIN_NAME: &str = "__TAURI_CHANNEL__"; +// TODO: ideally this const references CHANNEL_PLUGIN_NAME +pub const FETCH_CHANNEL_DATA_COMMAND: &str = "plugin:__TAURI_CHANNEL__|fetch"; +pub(crate) const CHANNEL_ID_HEADER_NAME: &str = "Tauri-Channel-Id"; + +/// Maps a channel id to a pending data that must be send to the JavaScript side via the IPC. +#[derive(Default, Clone)] +pub struct ChannelDataIpcQueue(pub(crate) Arc>>); + +/// An IPC channel. +#[derive(Clone)] +pub struct Channel { + id: u32, + on_message: Arc crate::Result<()> + Send + Sync>, +} + +impl Serialize for Channel { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{IPC_PAYLOAD_PREFIX}{}", self.id)) + } +} + +impl Channel { + /// Creates a new channel with the given message handler. + pub fn new crate::Result<()> + Send + Sync + 'static>( + on_message: F, + ) -> Self { + Self::_new(rand::random(), on_message) + } + + pub(crate) fn _new crate::Result<()> + Send + Sync + 'static>( + id: u32, + on_message: F, + ) -> Self { + #[allow(clippy::let_and_return)] + let channel = Self { + id, + on_message: Arc::new(on_message), + }; + + #[cfg(mobile)] + crate::plugin::mobile::register_channel(channel.clone()); + + channel + } + + pub(crate) fn from_ipc(window: Window, callback: CallbackFn) -> Self { + Channel::_new(callback.0, move |body| { + let data_id = rand::random(); + window + .state::() + .0 + .lock() + .unwrap() + .insert(data_id, body); + window.eval(&format!( + "__TAURI_INVOKE__('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': {data_id} }} }}).then(window['_' + {}]).catch(console.error)", + callback.0 + )) + }) + } + + pub(crate) fn load_from_ipc( + window: Window, + value: impl AsRef, + ) -> Option { + value + .as_ref() + .split_once(IPC_PAYLOAD_PREFIX) + .and_then(|(_prefix, id)| id.parse().ok()) + .map(|callback_id| Self::from_ipc(window, CallbackFn(callback_id))) + } + + /// The channel identifier. + pub fn id(&self) -> u32 { + self.id + } + + /// Sends the given data through the channel. + pub fn send(&self, data: T) -> crate::Result<()> { + let body = data.body()?; + (self.on_message)(body) + } +} + +impl<'de, R: Runtime> CommandArg<'de, R> for Channel { + /// Grabs the [`Window`] from the [`CommandItem`] and returns the associated [`Channel`]. + fn from_command(command: CommandItem<'de, R>) -> Result { + let name = command.name; + let arg = command.key; + let window = command.message.window(); + let value: String = + Deserialize::deserialize(command).map_err(|e| crate::Error::InvalidArgs(name, arg, e))?; + Channel::load_from_ipc(window, &value).ok_or_else(|| { + InvokeError::from_anyhow(anyhow::anyhow!( + "invalid channel value `{value}`, expected a string in the `{IPC_PAYLOAD_PREFIX}ID` format" + )) + }) + } +} + +#[command(root = "crate")] +fn fetch( + request: Request<'_>, + cache: State<'_, ChannelDataIpcQueue>, +) -> Result { + if let Some(id) = request + .headers() + .get(CHANNEL_ID_HEADER_NAME) + .and_then(|v| v.to_str().ok()) + .and_then(|id| id.parse().ok()) + { + if let Some(data) = cache.0.lock().unwrap().remove(&id) { + Ok(Response::new(data)) + } else { + Err("data not found") + } + } else { + Err("missing channel id header") + } +} + +pub fn plugin() -> TauriPlugin { + PluginBuilder::new(CHANNEL_PLUGIN_NAME) + .invoke_handler(crate::generate_handler![fetch]) + .build() +} diff --git a/core/tauri/src/api/ipc.rs b/core/tauri/src/ipc/format_callback.rs similarity index 65% rename from core/tauri/src/api/ipc.rs rename to core/tauri/src/ipc/format_callback.rs index 95a511601d2..31d7cbc599d 100644 --- a/core/tauri/src/api/ipc.rs +++ b/core/tauri/src/ipc/format_callback.rs @@ -2,18 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -//! Types and functions related to Inter Procedure Call(IPC). -//! -//! This module includes utilities to send messages to the JS layer of the webview. - -use serde::{Deserialize, Serialize}; +use serde::Serialize; use serde_json::value::RawValue; -pub use serialize_to_javascript::Options as SerializeOptions; use serialize_to_javascript::Serialized; -/// The `Callback` type is the return value of the `transformCallback` JavaScript function. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] -pub struct CallbackFn(pub usize); +use super::CallbackFn; /// The information about this is quite limited. On Chrome/Edge and Firefox, [the maximum string size is approximately 1 GB](https://stackoverflow.com/a/34958490). /// @@ -47,22 +40,9 @@ const MIN_JSON_PARSE_LEN: usize = 10_240; /// 1. `serde_json`'s ability to correctly escape and format json into a string. /// 2. JavaScript engines not accepting anything except another unescaped, literal single quote /// character to end a string that was opened with it. -/// -/// # Examples -/// -/// ``` -/// use tauri::api::ipc::{serialize_js_with, SerializeOptions}; -/// #[derive(serde::Serialize)] -/// struct Foo { -/// bar: String, -/// } -/// let foo = Foo { bar: "x".repeat(20_000).into() }; -/// let value = serialize_js_with(&foo, SerializeOptions::default(), |v| format!("console.log({v})")).unwrap(); -/// assert_eq!(value, format!("console.log(JSON.parse('{{\"bar\":\"{}\"}}'))", foo.bar)); -/// ``` -pub fn serialize_js_with String>( +fn serialize_js_with String>( value: &T, - options: SerializeOptions, + options: serialize_to_javascript::Options, cb: F, ) -> crate::api::Result { // get a raw &str representation of a serialized json value. @@ -97,80 +77,13 @@ pub fn serialize_js_with String>( Ok(return_val) } -/// Transforms & escapes a JSON value. -/// -/// This is a convenience function for [`serialize_js_with`], simply allocating the result to a String. -/// -/// For usage in functions where performance is more important than code readability, see [`serialize_js_with`]. -/// -/// # Examples -/// ```rust,no_run -/// use tauri::{Manager, api::ipc::serialize_js}; -/// use serde::Serialize; -/// -/// #[derive(Serialize)] -/// struct Foo { -/// bar: String, -/// } -/// -/// #[derive(Serialize)] -/// struct Bar { -/// baz: u32, -/// } -/// -/// tauri::Builder::default() -/// .setup(|app| { -/// let window = app.get_window("main").unwrap(); -/// window.eval(&format!( -/// "console.log({}, {})", -/// serialize_js(&Foo { bar: "bar".to_string() }).unwrap(), -/// serialize_js(&Bar { baz: 0 }).unwrap()), -/// )?; -/// Ok(()) -/// }); -/// ``` -pub fn serialize_js(value: &T) -> crate::api::Result { - serialize_js_with(value, Default::default(), |v| v.into()) -} - /// Formats a function name and argument to be evaluated as callback. /// /// This will serialize primitive JSON types (e.g. booleans, strings, numbers, etc.) as JavaScript literals, /// but will serialize arrays and objects whose serialized JSON string is smaller than 1 GB and larger /// than 10 KiB with `JSON.parse('...')`. /// See [json-parse-benchmark](https://github.com/GoogleChromeLabs/json-parse-benchmark). -/// -/// # Examples -/// - With string literals: -/// ``` -/// use tauri::api::ipc::{CallbackFn, format_callback}; -/// // callback with a string argument -/// let cb = format_callback(CallbackFn(12345), &"the string response").unwrap(); -/// assert!(cb.contains(r#"window["_12345"]("the string response")"#)); -/// ``` -/// -/// - With types implement [`serde::Serialize`]: -/// ``` -/// use tauri::api::ipc::{CallbackFn, format_callback}; -/// use serde::Serialize; -/// -/// // callback with large JSON argument -/// #[derive(Serialize)] -/// struct MyResponse { -/// value: String -/// } -/// -/// let cb = format_callback( -/// CallbackFn(6789), -/// &MyResponse { value: String::from_utf8(vec![b'X'; 10_240]).unwrap() -/// }).expect("failed to serialize"); -/// -/// assert!(cb.contains(r#"window["_6789"](JSON.parse('{"value":"XXXXXXXXX"#)); -/// ``` -pub fn format_callback( - function_name: CallbackFn, - arg: &T, -) -> crate::api::Result { +pub fn format(function_name: CallbackFn, arg: &T) -> crate::api::Result { serialize_js_with(arg, Default::default(), |arg| { format!( r#" @@ -194,42 +107,33 @@ pub fn format_callback( /// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise. /// /// Note that the callback strings are automatically generated by the `invoke` helper. -/// -/// # Examples -/// ``` -/// use tauri::api::ipc::{CallbackFn, format_callback_result}; -/// let res: Result = Ok(5); -/// let cb = format_callback_result(res, CallbackFn(145), CallbackFn(0)).expect("failed to format"); -/// assert!(cb.contains(r#"window["_145"](5)"#)); -/// -/// let res: Result<&str, &str> = Err("error message here"); -/// let cb = format_callback_result(res, CallbackFn(2), CallbackFn(1)).expect("failed to format"); -/// assert!(cb.contains(r#"window["_1"]("error message here")"#)); -/// ``` -// TODO: better example to explain -pub fn format_callback_result( +pub fn format_result( result: Result, success_callback: CallbackFn, error_callback: CallbackFn, ) -> crate::api::Result { match result { - Ok(res) => format_callback(success_callback, &res), - Err(err) => format_callback(error_callback, &err), + Ok(res) => format(success_callback, &res), + Err(err) => format(error_callback, &err), } } #[cfg(test)] mod test { - use crate::api::ipc::*; + use super::*; use quickcheck::{Arbitrary, Gen}; use quickcheck_macros::quickcheck; impl Arbitrary for CallbackFn { fn arbitrary(g: &mut Gen) -> CallbackFn { - CallbackFn(usize::arbitrary(g)) + CallbackFn(u32::arbitrary(g)) } } + fn serialize_js(value: &T) -> crate::api::Result { + serialize_js_with(value, Default::default(), |v| v.into()) + } + #[test] fn test_serialize_js() { assert_eq!(serialize_js(&()).unwrap(), "null"); @@ -282,7 +186,7 @@ mod test { #[quickcheck] fn qc_formatting(f: CallbackFn, a: String) -> bool { // call format callback - let fc = format_callback(f, &a).unwrap(); + let fc = format(f, &a).unwrap(); fc.contains(&format!( r#"window["_{}"](JSON.parse('{}'))"#, f.0, @@ -294,11 +198,10 @@ mod test { )) } - // check arbitrary strings in format_callback_result + // check arbitrary strings in format_result #[quickcheck] fn qc_format_res(result: Result, c: CallbackFn, ec: CallbackFn) -> bool { - let resp = - format_callback_result(result.clone(), c, ec).expect("failed to format callback result"); + let resp = format_result(result.clone(), c, ec).expect("failed to format callback result"); let (function, value) = match result { Ok(v) => (c, v), Err(e) => (ec, e), diff --git a/core/tauri/src/ipc/mod.rs b/core/tauri/src/ipc/mod.rs new file mode 100644 index 00000000000..c55db11c2f0 --- /dev/null +++ b/core/tauri/src/ipc/mod.rs @@ -0,0 +1,493 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Types and functions related to Inter Procedure Call(IPC). +//! +//! This module includes utilities to send messages to the JS layer of the webview. + +use std::sync::Arc; + +use futures_util::Future; +use http::HeaderMap; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value as JsonValue; +pub use serialize_to_javascript::Options as SerializeOptions; +use tauri_macros::default_runtime; + +use crate::{ + command::{CommandArg, CommandItem}, + Runtime, StateManager, Window, +}; + +pub(crate) mod channel; +#[cfg(not(ipc_custom_protocol))] +pub(crate) mod format_callback; +pub(crate) mod protocol; + +pub use channel::Channel; + +/// A closure that is run every time Tauri receives a message it doesn't explicitly handle. +pub type InvokeHandler = dyn Fn(Invoke) -> bool + Send + Sync + 'static; + +/// A closure that is responsible for respond a JS message. +pub type InvokeResponder = + dyn Fn(Window, String, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static; +type OwnedInvokeResponder = + dyn Fn(Window, String, InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static; + +/// Possible values of an IPC payload. +#[derive(Debug, Clone)] +pub enum InvokeBody { + /// Json payload. + Json(JsonValue), + /// Bytes payload. + Raw(Vec), +} + +impl Default for InvokeBody { + fn default() -> Self { + Self::Json(Default::default()) + } +} + +impl From for InvokeBody { + fn from(value: JsonValue) -> Self { + Self::Json(value) + } +} + +impl From> for InvokeBody { + fn from(value: Vec) -> Self { + Self::Raw(value) + } +} + +impl IpcResponse for InvokeBody { + fn body(self) -> crate::Result { + Ok(self) + } +} + +impl InvokeBody { + #[allow(dead_code)] + pub(crate) fn into_json(self) -> JsonValue { + match self { + Self::Json(v) => v, + Self::Raw(v) => { + JsonValue::Array(v.into_iter().map(|n| JsonValue::Number(n.into())).collect()) + } + } + } + + /// Attempts to deserialize the invoke body. + pub fn deserialize(self) -> serde_json::Result { + match self { + InvokeBody::Json(v) => serde_json::from_value(v), + InvokeBody::Raw(v) => serde_json::from_slice(&v), + } + } +} + +/// The IPC request. +#[derive(Debug)] +pub struct Request<'a> { + body: &'a InvokeBody, + headers: &'a HeaderMap, +} + +impl<'a> Request<'a> { + /// The request body. + pub fn body(&self) -> &InvokeBody { + self.body + } + + /// Thr request headers. + pub fn headers(&self) -> &HeaderMap { + self.headers + } +} + +impl<'a, R: Runtime> CommandArg<'a, R> for Request<'a> { + /// Returns the invoke [`Request`]. + fn from_command(command: CommandItem<'a, R>) -> Result { + Ok(Self { + body: command.message.payload(), + headers: command.message.headers(), + }) + } +} + +/// Marks a type as a response to an IPC call. +pub trait IpcResponse { + /// Resolve the IPC response body. + fn body(self) -> crate::Result; +} + +impl IpcResponse for T { + fn body(self) -> crate::Result { + serde_json::to_value(self) + .map(Into::into) + .map_err(Into::into) + } +} + +/// The IPC request. +pub struct Response { + body: InvokeBody, +} + +impl IpcResponse for Response { + fn body(self) -> crate::Result { + Ok(self.body) + } +} + +impl Response { + /// Defines a response with the given body. + pub fn new(body: impl Into) -> Self { + Self { body: body.into() } + } +} + +/// The message and resolver given to a custom command. +#[default_runtime(crate::Wry, wry)] +pub struct Invoke { + /// The message passed. + pub message: InvokeMessage, + + /// The resolver of the message. + pub resolver: InvokeResolver, +} + +/// Error response from an [`InvokeMessage`]. +#[derive(Debug)] +pub struct InvokeError(pub JsonValue); + +impl InvokeError { + /// Create an [`InvokeError`] as a string of the [`std::error::Error`] message. + #[inline(always)] + pub fn from_error(error: E) -> Self { + Self(JsonValue::String(error.to_string())) + } + + /// Create an [`InvokeError`] as a string of the [`anyhow::Error`] message. + #[inline(always)] + pub fn from_anyhow(error: anyhow::Error) -> Self { + Self(JsonValue::String(format!("{error:#}"))) + } +} + +impl From for InvokeError { + #[inline] + fn from(value: T) -> Self { + serde_json::to_value(value) + .map(Self) + .unwrap_or_else(Self::from_error) + } +} + +impl From for InvokeError { + #[inline(always)] + fn from(error: crate::Error) -> Self { + Self(JsonValue::String(error.to_string())) + } +} + +/// Response from a [`InvokeMessage`] passed to the [`InvokeResolver`]. +#[derive(Debug)] +pub enum InvokeResponse { + /// Resolve the promise. + Ok(InvokeBody), + /// Reject the promise. + Err(InvokeError), +} + +impl> From> for InvokeResponse { + #[inline] + fn from(result: Result) -> Self { + match result { + Ok(ok) => match ok.body() { + Ok(value) => Self::Ok(value), + Err(err) => Self::Err(InvokeError::from_error(err)), + }, + Err(err) => Self::Err(err.into()), + } + } +} + +impl From for InvokeResponse { + fn from(error: InvokeError) -> Self { + Self::Err(error) + } +} + +/// Resolver of a invoke message. +#[default_runtime(crate::Wry, wry)] +pub struct InvokeResolver { + window: Window, + responder: Arc>, + cmd: String, + pub(crate) callback: CallbackFn, + pub(crate) error: CallbackFn, +} + +impl Clone for InvokeResolver { + fn clone(&self) -> Self { + Self { + window: self.window.clone(), + responder: self.responder.clone(), + cmd: self.cmd.clone(), + callback: self.callback, + error: self.error, + } + } +} + +impl InvokeResolver { + pub(crate) fn new( + window: Window, + responder: Arc>, + cmd: String, + callback: CallbackFn, + error: CallbackFn, + ) -> Self { + Self { + window, + responder, + cmd, + callback, + error, + } + } + + /// Reply to the invoke promise with an async task. + pub fn respond_async(self, task: F) + where + T: IpcResponse, + F: Future> + Send + 'static, + { + crate::async_runtime::spawn(async move { + Self::return_task( + self.window, + self.responder, + task, + self.cmd, + self.callback, + self.error, + ) + .await; + }); + } + + /// Reply to the invoke promise with an async task which is already serialized. + pub fn respond_async_serialized(self, task: F) + where + F: Future> + Send + 'static, + { + crate::async_runtime::spawn(async move { + let response = match task.await { + Ok(ok) => InvokeResponse::Ok(ok), + Err(err) => InvokeResponse::Err(err), + }; + Self::return_result( + self.window, + self.responder, + response, + self.cmd, + self.callback, + self.error, + ) + }); + } + + /// Reply to the invoke promise with a serializable value. + pub fn respond(self, value: Result) { + Self::return_result( + self.window, + self.responder, + value.into(), + self.cmd, + self.callback, + self.error, + ) + } + + /// Resolve the invoke promise with a value. + pub fn resolve(self, value: T) { + self.respond(Ok(value)) + } + + /// Reject the invoke promise with a value. + pub fn reject(self, value: T) { + Self::return_result( + self.window, + self.responder, + Result::<(), _>::Err(value).into(), + self.cmd, + self.callback, + self.error, + ) + } + + /// Reject the invoke promise with an [`InvokeError`]. + pub fn invoke_error(self, error: InvokeError) { + Self::return_result( + self.window, + self.responder, + error.into(), + self.cmd, + self.callback, + self.error, + ) + } + + /// Asynchronously executes the given task + /// and evaluates its Result to the JS promise described by the `success_callback` and `error_callback` function names. + /// + /// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value. + /// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value. + pub async fn return_task( + window: Window, + responder: Arc>, + task: F, + cmd: String, + success_callback: CallbackFn, + error_callback: CallbackFn, + ) where + T: IpcResponse, + F: Future> + Send + 'static, + { + let result = task.await; + Self::return_closure( + window, + responder, + || result, + cmd, + success_callback, + error_callback, + ) + } + + pub(crate) fn return_closure Result>( + window: Window, + responder: Arc>, + f: F, + cmd: String, + success_callback: CallbackFn, + error_callback: CallbackFn, + ) { + Self::return_result( + window, + responder, + f().into(), + cmd, + success_callback, + error_callback, + ) + } + + pub(crate) fn return_result( + window: Window, + responder: Arc>, + response: InvokeResponse, + cmd: String, + success_callback: CallbackFn, + error_callback: CallbackFn, + ) { + (responder)(window, cmd, response, success_callback, error_callback); + } +} + +/// An invoke message. +#[default_runtime(crate::Wry, wry)] +#[derive(Debug)] +pub struct InvokeMessage { + /// The window that received the invoke message. + pub(crate) window: Window, + /// Application managed state. + pub(crate) state: Arc, + /// The IPC command. + pub(crate) command: String, + /// The JSON argument passed on the invoke message. + pub(crate) payload: InvokeBody, + /// The request headers. + pub(crate) headers: HeaderMap, +} + +impl Clone for InvokeMessage { + fn clone(&self) -> Self { + Self { + window: self.window.clone(), + state: self.state.clone(), + command: self.command.clone(), + payload: self.payload.clone(), + headers: self.headers.clone(), + } + } +} + +impl InvokeMessage { + /// Create an new [`InvokeMessage`] from a payload send to a window. + pub(crate) fn new( + window: Window, + state: Arc, + command: String, + payload: InvokeBody, + headers: HeaderMap, + ) -> Self { + Self { + window, + state, + command, + payload, + headers, + } + } + + /// The invoke command. + #[inline(always)] + pub fn command(&self) -> &str { + &self.command + } + + /// The window that received the invoke. + #[inline(always)] + pub fn window(&self) -> Window { + self.window.clone() + } + + /// A reference to window that received the invoke. + #[inline(always)] + pub fn window_ref(&self) -> &Window { + &self.window + } + + /// A reference to the payload the invoke received. + #[inline(always)] + pub fn payload(&self) -> &InvokeBody { + &self.payload + } + + /// The state manager associated with the application + #[inline(always)] + pub fn state(&self) -> Arc { + self.state.clone() + } + + /// A reference to the state manager associated with application. + #[inline(always)] + pub fn state_ref(&self) -> &StateManager { + &self.state + } + + /// The request headers. + #[inline(always)] + pub fn headers(&self) -> &HeaderMap { + &self.headers + } +} + +/// The `Callback` type is the return value of the `transformCallback` JavaScript function. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct CallbackFn(pub u32); diff --git a/core/tauri/src/ipc/protocol.rs b/core/tauri/src/ipc/protocol.rs new file mode 100644 index 00000000000..60883c5c88e --- /dev/null +++ b/core/tauri/src/ipc/protocol.rs @@ -0,0 +1,275 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use http::{ + header::{ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN}, + HeaderValue, Method, StatusCode, +}; + +use crate::{ + manager::WindowManager, + runtime::http::{Request as HttpRequest, Response as HttpResponse}, + window::{InvokeRequest, UriSchemeProtocolHandler}, + Runtime, +}; + +use super::{CallbackFn, InvokeBody, InvokeResponse}; + +const TAURI_CALLBACK_HEADER_NAME: &str = "Tauri-Callback"; +const TAURI_ERROR_HEADER_NAME: &str = "Tauri-Error"; + +#[cfg(not(ipc_custom_protocol))] +pub fn message_handler( + manager: WindowManager, +) -> crate::runtime::webview::WebviewIpcHandler { + Box::new(move |window, request| handle_ipc_message(request, &manager, &window.label)) +} + +pub fn get(manager: WindowManager, label: String) -> UriSchemeProtocolHandler { + Box::new(move |request| { + let mut response = match *request.method() { + Method::POST => { + let (mut response, content_type) = match handle_ipc_request(request, &manager, &label) { + Ok(data) => match data { + InvokeResponse::Ok(InvokeBody::Json(v)) => ( + HttpResponse::new(serde_json::to_vec(&v)?.into()), + mime::APPLICATION_JSON, + ), + InvokeResponse::Ok(InvokeBody::Raw(v)) => { + (HttpResponse::new(v.into()), mime::APPLICATION_OCTET_STREAM) + } + InvokeResponse::Err(e) => { + let mut response = HttpResponse::new(serde_json::to_vec(&e.0)?.into()); + response.set_status(StatusCode::BAD_REQUEST); + (response, mime::TEXT_PLAIN) + } + }, + Err(e) => { + let mut response = HttpResponse::new(e.as_bytes().to_vec().into()); + response.set_status(StatusCode::BAD_REQUEST); + (response, mime::TEXT_PLAIN) + } + }; + + response.set_mimetype(Some(content_type.essence_str().into())); + + response + } + + Method::OPTIONS => { + let mut r = HttpResponse::new(Vec::new().into()); + r.headers_mut().insert( + ACCESS_CONTROL_ALLOW_HEADERS, + HeaderValue::from_static("Content-Type, Tauri-Callback, Tauri-Error, Tauri-Channel-Id"), + ); + r + } + + _ => { + let mut r = HttpResponse::new( + "only POST and OPTIONS are allowed" + .as_bytes() + .to_vec() + .into(), + ); + r.set_status(StatusCode::METHOD_NOT_ALLOWED); + r.set_mimetype(Some(mime::TEXT_PLAIN.essence_str().into())); + r + } + }; + + response + .headers_mut() + .insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*")); + + Ok(response) + }) +} + +#[cfg(not(ipc_custom_protocol))] +fn handle_ipc_message(message: String, manager: &WindowManager, label: &str) { + if let Some(window) = manager.get_window(label) { + use serde::{Deserialize, Deserializer}; + + pub(crate) struct HeaderMap(http::HeaderMap); + + impl<'de> Deserialize<'de> for HeaderMap { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let map = std::collections::HashMap::::deserialize(deserializer)?; + let mut headers = http::HeaderMap::default(); + for (key, value) in map { + if let (Ok(key), Ok(value)) = ( + http::HeaderName::from_bytes(key.as_bytes()), + http::HeaderValue::from_str(&value), + ) { + headers.insert(key, value); + } else { + return Err(serde::de::Error::custom(format!( + "invalid header `{key}` `{value}`" + ))); + } + } + Ok(Self(headers)) + } + } + + #[derive(Deserialize)] + struct RequestOptions { + headers: HeaderMap, + } + + #[derive(Deserialize)] + struct Message { + cmd: String, + callback: CallbackFn, + error: CallbackFn, + #[serde(flatten)] + payload: serde_json::Value, + options: Option, + } + + #[allow(unused_mut)] + let mut invoke_message: Option> = None; + + #[cfg(feature = "isolation")] + { + #[derive(serde::Deserialize)] + struct IsolationMessage<'a> { + cmd: String, + callback: CallbackFn, + error: CallbackFn, + #[serde(flatten)] + payload: crate::utils::pattern::isolation::RawIsolationPayload<'a>, + options: Option, + } + + if let crate::Pattern::Isolation { crypto_keys, .. } = manager.pattern() { + invoke_message.replace( + serde_json::from_str::>(&message) + .map_err(Into::into) + .and_then(|message| { + Ok(Message { + cmd: message.cmd, + callback: message.callback, + error: message.error, + payload: serde_json::from_slice(&crypto_keys.decrypt(message.payload)?)?, + options: message.options, + }) + }), + ); + } + } + + match invoke_message + .unwrap_or_else(|| serde_json::from_str::(&message).map_err(Into::into)) + { + Ok(message) => { + let _ = window.on_message(InvokeRequest { + cmd: message.cmd, + callback: message.callback, + error: message.error, + body: message.payload.into(), + headers: message.options.map(|o| o.headers.0).unwrap_or_default(), + }); + } + Err(e) => { + let _ = window.eval(&format!( + r#"console.error({})"#, + serde_json::Value::String(e.to_string()) + )); + } + } + } +} + +fn handle_ipc_request( + request: &HttpRequest, + manager: &WindowManager, + label: &str, +) -> std::result::Result { + if let Some(window) = manager.get_window(label) { + // TODO: consume instead + #[allow(unused_mut)] + let mut body = request.body().clone(); + + let cmd = request + .uri() + .strip_prefix("ipc://localhost/") + .map(|c| c.to_string()) + // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows + // where `$P` is not `localhost/*` + // in this case the IPC call is considered invalid + .unwrap_or_else(|| "".to_string()); + let cmd = percent_encoding::percent_decode(cmd.as_bytes()) + .decode_utf8_lossy() + .to_string(); + + // the body is not set if ipc_custom_protocol is not enabled so we'll just ignore it + #[cfg(all(feature = "isolation", ipc_custom_protocol))] + if let crate::Pattern::Isolation { crypto_keys, .. } = manager.pattern() { + body = crate::utils::pattern::isolation::RawIsolationPayload::try_from(&body) + .and_then(|raw| crypto_keys.decrypt(raw)) + .map_err(|e| e.to_string())?; + } + + let callback = CallbackFn( + request + .headers() + .get(TAURI_CALLBACK_HEADER_NAME) + .ok_or("missing Tauri-Callback header")? + .to_str() + .map_err(|_| "Tauri callback header value must be a string")? + .parse() + .map_err(|_| "Tauri callback header value must be a numeric string")?, + ); + let error = CallbackFn( + request + .headers() + .get(TAURI_ERROR_HEADER_NAME) + .ok_or("missing Tauri-Error header")? + .to_str() + .map_err(|_| "Tauri error header value must be a string")? + .parse() + .map_err(|_| "Tauri error header value must be a numeric string")?, + ); + + let content_type = request + .headers() + .get(reqwest::header::CONTENT_TYPE) + .and_then(|h| h.to_str().ok()) + .map(|mime| mime.parse()) + .unwrap_or(Ok(mime::APPLICATION_OCTET_STREAM)) + .map_err(|_| "unknown content type")?; + let body = if content_type == mime::APPLICATION_OCTET_STREAM { + body.into() + } else if content_type == mime::APPLICATION_JSON { + if cfg!(ipc_custom_protocol) { + serde_json::from_slice::(&body) + .map_err(|e| e.to_string())? + .into() + } else { + // the body is not set if ipc_custom_protocol is not enabled so we'll just ignore it + serde_json::Value::Object(Default::default()).into() + } + } else { + return Err(format!("content type {content_type} is not implemented")); + }; + + let payload = InvokeRequest { + cmd, + callback, + error, + body, + headers: request.headers().clone(), + }; + + let rx = window.on_message(payload); + Ok(rx.recv().unwrap()) + } else { + Err("window not found".into()) + } +} diff --git a/core/tauri/src/jni_helpers.rs b/core/tauri/src/jni_helpers.rs new file mode 100644 index 00000000000..4f72bfacef1 --- /dev/null +++ b/core/tauri/src/jni_helpers.rs @@ -0,0 +1,98 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::Runtime; +use jni::{ + errors::Error as JniError, + objects::{JObject, JValueOwned}, + JNIEnv, +}; +use serde_json::Value as JsonValue; +use tauri_runtime::RuntimeHandle; + +fn json_to_java<'a, R: Runtime>( + env: &mut JNIEnv<'a>, + activity: &JObject<'_>, + runtime_handle: &R::Handle, + json: &JsonValue, +) -> Result<(&'static str, JValueOwned<'a>), JniError> { + let (class, v) = match json { + JsonValue::Null => ("Ljava/lang/Object;", JObject::null().into()), + JsonValue::Bool(val) => ("Z", (*val).into()), + JsonValue::Number(val) => { + if let Some(v) = val.as_i64() { + ("J", v.into()) + } else if let Some(v) = val.as_f64() { + ("D", v.into()) + } else { + ("Ljava/lang/Object;", JObject::null().into()) + } + } + JsonValue::String(val) => ( + "Ljava/lang/Object;", + JObject::from(env.new_string(val)?).into(), + ), + JsonValue::Array(val) => { + let js_array_class = runtime_handle.find_class(env, activity, "app/tauri/plugin/JSArray")?; + let data = env.new_object(js_array_class, "()V", &[])?; + + for v in val { + let (signature, val) = json_to_java::(env, activity, runtime_handle, v)?; + env.call_method( + &data, + "put", + format!("({signature})Lorg/json/JSONArray;"), + &[val.borrow()], + )?; + } + + ("Ljava/lang/Object;", data.into()) + } + JsonValue::Object(val) => { + let data = { + let js_object_class = + runtime_handle.find_class(env, activity, "app/tauri/plugin/JSObject")?; + env.new_object(js_object_class, "()V", &[])? + }; + + for (key, value) in val { + let (signature, val) = json_to_java::(env, activity, runtime_handle, value)?; + let key = env.new_string(key)?; + env.call_method( + &data, + "put", + format!("(Ljava/lang/String;{signature})Lapp/tauri/plugin/JSObject;"), + &[(&key).into(), val.borrow()], + )?; + } + + ("Ljava/lang/Object;", data.into()) + } + }; + Ok((class, v)) +} + +pub fn to_jsobject<'a, R: Runtime>( + env: &mut JNIEnv<'a>, + activity: &JObject<'_>, + runtime_handle: &R::Handle, + json: &JsonValue, +) -> Result, JniError> { + if let JsonValue::Object(_) = json { + json_to_java::(env, activity, runtime_handle, json).map(|(_class, data)| data) + } else { + Ok(empty_object::(env, activity, runtime_handle)?.into()) + } +} + +fn empty_object<'a, R: Runtime>( + env: &mut JNIEnv<'a>, + activity: &JObject<'_>, + runtime_handle: &R::Handle, +) -> Result, JniError> { + // currently the Kotlin lib cannot handle nulls or raw values, it must be an object + let js_object_class = runtime_handle.find_class(env, activity, "app/tauri/plugin/JSObject")?; + let data = env.new_object(js_object_class, "()V", &[])?; + Ok(data) +} diff --git a/core/tauri/src/lib.rs b/core/tauri/src/lib.rs index 32c86cc9237..a66aca8bd83 100644 --- a/core/tauri/src/lib.rs +++ b/core/tauri/src/lib.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +//! [![](https://github.com/tauri-apps/tauri/raw/dev/.github/splash.png)](https://tauri.app) +//! //! Tauri is a framework for building tiny, blazing fast binaries for all major desktop platforms. //! Developers can integrate any front-end framework that compiles to HTML, JS and CSS for building their user interface. //! The backend of the application is a rust-sourced binary with an API that the front-end can interact with. @@ -14,30 +16,18 @@ //! - **test**: Enables the [`test`] module exposing unit test helpers. //! - **dox**: Internal feature to generate Rust documentation without linking on Linux. //! - **objc-exception**: Wrap each msg_send! in a @try/@catch and panics if an exception is caught, preventing Objective-C from unwinding into Rust. -//! - **linux-protocol-headers**: Enables headers support for custom protocol requests on Linux. Requires webkit2gtk v2.36 or above. +//! - **linux-ipc-protocol**: Use custom protocol for faster IPC on Linux. Requires webkit2gtk v2.40 or above. +//! - **linux-libxdo**: Enables linking to libxdo which enables Cut, Copy, Paste and SelectAll menu items to work on Linux. //! - **isolation**: Enables the isolation pattern. Enabled by default if the `tauri > pattern > use` config option is set to `isolation` on the `tauri.conf.json` file. //! - **custom-protocol**: Feature managed by the Tauri CLI. When enabled, Tauri assumes a production environment instead of a development one. -//! - **updater**: Enables the application auto updater. Enabled by default if the `updater` config is defined on the `tauri.conf.json` file. //! - **devtools**: Enables the developer tools (Web inspector) and [`Window::open_devtools`]. Enabled by default on debug builds. //! On macOS it uses private APIs, so you can't enable it if your app will be published to the App Store. -//! - **shell-open-api**: Enables the [`api::shell`] module. -//! - **http-api**: Enables the [`api::http`] module. -//! - **http-multipart**: Adds support to `multipart/form-data` requests. -//! - **reqwest-client**: Alias for the `http-api` feature flag. +//! - **native-tls**: Provides TLS support to connect over HTTPS. //! - **native-tls-vendored**: Compile and statically link to a vendored copy of OpenSSL. -//! - **reqwest-native-tls-vendored**: Alias for the `native-tls-vendored` feature flag. -//! - **os-api**: Enables the [`api::os`] module. -//! - **process-command-api**: Enables the [`api::process::Command`] APIs. -//! - **global-shortcut**: Enables the global shortcut APIs. -//! - **clipboard**: Enables the clipboard APIs. -//! - **process-relaunch-dangerous-allow-symlink-macos**: Allows the [`api::process::current_binary`] function to allow symlinks on macOS (this is dangerous, see the Security section in the documentation website). -//! - **dialog**: Enables the [`api::dialog`] module. -//! - **notification**: Enables the [`api::notification`] module. -//! - **fs-extract-api**: Enabled the `tauri::api::file::Extract` API. -//! - **cli**: Enables usage of `clap` for CLI argument parsing. Enabled by default if the `cli` config is defined on the `tauri.conf.json` file. -//! - **system-tray**: Enables application system tray API. Enabled by default if the `systemTray` config is defined on the `tauri.conf.json` file. +//! - **rustls-tls**: Provides TLS support to connect over HTTPS using rustls. +//! - **process-relaunch-dangerous-allow-symlink-macos**: Allows the [`process::current_binary`] function to allow symlinks on macOS (this is dangerous, see the Security section in the documentation website). +//! - **tray-icon**: Enables application tray icon APIs. Enabled by default if the `trayIcon` config is defined on the `tauri.conf.json` file. //! - **macos-private-api**: Enables features only available in **macOS**'s private APIs, currently the `transparent` window functionality and the `fullScreenEnabled` preference setting to `true`. Enabled by default if the `tauri > macosPrivateApi` config flag is set to `true` on the `tauri.conf.json` file. -//! - **windows7-compat**: Enables compatibility with Windows 7 for the notification API. //! - **window-data-url**: Enables usage of data URLs on the webview. //! - **compression** *(enabled by default): Enables asset compression. You should only disable this if you want faster compile times in release builds - it produces larger binaries. //! - **config-json5**: Adds support to JSON5 format for `tauri.conf.json`. @@ -50,153 +40,71 @@ //! The following are a list of [Cargo features](https://doc.rust-lang.org/stable/cargo/reference/manifest.html#the-features-section) that enables commands for Tauri's API package. //! These features are automatically enabled by the Tauri CLI based on the `allowlist` configuration under `tauri.conf.json`. //! -//! - **api-all**: Enables all API endpoints. -//! -//! ### Clipboard allowlist -//! -//! - **clipboard-all**: Enables all [Clipboard APIs](https://tauri.app/en/docs/api/js/modules/clipboard/). -//! - **clipboard-read-text**: Enables the [`readText` API](https://tauri.app/en/docs/api/js/modules/clipboard/#readtext). -//! - **clipboard-write-text**: Enables the [`writeText` API](https://tauri.app/en/docs/api/js/modules/clipboard/#writetext). -//! -//! ### Dialog allowlist -//! -//! - **dialog-all**: Enables all [Dialog APIs](https://tauri.app/en/docs/api/js/modules/dialog). -//! - **dialog-ask**: Enables the [`ask` API](https://tauri.app/en/docs/api/js/modules/dialog#ask). -//! - **dialog-confirm**: Enables the [`confirm` API](https://tauri.app/en/docs/api/js/modules/dialog#confirm). -//! - **dialog-message**: Enables the [`message` API](https://tauri.app/en/docs/api/js/modules/dialog#message). -//! - **dialog-open**: Enables the [`open` API](https://tauri.app/en/docs/api/js/modules/dialog#open). -//! - **dialog-save**: Enables the [`save` API](https://tauri.app/en/docs/api/js/modules/dialog#save). -//! -//! ### Filesystem allowlist -//! -//! - **fs-all**: Enables all [Filesystem APIs](https://tauri.app/en/docs/api/js/modules/fs). -//! - **fs-copy-file**: Enables the [`copyFile` API](https://tauri.app/en/docs/api/js/modules/fs#copyfile). -//! - **fs-create-dir**: Enables the [`createDir` API](https://tauri.app/en/docs/api/js/modules/fs#createdir). -//! - **fs-exists**: Enables the [`exists` API](https://tauri.app/en/docs/api/js/modules/fs#exists). -//! - **fs-read-dir**: Enables the [`readDir` API](https://tauri.app/en/docs/api/js/modules/fs#readdir). -//! - **fs-read-file**: Enables the [`readTextFile` API](https://tauri.app/en/docs/api/js/modules/fs#readtextfile) and the [`readBinaryFile` API](https://tauri.app/en/docs/api/js/modules/fs#readbinaryfile). -//! - **fs-remove-dir**: Enables the [`removeDir` API](https://tauri.app/en/docs/api/js/modules/fs#removedir). -//! - **fs-remove-file**: Enables the [`removeFile` API](https://tauri.app/en/docs/api/js/modules/fs#removefile). -//! - **fs-rename-file**: Enables the [`renameFile` API](https://tauri.app/en/docs/api/js/modules/fs#renamefile). -//! - **fs-write-file**: Enables the [`writeFile` API](https://tauri.app/en/docs/api/js/modules/fs#writefile) and the [`writeBinaryFile` API](https://tauri.app/en/docs/api/js/modules/fs#writebinaryfile). -//! -//! ### Global shortcut allowlist -//! -//! - **global-shortcut-all**: Enables all [GlobalShortcut APIs](https://tauri.app/en/docs/api/js/modules/globalShortcut). -//! -//! ### HTTP allowlist -//! -//! - **http-all**: Enables all [HTTP APIs](https://tauri.app/en/docs/api/js/modules/http). -//! - **http-request**: Enables the [`request` APIs](https://tauri.app/en/docs/api/js/classes/http.client/). -//! -//! ### Notification allowlist -//! -//! - **notification-all**: Enables all [Notification APIs](https://tauri.app/en/docs/api/js/modules/notification). -//! -//! ### OS allowlist -//! -//! - **os-all**: Enables all [OS APIs](https://tauri.app/en/docs/api/js/modules/os). -//! -//! ### Path allowlist -//! -//! - **path-all**: Enables all [Path APIs](https://tauri.app/en/docs/api/js/modules/path). -//! -//! ### Process allowlist -//! -//! - **process-all**: Enables all [Process APIs](https://tauri.app/en/docs/api/js/modules/process). -//! - **process-exit**: Enables the [`exit` API](https://tauri.app/en/docs/api/js/modules/process#exit). -//! - **process-relaunch**: Enables the [`relaunch` API](https://tauri.app/en/docs/api/js/modules/process#relaunch). -//! //! ### Protocol allowlist //! -//! - **protocol-all**: Enables all Protocol APIs. //! - **protocol-asset**: Enables the `asset` custom protocol. -//! -//! ### Shell allowlist -//! -//! - **shell-all**: Enables all [Clipboard APIs](https://tauri.app/en/docs/api/js/modules/shell). -//! - **shell-execute**: Enables [executing arbitrary programs](https://tauri.app/en/docs/api/js/classes/shell.Command#constructor). -//! - **shell-sidecar**: Enables [executing a `sidecar` program](https://tauri.app/en/docs/api/js/classes/shell.Command#sidecar). -//! - **shell-open**: Enables the [`open` API](https://tauri.app/en/docs/api/js/modules/shell#open). -//! -//! ### Window allowlist -//! -//! - **window-all**: Enables all [Window APIs](https://tauri.app/en/docs/api/js/modules/window). -//! - **window-create**: Enables the API used to [create new windows](https://tauri.app/en/docs/api/js/classes/window.webviewwindow/). -//! - **window-center**: Enables the [`center` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#center). -//! - **window-request-user-attention**: Enables the [`requestUserAttention` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#requestuserattention). -//! - **window-set-resizable**: Enables the [`setResizable` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setresizable). -//! - **window-set-maximizable**: Enables the [`setMaximizable` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setmaximizable). -//! - **window-set-minimizable**: Enables the [`setMinimizable` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setminimizable). -//! - **window-set-closable**: Enables the [`setClosable` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setclosable). -//! - **window-set-title**: Enables the [`setTitle` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#settitle). -//! - **window-maximize**: Enables the [`maximize` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#maximize). -//! - **window-unmaximize**: Enables the [`unmaximize` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#unmaximize). -//! - **window-minimize**: Enables the [`minimize` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#minimize). -//! - **window-unminimize**: Enables the [`unminimize` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#unminimize). -//! - **window-show**: Enables the [`show` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#show). -//! - **window-hide**: Enables the [`hide` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#hide). -//! - **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). -//! - **window-set-position**: Enables the [`setPosition` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setposition). -//! - **window-set-fullscreen**: Enables the [`setFullscreen` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setfullscreen). -//! - **window-set-focus**: Enables the [`setFocus` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setfocus). -//! - **window-set-icon**: Enables the [`setIcon` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#seticon). -//! - **window-set-skip-taskbar**: Enables the [`setSkipTaskbar` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setskiptaskbar). -//! - **window-set-cursor-grab**: Enables the [`setCursorGrab` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setcursorgrab). -//! - **window-set-cursor-visible**: Enables the [`setCursorVisible` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setcursorvisible). -//! - **window-set-cursor-icon**: Enables the [`setCursorIcon` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setcursoricon). -//! - **window-set-cursor-position**: Enables the [`setCursorPosition` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setcursorposition). -//! - **window-set-ignore-cursor-events**: Enables the [`setIgnoreCursorEvents` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#setignorecursorevents). -//! - **window-start-dragging**: Enables the [`startDragging` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#startdragging). -//! - **window-print**: Enables the [`print` API](https://tauri.app/en/docs/api/js/classes/window.WebviewWindow#print). -//! -//! ### App allowlist -//! -//! - **app-all**: Enables all [App APIs](https://tauri.app/en/docs/api/js/modules/app). -//! - **app-show**: Enables the [`show` API](https://tauri.app/en/docs/api/js/modules/app#show). -//! - **app-hide**: Enables the [`hide` API](https://tauri.app/en/docs/api/js/modules/app#hide). +#![doc( + html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", + html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" +)] #![warn(missing_docs, rust_2018_idioms)] #![cfg_attr(doc_cfg, feature(doc_cfg))] +/// Setups the binding that initializes an iOS plugin. +#[cfg(target_os = "ios")] +#[macro_export] +macro_rules! ios_plugin_binding { + ($fn_name: ident) => { + tauri::swift_rs::swift!(fn $fn_name() -> *const ::std::ffi::c_void); + } +} +#[cfg(target_os = "ios")] +#[doc(hidden)] +pub use cocoa; #[cfg(target_os = "macos")] #[doc(hidden)] pub use embed_plist; /// The Tauri error enum. pub use error::Error; -#[cfg(shell_scope)] +#[cfg(target_os = "ios")] #[doc(hidden)] -pub use regex; +pub use swift_rs; +#[cfg(mobile)] +pub use tauri_macros::mobile_entry_point; pub use tauri_macros::{command, generate_handler}; pub mod api; pub(crate) mod app; +#[cfg(feature = "protocol-asset")] +pub(crate) mod asset_protocol; pub mod async_runtime; pub mod command; -/// The Tauri API endpoints. -mod endpoints; mod error; mod event; -mod hooks; +pub mod ipc; mod manager; mod pattern; pub mod plugin; +mod vibrancy; pub mod window; use tauri_runtime as runtime; -#[cfg(protocol_asset)] -mod asset_protocol; +#[cfg(target_os = "ios")] +mod ios; +#[cfg(target_os = "android")] +mod jni_helpers; +#[cfg(desktop)] +pub mod menu; +/// Path APIs. +pub mod path; +pub mod process; /// The allowlist scopes. pub mod scope; mod state; -#[cfg(updater)] -#[cfg_attr(doc_cfg, doc(cfg(feature = "updater")))] -pub mod updater; + +#[cfg(all(desktop, feature = "tray-icon"))] +#[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))] +pub mod tray; pub use tauri_utils as utils; /// A Tauri [`Runtime`] wrapper around wry. @@ -204,6 +112,55 @@ pub use tauri_utils as utils; #[cfg_attr(doc_cfg, doc(cfg(feature = "wry")))] pub type Wry = tauri_runtime_wry::Wry; +#[cfg(all(feature = "wry", target_os = "android"))] +#[cfg_attr(doc_cfg, doc(cfg(all(feature = "wry", target_os = "android"))))] +#[doc(hidden)] +#[macro_export] +macro_rules! android_binding { + ($domain:ident, $package:ident, $main: ident, $wry: path) => { + ::tauri::wry::android_binding!($domain, $package, $main, $wry); + ::tauri::wry::application::android_fn!( + app_tauri, + plugin, + PluginManager, + handlePluginResponse, + [i32, JString, JString], + ); + ::tauri::wry::application::android_fn!( + app_tauri, + plugin, + PluginManager, + sendChannelData, + [i64, JString], + ); + + // this function is a glue between PluginManager.kt > handlePluginResponse and Rust + #[allow(non_snake_case)] + pub fn handlePluginResponse( + mut env: JNIEnv, + _: JClass, + id: i32, + success: JString, + error: JString, + ) { + ::tauri::handle_android_plugin_response(&mut env, id, success, error); + } + + // this function is a glue between PluginManager.kt > sendChannelData and Rust + #[allow(non_snake_case)] + pub fn sendChannelData(mut env: JNIEnv, _: JClass, id: i64, data: JString) { + ::tauri::send_channel_data(&mut env, id, data); + } + }; +} + +#[cfg(all(feature = "wry", target_os = "android"))] +#[doc(hidden)] +pub use plugin::mobile::{handle_android_plugin_response, send_channel_data}; +#[cfg(all(feature = "wry", target_os = "android"))] +#[doc(hidden)] +pub use tauri_runtime_wry::wry; + /// `Result` pub type Result = std::result::Result; @@ -211,7 +168,11 @@ pub type Result = std::result::Result; pub type SyncTask = Box; use serde::Serialize; -use std::{collections::HashMap, fmt, sync::Arc}; +use std::{ + collections::HashMap, + fmt::{self, Debug}, + sync::Arc, +}; // Export types likely to be used by the application. pub use runtime::http; @@ -222,30 +183,16 @@ pub use tauri_runtime_wry::webview_version; #[cfg(target_os = "macos")] #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))] -pub use runtime::{menu::NativeImage, ActivationPolicy}; +pub use runtime::ActivationPolicy; #[cfg(target_os = "macos")] pub use self::utils::TitleBarStyle; -#[cfg(all(desktop, feature = "system-tray"))] -#[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] -pub use { - self::app::tray::{SystemTray, SystemTrayEvent, SystemTrayHandle, SystemTrayMenuItemHandle}, - self::runtime::menu::{SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu}, -}; -pub use { - self::app::WindowMenuEvent, - self::event::{Event, EventHandler}, - self::runtime::menu::{AboutMetadata, CustomMenuItem, Menu, MenuEntry, MenuItem, Submenu}, - self::window::menu::MenuEvent, -}; + +pub use self::event::{Event, EventHandler}; pub use { self::app::{ - App, AppHandle, AssetResolver, Builder, CloseRequestApi, GlobalWindowEvent, PathResolver, - RunEvent, WindowEvent, - }, - self::hooks::{ - Invoke, InvokeError, InvokeHandler, InvokeMessage, InvokePayload, InvokeResolver, - InvokeResponder, InvokeResponse, OnPageLoad, PageLoadPayload, SetupHook, + App, AppHandle, AssetResolver, Builder, CloseRequestApi, GlobalWindowEvent, RunEvent, + WindowEvent, }, self::manager::Asset, self::runtime::{ @@ -266,72 +213,56 @@ pub use { scope::*, }; -#[cfg(feature = "clipboard")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "clipboard")))] -pub use self::runtime::ClipboardManager; - -#[cfg(all(desktop, feature = "global-shortcut"))] -#[cfg_attr(doc_cfg, doc(cfg(feature = "global-shortcut")))] -pub use self::runtime::GlobalShortcutManager; - /// The Tauri version. pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -/// Updater events. -#[cfg(updater)] -#[cfg_attr(doc_cfg, doc(cfg(feature = "updater")))] -#[derive(Debug, Clone)] -pub enum UpdaterEvent { - /// An update is available. - UpdateAvailable { - /// The update body. - body: String, - /// The update release date. - date: Option, - /// The update version. - version: String, - }, - /// The update is pending and about to be downloaded. - Pending, - /// The update download received a progress event. - DownloadProgress { - /// The amount that was downloaded on this iteration. - /// Does not accumulate with previous chunks. - chunk_length: usize, - /// The total - content_length: Option, - }, - /// The update has been downloaded and is now about to be installed. - Downloaded, - /// The update has been applied and the app is now up to date. - Updated, - /// The app is already up to date. - AlreadyUpToDate, - /// An error occurred while updating. - Error(String), -} +#[cfg(target_os = "ios")] +#[doc(hidden)] +pub fn log_stdout() { + use std::{ + ffi::CString, + fs::File, + io::{BufRead, BufReader}, + os::unix::prelude::*, + thread, + }; -#[cfg(updater)] -impl UpdaterEvent { - pub(crate) fn status_message(self) -> &'static str { - match self { - Self::Pending => updater::EVENT_STATUS_PENDING, - Self::Downloaded => updater::EVENT_STATUS_DOWNLOADED, - Self::Updated => updater::EVENT_STATUS_SUCCESS, - Self::AlreadyUpToDate => updater::EVENT_STATUS_UPTODATE, - Self::Error(_) => updater::EVENT_STATUS_ERROR, - _ => unreachable!(), + let mut logpipe: [RawFd; 2] = Default::default(); + unsafe { + libc::pipe(logpipe.as_mut_ptr()); + libc::dup2(logpipe[1], libc::STDOUT_FILENO); + libc::dup2(logpipe[1], libc::STDERR_FILENO); + } + thread::spawn(move || unsafe { + let file = File::from_raw_fd(logpipe[0]); + let mut reader = BufReader::new(file); + let mut buffer = String::new(); + loop { + buffer.clear(); + if let Ok(len) = reader.read_line(&mut buffer) { + if len == 0 { + break; + } else if let Ok(msg) = CString::new(buffer.clone()) + .map_err(|_| ()) + .and_then(|c| c.into_string().map_err(|_| ())) + { + log::info!("{}", msg); + } + } } - } + }); } /// The user event type. #[derive(Debug, Clone)] pub enum EventLoopMessage { - /// Updater event. - #[cfg(updater)] - #[cfg_attr(doc_cfg, doc(cfg(feature = "updater")))] - Updater(UpdaterEvent), + /// An event from a menu item, could be on the window menu bar, application menu bar (on macOS) or tray icon menu. + #[cfg(desktop)] + MenuEvent(menu::MenuEvent), + /// An event from a menu item, could be on the window menu bar, application menu bar (on macOS) or tray icon menu. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))] + TrayIconEvent(tray::TrayIconEvent), } /// The webview runtime interface. A wrapper around [`runtime::Runtime`] with the proper user event type associated. @@ -471,12 +402,11 @@ pub struct Context { pub(crate) assets: Arc, pub(crate) default_window_icon: Option, pub(crate) app_icon: Option>, - pub(crate) system_tray_icon: Option, + #[cfg(all(desktop, feature = "tray-icon"))] + pub(crate) tray_icon: Option, pub(crate) package_info: PackageInfo, pub(crate) _info_plist: (), pub(crate) pattern: Pattern, - #[cfg(shell_scope)] - pub(crate) shell_scope: scope::ShellScopeConfig, } impl fmt::Debug for Context { @@ -485,11 +415,12 @@ impl fmt::Debug for Context { d.field("config", &self.config) .field("default_window_icon", &self.default_window_icon) .field("app_icon", &self.app_icon) - .field("system_tray_icon", &self.system_tray_icon) .field("package_info", &self.package_info) .field("pattern", &self.pattern); - #[cfg(shell_scope)] - d.field("shell_scope", &self.shell_scope); + + #[cfg(all(desktop, feature = "tray-icon"))] + d.field("tray_icon", &self.tray_icon); + d.finish() } } @@ -532,15 +463,19 @@ impl Context { } /// The icon to use on the system tray UI. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))] #[inline(always)] - pub fn system_tray_icon(&self) -> Option<&Icon> { - self.system_tray_icon.as_ref() + pub fn tray_icon(&self) -> Option<&Icon> { + self.tray_icon.as_ref() } - /// A mutable reference to the icon to use on the system tray UI. + /// A mutable reference to the icon to use on the tray icon. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))] #[inline(always)] - pub fn system_tray_icon_mut(&mut self) -> &mut Option { - &mut self.system_tray_icon + pub fn tray_icon_mut(&mut self) -> &mut Option { + &mut self.tray_icon } /// Package information. @@ -561,13 +496,6 @@ impl Context { &self.pattern } - /// The scoped shell commands, where the `HashMap` key is the name each configuration. - #[cfg(shell_scope)] - #[inline(always)] - pub fn allowed_commands(&self) -> &scope::ShellScopeConfig { - &self.shell_scope - } - /// Create a new [`Context`] from the minimal required items. #[inline(always)] #[allow(clippy::too_many_arguments)] @@ -576,32 +504,44 @@ impl Context { assets: Arc, default_window_icon: Option, app_icon: Option>, - system_tray_icon: Option, package_info: PackageInfo, info_plist: (), pattern: Pattern, - #[cfg(shell_scope)] shell_scope: scope::ShellScopeConfig, ) -> Self { Self { config, assets, default_window_icon, app_icon, - system_tray_icon, + #[cfg(all(desktop, feature = "tray-icon"))] + tray_icon: None, package_info, _info_plist: info_plist, pattern, - #[cfg(shell_scope)] - shell_scope, } } + + /// Sets the app tray icon. + #[cfg(all(desktop, feature = "tray-icon"))] + #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))] + #[inline(always)] + pub fn set_tray_icon(&mut self, icon: Icon) { + self.tray_icon.replace(icon); + } + + /// Sets the app shell scope. + #[cfg(shell_scope)] + #[inline(always)] + pub fn set_shell_scope(&mut self, scope: scope::ShellScopeConfig) { + self.shell_scope = scope; + } } // TODO: expand these docs /// Manages a running application. pub trait Manager: sealed::ManagerBase { /// The application handle associated with this manager. - fn app_handle(&self) -> AppHandle { + fn app_handle(&self) -> &AppHandle { self.managed_app_handle() } @@ -610,6 +550,11 @@ pub trait Manager: sealed::ManagerBase { self.manager().config() } + /// The [`PackageInfo`] the manager was created with. + fn package_info(&self) -> &PackageInfo { + self.manager().package_info() + } + /// Emits a event to all windows. /// /// Only the webviews receives this event. @@ -720,7 +665,7 @@ pub trait Manager: sealed::ManagerBase { /// /// tauri::Builder::default() /// .setup(|app| { - /// let handle = app.handle(); + /// let handle = app.handle().clone(); /// let handler = app.listen_global("ready", move |event| { /// println!("app is ready"); /// @@ -756,18 +701,13 @@ pub trait Manager: sealed::ManagerBase { /// Add `state` to the state managed by the application. /// - /// This method can be called any number of times as long as each call - /// refers to a different `T`. - /// If a state for `T` is already managed, the function returns false and the value is ignored. + /// If the state for the `T` type has previously been set, the state is unchanged and false is returned. Otherwise true is returned. /// /// Managed state can be retrieved by any command handler via the /// [`State`](crate::State) guard. In particular, if a value of type `T` /// is managed by Tauri, adding `State` to the list of arguments in a /// command handler instructs Tauri to retrieve the managed value. - /// - /// # Panics - /// - /// Panics if state of type `T` is already being managed. + /// Additionally, [`state`](Self#method.state) can be used to retrieve the value manually. /// /// # Mutability /// @@ -883,26 +823,20 @@ pub trait Manager: sealed::ManagerBase { self.state::().inner().clone() } - /// Gets the scope for the filesystem APIs. - fn fs_scope(&self) -> FsScope { - self.state::().inner().fs.clone() - } - /// Gets the scope for the IPC. fn ipc_scope(&self) -> IpcScope { self.state::().inner().ipc.clone() } /// Gets the scope for the asset protocol. - #[cfg(protocol_asset)] + #[cfg(feature = "protocol-asset")] fn asset_protocol_scope(&self) -> FsScope { self.state::().inner().asset_protocol.clone() } - /// Gets the scope for the shell execute APIs. - #[cfg(shell_scope)] - fn shell_scope(&self) -> ShellScope { - self.state::().inner().shell.clone() + /// The path resolver. + fn path(&self) -> &crate::path::PathResolver { + self.state::>().inner() } } @@ -928,7 +862,7 @@ pub(crate) mod sealed { /// The manager behind the [`Managed`] item. fn manager(&self) -> &WindowManager; fn runtime(&self) -> RuntimeOrDispatch<'_, R>; - fn managed_app_handle(&self) -> AppHandle; + fn managed_app_handle(&self) -> &AppHandle; } } @@ -976,59 +910,25 @@ mod tests { } } } +} - #[test] - fn all_allowlist_features_are_aliased() { - let manifest = get_manifest(); - let all_modules = manifest - .features - .iter() - .find(|(f, _)| f.as_str() == "api-all") - .map(|(_, enabled)| enabled) - .expect("api-all feature must exist"); - - let checked_features = CHECKED_FEATURES.split(',').collect::>(); - assert!( - checked_features.contains(&"api-all"), - "`api-all` is not aliased" - ); - - // features that look like an allowlist feature, but are not - let allowed = [ - "fs-extract-api", - "http-api", - "http-multipart", - "os-api", - "process-command-api", - "process-relaunch-dangerous-allow-symlink-macos", - "window-data-url", - ]; - - for module_all_feature in all_modules { - let module = module_all_feature.replace("-all", ""); - assert!( - checked_features.contains(&module_all_feature.as_str()), - "`{module}` is not aliased" - ); - - let module_prefix = format!("{module}-"); - // we assume that module features are the ones that start with `-` - // though it's not 100% accurate, we have an allowed list to fix it - let module_features = manifest - .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()), - "`{module_feature}` is not aliased" - ); - } - } - } +#[allow(unused)] +macro_rules! run_main_thread { + ($self:ident, $ex:expr) => {{ + use std::sync::mpsc::channel; + let (tx, rx) = channel(); + let self_ = $self.clone(); + let task = move || { + let _ = tx.send($ex(self_)); + }; + $self.app_handle.run_on_main_thread(Box::new(task))?; + rx.recv().map_err(|_| crate::Error::FailedToReceiveMessage) + }}; } +#[allow(unused)] +pub(crate) use run_main_thread; + #[cfg(test)] mod test_utils { use proptest::prelude::*; diff --git a/core/tauri/src/manager.rs b/core/tauri/src/manager.rs index a57e1cc052d..b5cccd32455 100644 --- a/core/tauri/src/manager.rs +++ b/core/tauri/src/manager.rs @@ -7,55 +7,60 @@ use std::{ collections::{HashMap, HashSet}, fmt, fs::create_dir_all, + path::PathBuf, sync::{Arc, Mutex, MutexGuard}, }; +#[cfg(desktop)] +use crate::menu::{Menu, MenuId}; +#[cfg(all(desktop, feature = "tray-icon"))] +use crate::tray::{TrayIcon, TrayIconId}; use serde::Serialize; -use serde_json::Value as JsonValue; use serialize_to_javascript::{default_template, DefaultTemplate, Template}; use url::Url; use tauri_macros::default_runtime; use tauri_utils::debug_eprintln; -#[cfg(feature = "isolation")] -use tauri_utils::pattern::isolation::RawIsolationPayload; use tauri_utils::{ assets::{AssetKey, CspHash}, config::{Csp, CspDirectiveSources}, html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN}, }; -use crate::app::{GlobalMenuEventListener, WindowMenuEvent}; -use crate::hooks::IpcJavascript; -#[cfg(feature = "isolation")] -use crate::hooks::IsolationJavascript; -use crate::pattern::PatternJavascript; use crate::{ - app::{AppHandle, GlobalWindowEvent, GlobalWindowEventListener}, + app::{AppHandle, GlobalWindowEvent, GlobalWindowEventListener, OnPageLoad, PageLoadPayload}, event::{assert_event_name_is_valid, Event, EventHandler, Listeners}, - hooks::{InvokeHandler, InvokePayload, InvokeResponder, OnPageLoad, PageLoadPayload}, + ipc::{Invoke, InvokeHandler, InvokeResponder}, + pattern::PatternJavascript, plugin::PluginStore, runtime::{ http::{ MimeType, Request as HttpRequest, Response as HttpResponse, ResponseBuilder as HttpResponseBuilder, }, - webview::{WebviewIpcHandler, WindowBuilder}, - window::{dpi::PhysicalSize, DetachedWindow, FileDropEvent, PendingWindow}, + webview::WindowBuilder, + window::{ + dpi::{PhysicalPosition, PhysicalSize}, + DetachedWindow, FileDropEvent, PendingWindow, + }, }, utils::{ assets::Assets, config::{AppUrl, Config, WindowUrl}, PackageInfo, }, - Context, EventLoopMessage, Icon, Invoke, Manager, Pattern, Runtime, Scopes, StateManager, Window, + window::{UriSchemeProtocolHandler, WebResourceRequestHandler}, + Context, EventLoopMessage, Icon, Manager, Pattern, Runtime, Scopes, StateManager, Window, WindowEvent, }; -#[cfg(any(target_os = "linux", target_os = "windows"))] -use crate::api::path::{resolve_path, BaseDirectory}; +#[cfg(desktop)] +use crate::app::GlobalMenuEventListener; +#[cfg(all(desktop, feature = "tray-icon"))] +use crate::app::GlobalTrayIconEventListener; -use crate::{runtime::menu::Menu, MenuEvent}; +#[cfg(any(target_os = "linux", target_os = "windows"))] +use crate::path::BaseDirectory; const WINDOW_RESIZED_EVENT: &str = "tauri://resize"; const WINDOW_MOVED_EVENT: &str = "tauri://move"; @@ -68,10 +73,29 @@ const WINDOW_THEME_CHANGED: &str = "tauri://theme-changed"; const WINDOW_FILE_DROP_EVENT: &str = "tauri://file-drop"; const WINDOW_FILE_DROP_HOVER_EVENT: &str = "tauri://file-drop-hover"; const WINDOW_FILE_DROP_CANCELLED_EVENT: &str = "tauri://file-drop-cancelled"; -const MENU_EVENT: &str = "tauri://menu"; -pub(crate) const STRINGIFY_IPC_MESSAGE_FN: &str = - include_str!("../scripts/stringify-ipc-message-fn.js"); +pub(crate) const PROCESS_IPC_MESSAGE_FN: &str = + include_str!("../scripts/process-ipc-message-fn.js"); + +// we need to proxy the dev server on mobile because we can't use `localhost`, so we use the local IP address +// and we do not get a secure context without the custom protocol that proxies to the dev server +// additionally, we need the custom protocol to inject the initialization scripts on Android +// must also keep in sync with the `let mut response` assignment in prepare_uri_scheme_protocol +const PROXY_DEV_SERVER: bool = cfg!(all(dev, mobile)); + +#[cfg(feature = "isolation")] +#[derive(Template)] +#[default_template("../scripts/isolation.js")] +pub(crate) struct IsolationJavascript<'a> { + pub(crate) isolation_src: &'a str, + pub(crate) style: &'a str, +} + +#[derive(Template)] +#[default_template("../scripts/ipc.js")] +pub(crate) struct IpcJavascript<'a> { + pub(crate) isolation_origin: &'a str, +} #[derive(Default)] /// Spaced and quoted Content-Security-Policy hash values. @@ -145,11 +169,6 @@ fn set_csp( Csp::DirectiveMap(csp).to_string() } -#[cfg(target_os = "linux")] -fn set_html_csp(html: &str, csp: &str) -> String { - html.replacen(tauri_utils::html::CSP_TOKEN, csp, 1) -} - // inspired by https://github.com/rust-lang/rust/blob/1be5c8f90912c446ecbdc405cbc4a89f9acd20fd/library/alloc/src/str.rs#L260-L297 fn replace_with_callback String>( original: &str, @@ -198,9 +217,7 @@ fn replace_csp_nonce( #[default_runtime(crate::Wry, wry)] pub struct InnerWindowManager { - windows: Mutex>>, - #[cfg(all(desktop, feature = "system-tray"))] - pub(crate) trays: Mutex>>, + pub(crate) windows: Mutex>>, pub(crate) plugins: Mutex>, listeners: Listeners, pub(crate) state: Arc, @@ -215,19 +232,44 @@ pub struct InnerWindowManager { assets: Arc, pub(crate) default_window_icon: Option, pub(crate) app_icon: Option>, + #[cfg(all(desktop, feature = "tray-icon"))] pub(crate) tray_icon: Option, package_info: PackageInfo, /// The webview protocols available to all windows. uri_scheme_protocols: HashMap>>, + /// A set containing a reference to the active menus, including + /// the app-wide menu and the window-specific menus + /// + /// This should be mainly used to acceess [`Menu::haccel`] + /// to setup the accelerator handling in the event loop + #[cfg(desktop)] + pub menus: Arc>>>, /// The menu set to all windows. - menu: Option, + #[cfg(desktop)] + pub(crate) menu: Arc>>>, /// Menu event listeners to all windows. - menu_event_listeners: Arc>>, + #[cfg(desktop)] + pub(crate) menu_event_listeners: Arc>>>>, + /// Menu event listeners to specific windows. + #[cfg(desktop)] + pub(crate) window_menu_event_listeners: + Arc>>>>, /// Window event listeners to all windows. window_event_listeners: Arc>>, + /// Tray icons + #[cfg(all(desktop, feature = "tray-icon"))] + pub(crate) tray_icons: Arc>>>, + /// Global Tray icon event listeners. + #[cfg(all(desktop, feature = "tray-icon"))] + pub(crate) global_tray_event_listeners: + Arc>>>>, + /// Tray icon event listeners. + #[cfg(all(desktop, feature = "tray-icon"))] + pub(crate) tray_event_listeners: + Arc>>>>, /// Responder for invoke calls. - invoke_responder: Arc>, + invoke_responder: Option>>, /// The script that initializes the invoke system. invoke_initialization_script: String, /// Application pattern. @@ -236,17 +278,20 @@ pub struct InnerWindowManager { impl fmt::Debug for InnerWindowManager { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("InnerWindowManager") - .field("plugins", &self.plugins) + let mut d = f.debug_struct("InnerWindowManager"); + + d.field("plugins", &self.plugins) .field("state", &self.state) .field("config", &self.config) .field("default_window_icon", &self.default_window_icon) .field("app_icon", &self.app_icon) - .field("tray_icon", &self.tray_icon) .field("package_info", &self.package_info) - .field("menu", &self.menu) - .field("pattern", &self.pattern) - .finish() + .field("pattern", &self.pattern); + + #[cfg(all(desktop, feature = "tray-icon"))] + d.field("tray_icon", &self.tray_icon); + + d.finish() } } @@ -286,7 +331,7 @@ impl Clone for WindowManager { } impl WindowManager { - #[allow(clippy::too_many_arguments)] + #[allow(clippy::too_many_arguments, clippy::type_complexity)] pub(crate) fn with_handlers( #[allow(unused_mut)] mut context: Context, plugins: PluginStore, @@ -295,8 +340,11 @@ impl WindowManager { uri_scheme_protocols: HashMap>>, state: StateManager, window_event_listeners: Vec>, - (menu, menu_event_listeners): (Option, Vec>), - (invoke_responder, invoke_initialization_script): (Arc>, String), + #[cfg(desktop)] window_menu_event_listeners: HashMap< + String, + GlobalMenuEventListener>, + >, + (invoke_responder, invoke_initialization_script): (Option>>, String), ) -> Self { // generate a random isolation key at runtime #[cfg(feature = "isolation")] @@ -307,8 +355,6 @@ impl WindowManager { Self { inner: Arc::new(InnerWindowManager { windows: Mutex::default(), - #[cfg(all(desktop, feature = "system-tray"))] - trays: Default::default(), plugins: Mutex::new(plugins), listeners: Listeners::default(), state: Arc::new(state), @@ -318,13 +364,26 @@ impl WindowManager { assets: context.assets, default_window_icon: context.default_window_icon, app_icon: context.app_icon, - tray_icon: context.system_tray_icon, + #[cfg(all(desktop, feature = "tray-icon"))] + tray_icon: context.tray_icon, package_info: context.package_info, pattern: context.pattern, uri_scheme_protocols, - menu, - menu_event_listeners: Arc::new(menu_event_listeners), + #[cfg(desktop)] + menus: Default::default(), + #[cfg(desktop)] + menu: Default::default(), + #[cfg(desktop)] + menu_event_listeners: Default::default(), + #[cfg(desktop)] + window_menu_event_listeners: Arc::new(Mutex::new(window_menu_event_listeners)), window_event_listeners: Arc::new(window_event_listeners), + #[cfg(all(desktop, feature = "tray-icon"))] + tray_icons: Default::default(), + #[cfg(all(desktop, feature = "tray-icon"))] + global_tray_event_listeners: Default::default(), + #[cfg(all(desktop, feature = "tray-icon"))] + tray_event_listeners: Default::default(), invoke_responder, invoke_initialization_script, }), @@ -345,8 +404,85 @@ impl WindowManager { self.inner.state.clone() } + #[cfg(desktop)] + pub(crate) fn prepare_window_menu_creation_handler( + &self, + window_menu: Option<&crate::window::WindowMenu>, + ) -> Option)> { + { + if let Some(menu) = window_menu { + self + .menus_stash_lock() + .insert(menu.menu.id().clone(), menu.menu.clone()); + } + } + + #[cfg(target_os = "macos")] + return None; + + #[cfg_attr(target_os = "macos", allow(unused_variables, unreachable_code))] + if let Some(menu) = &window_menu { + let menu = menu.menu.clone(); + Some(move |raw: tauri_runtime::window::RawWindow<'_>| { + #[cfg(target_os = "windows")] + let _ = menu.inner().init_for_hwnd(raw.hwnd as _); + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + let _ = menu + .inner() + .init_for_gtk_window(raw.gtk_window, raw.default_vbox); + }) + } else { + None + } + } + + /// App-wide menu. + #[cfg(desktop)] + pub(crate) fn menu_lock(&self) -> MutexGuard<'_, Option>> { + self.inner.menu.lock().expect("poisoned window manager") + } + + /// Menus stash. + #[cfg(desktop)] + pub(crate) fn menus_stash_lock(&self) -> MutexGuard<'_, HashMap>> { + self.inner.menus.lock().expect("poisoned window manager") + } + + #[cfg(desktop)] + pub(crate) fn is_menu_in_use>(&self, id: &I) -> bool { + self + .menu_lock() + .as_ref() + .map(|m| id.eq(m.id())) + .unwrap_or(false) + } + + /// Menus stash. + #[cfg(desktop)] + pub(crate) fn insert_menu_into_stash(&self, menu: &Menu) { + self + .menus_stash_lock() + .insert(menu.id().clone(), menu.clone()); + } + + #[cfg(desktop)] + pub(crate) fn remove_menu_from_stash_by_id(&self, id: Option<&MenuId>) { + if let Some(id) = id { + let is_used_by_a_window = self.windows_lock().values().any(|w| w.is_menu_in_use(id)); + if !(self.is_menu_in_use(id) || is_used_by_a_window) { + self.menus_stash_lock().remove(id); + } + } + } + /// The invoke responder. - pub(crate) fn invoke_responder(&self) -> Arc> { + pub(crate) fn invoke_responder(&self) -> Option>> { self.inner.invoke_responder.clone() } @@ -370,13 +506,17 @@ impl WindowManager { pub(crate) fn get_url(&self) -> Cow<'_, Url> { match self.base_path() { AppUrl::Url(WindowUrl::External(url)) => Cow::Borrowed(url), - #[cfg(windows)] - _ => Cow::Owned(Url::parse("https://tauri.localhost").unwrap()), - #[cfg(not(windows))] - _ => Cow::Owned(Url::parse("tauri://localhost").unwrap()), + _ => self.protocol_url(), } } + pub(crate) fn protocol_url(&self) -> Cow<'_, Url> { + #[cfg(any(windows, target_os = "android"))] + return Cow::Owned(Url::parse("https://tauri.localhost").unwrap()); + #[cfg(not(any(windows, target_os = "android")))] + Cow::Owned(Url::parse("tauri://localhost").unwrap()) + } + fn csp(&self) -> Option { if cfg!(feature = "custom-protocol") { self.inner.config.tauri.security.csp.clone() @@ -442,8 +582,7 @@ impl WindowManager { window_labels_array = serde_json::to_string(&window_labels)?, current_window_label = serde_json::to_string(&label)?, )) - .initialization_script(&self.initialization_script(&ipc_init.into_string(),&pattern_init.into_string(),&plugin_init, is_init_global)?) - ; + .initialization_script(&self.initialization_script(&ipc_init.into_string(),&pattern_init.into_string(),&plugin_init, is_init_global)?); #[cfg(feature = "isolation")] if let Pattern::Isolation { schema, .. } = self.pattern() { @@ -496,7 +635,15 @@ impl WindowManager { registered_scheme_protocols.push("tauri".into()); } - #[cfg(protocol_asset)] + if !registered_scheme_protocols.contains(&"ipc".into()) { + pending.register_uri_scheme_protocol( + "ipc", + crate::ipc::protocol::get(self.clone(), pending.label.clone()), + ); + registered_scheme_protocols.push("ipc".into()); + } + + #[cfg(feature = "protocol-asset")] if !registered_scheme_protocols.contains(&"asset".into()) { let asset_scope = self.state().get::().asset_protocol.clone(); pending.register_uri_scheme_protocol("asset", move |request| { @@ -528,27 +675,27 @@ impl WindowManager { let asset = String::from_utf8_lossy(asset.as_ref()); let template = tauri_utils::pattern::isolation::IsolationJavascriptRuntime { runtime_aes_gcm_key: &aes_gcm_key, - stringify_ipc_message_fn: STRINGIFY_IPC_MESSAGE_FN, + process_ipc_message_fn: PROCESS_IPC_MESSAGE_FN, }; match template.render(asset.as_ref(), &Default::default()) { Ok(asset) => HttpResponseBuilder::new() - .mimetype("text/html") + .mimetype(mime::TEXT_HTML.as_ref()) .body(asset.into_string().as_bytes().to_vec()), Err(_) => HttpResponseBuilder::new() .status(500) - .mimetype("text/plain") + .mimetype(mime::TEXT_PLAIN.as_ref()) .body(Vec::new()), } } None => HttpResponseBuilder::new() .status(404) - .mimetype("text/plain") + .mimetype(mime::TEXT_PLAIN.as_ref()) .body(Vec::new()), }, _ => HttpResponseBuilder::new() .status(404) - .mimetype("text/plain") + .mimetype(mime::TEXT_PLAIN.as_ref()) .body(Vec::new()), } }); @@ -557,46 +704,6 @@ impl WindowManager { Ok(pending) } - fn prepare_ipc_handler( - &self, - app_handle: AppHandle, - ) -> WebviewIpcHandler { - let manager = self.clone(); - Box::new(move |window, #[allow(unused_mut)] mut request| { - let window = Window::new(manager.clone(), window, app_handle.clone()); - - #[cfg(feature = "isolation")] - if let Pattern::Isolation { crypto_keys, .. } = manager.pattern() { - match RawIsolationPayload::try_from(request.as_str()) - .and_then(|raw| crypto_keys.decrypt(raw)) - { - Ok(json) => request = json, - Err(e) => { - let error: crate::Error = e.into(); - let _ = window.eval(&format!( - r#"console.error({})"#, - JsonValue::String(error.to_string()) - )); - return; - } - } - } - - match serde_json::from_str::(&request) { - Ok(message) => { - let _ = window.on_message(message); - } - Err(e) => { - let error: crate::Error = e.into(); - let _ = window.eval(&format!( - r#"console.error({})"#, - JsonValue::String(error.to_string()) - )); - } - } - }) - } - pub fn get_asset(&self, mut path: String) -> Result> { let assets = &self.inner.assets; if path.ends_with('/') { @@ -618,7 +725,7 @@ impl WindowManager { let asset_response = assets .get(&path.as_str().into()) .or_else(|| { - eprintln!("Asset `{path}` not found; fallback to {path}.html"); + debug_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; @@ -680,55 +787,126 @@ impl WindowManager { } } - #[allow(clippy::type_complexity)] fn prepare_uri_scheme_protocol( &self, window_origin: &str, - web_resource_request_handler: Option< - Box, - >, - ) -> Box Result> + Send + Sync> - { + web_resource_request_handler: Option>, + ) -> UriSchemeProtocolHandler { + #[cfg(all(dev, mobile))] + let url = { + let mut url = self.get_url().as_str().to_string(); + if url.ends_with('/') { + url.pop(); + } + url + }; + #[cfg(not(all(dev, mobile)))] let manager = self.clone(); let window_origin = window_origin.to_string(); + + #[cfg(all(dev, mobile))] + #[derive(Clone)] + struct CachedResponse { + status: http::StatusCode, + headers: http::HeaderMap, + body: bytes::Bytes, + } + + #[cfg(all(dev, mobile))] + let response_cache = Arc::new(Mutex::new(HashMap::new())); + Box::new(move |request| { - let path = request - .uri() - .split(&['?', '#'][..]) + // use the entire URI as we are going to proxy the request + let path = if PROXY_DEV_SERVER { + request.uri() + } else { // ignore query string and fragment - .next() - .unwrap() + request.uri().split(&['?', '#'][..]).next().unwrap() + }; + + let path = path .strip_prefix("tauri://localhost") .map(|p| p.to_string()) // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows // where `$P` is not `localhost/*` .unwrap_or_else(|| "".to_string()); - let asset = manager.get_asset(path)?; - let mut builder = HttpResponseBuilder::new() - .header("Access-Control-Allow-Origin", &window_origin) - .mimetype(&asset.mime_type); - if let Some(csp) = &asset.csp_header { - builder = builder.header("Content-Security-Policy", csp); - } - let mut response = builder.body(asset.bytes)?; - if let Some(handler) = &web_resource_request_handler { - handler(request, &mut response); - // if it's an HTML file, we need to set the CSP meta tag on Linux - #[cfg(target_os = "linux")] - if let Some(response_csp) = response.headers().get("Content-Security-Policy") { - let response_csp = String::from_utf8_lossy(response_csp.as_bytes()); - let body = set_html_csp(&String::from_utf8_lossy(response.body()), &response_csp); - *response.body_mut() = body.as_bytes().to_vec(); - } - } else { - #[cfg(target_os = "linux")] + let mut builder = + HttpResponseBuilder::new().header("Access-Control-Allow-Origin", &window_origin); + + #[cfg(all(dev, mobile))] + let mut response = { + let decoded_path = percent_encoding::percent_decode(path.as_bytes()) + .decode_utf8_lossy() + .to_string(); + let url = format!("{url}{decoded_path}"); + #[allow(unused_mut)] + let mut client_builder = reqwest::ClientBuilder::new(); + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] { - if let Some(csp) = &asset.csp_header { - let body = set_html_csp(&String::from_utf8_lossy(response.body()), csp); - *response.body_mut() = body.as_bytes().to_vec(); + client_builder = client_builder.danger_accept_invalid_certs(true); + } + let mut proxy_builder = client_builder + .build() + .unwrap() + .request(request.method().clone(), &url); + for (name, value) in request.headers() { + proxy_builder = proxy_builder.header(name, value); + } + match crate::async_runtime::block_on(proxy_builder.send()) { + Ok(r) => { + let mut response_cache_ = response_cache.lock().unwrap(); + let mut response = None; + if r.status() == http::StatusCode::NOT_MODIFIED { + response = response_cache_.get(&url); + } + let response = if let Some(r) = response { + r + } else { + let status = r.status(); + let headers = r.headers().clone(); + let body = crate::async_runtime::block_on(r.bytes())?; + let response = CachedResponse { + status, + headers, + body, + }; + response_cache_.insert(url.clone(), response); + response_cache_.get(&url).unwrap() + }; + for (name, value) in &response.headers { + builder = builder.header(name, value); + } + builder + .status(response.status) + .body(response.body.to_vec())? + } + Err(e) => { + debug_eprintln!("Failed to request {}: {}", url.as_str(), e); + return Err(Box::new(e)); } } + }; + + #[cfg(not(all(dev, mobile)))] + let mut response = { + let asset = manager.get_asset(path)?; + builder = builder.mimetype(&asset.mime_type); + if let Some(csp) = &asset.csp_header { + builder = builder.header("Content-Security-Policy", csp); + } + builder.body(asset.bytes)? + }; + if let Some(handler) = &web_resource_request_handler { + handler(request, &mut response); + } + // if it's an HTML file, we need to set the CSP meta tag on Linux + #[cfg(all(not(dev), target_os = "linux"))] + if let Some(response_csp) = response.headers().get("Content-Security-Policy") { + let response_csp = String::from_utf8_lossy(response_csp.as_bytes()); + let html = String::from_utf8_lossy(response.body()); + let body = html.replacen(tauri_utils::html::CSP_TOKEN, &response_csp, 1); + *response.body_mut() = body.as_bytes().to_vec().into(); } Ok(response) }) @@ -761,8 +939,12 @@ impl WindowManager { plugin_initialization_script: &'a str, #[raw] freeze_prototype: &'a str, - #[raw] - hotkeys: &'a str, + } + + #[derive(Template)] + #[default_template("../scripts/core.js")] + struct CoreJavascript<'a> { + os_name: &'a str, } let bundle_script = if with_global_tauri { @@ -777,34 +959,6 @@ impl WindowManager { "" }; - #[cfg(any(debug_assertions, feature = "devtools"))] - let hotkeys = &format!( - " - {}; - window.hotkeys('{}', () => {{ - window.__TAURI_INVOKE__('tauri', {{ - __tauriModule: 'Window', - message: {{ - cmd: 'manage', - data: {{ - cmd: {{ - type: '__toggleDevtools' - }} - }} - }} - }}); - }}); - ", - include_str!("../scripts/hotkey.js"), - if cfg!(target_os = "macos") { - "command+option+i" - } else { - "ctrl+shift+i" - } - ); - #[cfg(not(any(debug_assertions, feature = "devtools")))] - let hotkeys = ""; - InitJavascript { pattern_script, ipc_script, @@ -819,11 +973,14 @@ impl WindowManager { "window['_' + window.__TAURI__.transformCallback(cb) ]".into() ) ), - core_script: include_str!("../scripts/core.js"), + core_script: &CoreJavascript { + os_name: std::env::consts::OS, + } + .render_default(&Default::default())? + .into_string(), event_initialization_script: &self.event_initialization_script(), plugin_initialization_script, freeze_prototype, - hotkeys, } .render_default(&Default::default()) .map(|s| s.into_string()) @@ -865,13 +1022,13 @@ mod test { let manager: WindowManager = WindowManager::with_handlers( context, PluginStore::default(), - Box::new(|_| ()), + Box::new(|_| false), Box::new(|_, _| ()), Default::default(), StateManager::new(), Default::default(), Default::default(), - (std::sync::Arc::new(|_, _, _, _| ()), "".into()), + (None, "".into()), ); #[cfg(custom_protocol)] @@ -892,8 +1049,8 @@ mod test { } impl WindowManager { - pub fn run_invoke_handler(&self, invoke: Invoke) { - (self.inner.invoke_handler)(invoke); + pub fn run_invoke_handler(&self, invoke: Invoke) -> bool { + (self.inner.invoke_handler)(invoke) } pub fn run_on_page_load(&self, window: Window, payload: PageLoadPayload) { @@ -906,13 +1063,13 @@ impl WindowManager { .on_page_load(window, payload); } - pub fn extend_api(&self, invoke: Invoke) { + pub fn extend_api(&self, plugin: &str, invoke: Invoke) -> bool { self .inner .plugins .lock() .expect("poisoned plugin store") - .extend_api(invoke); + .extend_api(plugin, invoke) } pub fn initialize_plugins(&self, app: &AppHandle) -> crate::Result<()> { @@ -936,7 +1093,11 @@ impl WindowManager { #[allow(unused_mut)] // mut url only for the data-url parsing let mut url = match &pending.webview_attributes.url { WindowUrl::App(path) => { - let url = self.get_url(); + let url = if PROXY_DEV_SERVER { + Cow::Owned(Url::parse("tauri://localhost").unwrap()) + } else { + self.get_url() + }; // ignore "index.html" just to simplify the url if path.to_str() != Some("index.html") { url @@ -948,7 +1109,16 @@ impl WindowManager { url.into_owned() } } - WindowUrl::External(url) => url.clone(), + WindowUrl::External(url) => { + let config_url = self.get_url(); + let is_local = config_url.make_relative(url).is_some(); + let mut url = url.clone(); + if is_local && PROXY_DEV_SERVER { + url.set_scheme("tauri").unwrap(); + url.set_host(Some("localhost")).unwrap(); + } + url + } _ => unimplemented!(), }; @@ -969,7 +1139,7 @@ impl WindowManager { if html.contains('<') && html.contains('>') { let mut document = tauri_utils::html::parse(html); tauri_utils::html::inject_csp(&mut document, &csp.to_string()); - url.set_path(&format!("text/html,{}", document.to_string())); + url.set_path(&format!("{},{}", mime::TEXT_HTML, document.to_string())); } } } @@ -985,26 +1155,51 @@ impl WindowManager { } } - if pending.window_builder.get_menu().is_none() { - if let Some(menu) = &self.inner.menu { - pending = pending.set_menu(menu.clone()); - } + #[cfg(target_os = "android")] + { + pending = pending.on_webview_created(move |ctx| { + let plugin_manager = ctx + .env + .call_method( + ctx.activity, + "getPluginManager", + "()Lapp/tauri/plugin/PluginManager;", + &[], + )? + .l()?; + + // tell the manager the webview is ready + ctx.env.call_method( + plugin_manager, + "onWebViewCreated", + "(Landroid/webkit/WebView;)V", + &[ctx.webview.into()], + )?; + + Ok(()) + }); } let label = pending.label.clone(); - pending = self.prepare_pending_window(pending, &label, window_labels, app_handle.clone())?; - pending.ipc_handler = Some(self.prepare_ipc_handler(app_handle)); + pending = self.prepare_pending_window( + pending, + &label, + window_labels, + #[allow(clippy::redundant_clone)] + app_handle.clone(), + )?; + #[cfg(not(ipc_custom_protocol))] + { + pending.ipc_handler = Some(crate::ipc::protocol::message_handler(self.clone())); + } // in `Windows`, we need to force a data_directory // but we do respect user-specification #[cfg(any(target_os = "linux", target_os = "windows"))] if pending.webview_attributes.data_directory.is_none() { - let local_app_data = resolve_path( - &self.inner.config, - &self.inner.package_info, - self.inner.state.get::().inner(), + let local_app_data = app_handle.path().resolve( &self.inner.config.tauri.bundle.identifier, - Some(BaseDirectory::LocalData), + BaseDirectory::LocalData, ); if let Ok(user_data_dir) = local_app_data { pending.webview_attributes.data_directory = Some(user_data_dir); @@ -1021,6 +1216,8 @@ impl WindowManager { #[cfg(feature = "isolation")] let pattern = self.pattern().clone(); let navigation_handler = pending.navigation_handler.take(); + let manager = self.inner.clone(); + let label = pending.label.clone(); pending.navigation_handler = Some(Box::new(move |url| { // always allow navigation events for the isolation iframe and do not emit them for consumers #[cfg(feature = "isolation")] @@ -1032,7 +1229,17 @@ impl WindowManager { } } if let Some(handler) = &navigation_handler { - handler(url) + if !handler(url) { + return false; + } + } + let window = manager.windows.lock().unwrap().get(&label).cloned(); + if let Some(w) = window { + manager + .plugins + .lock() + .expect("poisoned plugin store") + .on_navigation(&w, url) } else { true } @@ -1041,12 +1248,19 @@ impl WindowManager { Ok(pending) } - pub fn attach_window( + pub(crate) fn attach_window( &self, app_handle: AppHandle, window: DetachedWindow, + #[cfg(desktop)] menu: Option>, ) -> Window { - let window = Window::new(self.clone(), window, app_handle); + let window = Window::new( + self.clone(), + window, + app_handle, + #[cfg(desktop)] + menu, + ); let window_ = window.clone(); let window_event_listeners = self.inner.window_event_listeners.clone(); @@ -1060,19 +1274,6 @@ impl WindowManager { }); } }); - { - let window_ = window.clone(); - let menu_event_listeners = self.inner.menu_event_listeners.clone(); - window.on_menu_event(move |event| { - let _ = on_menu_event(&window_, &event); - for handler in menu_event_listeners.iter() { - handler(WindowMenuEvent { - window: window_.clone(), - menu_item_id: event.menu_item_id.clone(), - }); - } - }); - } // insert the window into our manager { @@ -1093,6 +1294,15 @@ impl WindowManager { .created(window_); }); + #[cfg(target_os = "ios")] + { + window + .with_webview(|w| { + unsafe { crate::ios::on_webview_created(w.inner() as _, w.view_controller() as _) }; + }) + .expect("failed to run on_webview_created hook"); + } + window } @@ -1193,31 +1403,10 @@ impl WindowManager { } } -/// Tray APIs -#[cfg(all(desktop, feature = "system-tray"))] -impl WindowManager { - pub fn get_tray(&self, id: &str) -> Option> { - self.inner.trays.lock().unwrap().get(id).cloned() - } - - pub fn trays(&self) -> HashMap> { - self.inner.trays.lock().unwrap().clone() - } - - pub fn attach_tray(&self, id: String, tray: crate::SystemTrayHandle) { - self.inner.trays.lock().unwrap().insert(id, tray); - } - - pub fn get_tray_by_runtime_id(&self, id: u16) -> Option<(String, crate::SystemTrayHandle)> { - let trays = self.inner.trays.lock().unwrap(); - let iter = trays.iter(); - for (tray_id, tray) in iter { - if tray.id == id { - return Some((tray_id.clone(), tray.clone())); - } - } - None - } +#[derive(Serialize, Clone)] +struct FileDropPayload<'a> { + paths: &'a Vec, + position: &'a PhysicalPosition, } fn on_window_event( @@ -1241,7 +1430,7 @@ fn on_window_event( 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}"); }} }})()"#, ))?; } } @@ -1265,8 +1454,11 @@ fn on_window_event( }, )?, WindowEvent::FileDrop(event) => match event { - FileDropEvent::Hovered(paths) => window.emit(WINDOW_FILE_DROP_HOVER_EVENT, paths)?, - FileDropEvent::Dropped(paths) => { + FileDropEvent::Hovered { paths, position } => { + let payload = FileDropPayload { paths, position }; + window.emit(WINDOW_FILE_DROP_HOVER_EVENT, payload)? + } + FileDropEvent::Dropped { paths, position } => { let scopes = window.state::(); for path in paths { if path.is_file() { @@ -1275,7 +1467,8 @@ fn on_window_event( let _ = scopes.allow_directory(path, false); } } - window.emit(WINDOW_FILE_DROP_EVENT, paths)? + let payload = FileDropPayload { paths, position }; + window.emit(WINDOW_FILE_DROP_EVENT, payload)? } FileDropEvent::Cancelled => window.emit(WINDOW_FILE_DROP_CANCELLED_EVENT, ())?, _ => unimplemented!(), @@ -1292,10 +1485,6 @@ struct ScaleFactorChanged { size: PhysicalSize, } -fn on_menu_event(window: &Window, event: &MenuEvent) -> crate::Result<()> { - window.emit(MENU_EVENT, event.menu_item_id.clone()) -} - #[cfg(feature = "isolation")] fn request_to_path(request: &tauri_runtime::http::Request, base_url: &str) -> String { let mut path = request diff --git a/core/tauri/src/menu/builders/check.rs b/core/tauri/src/menu/builders/check.rs new file mode 100644 index 00000000000..fa900d206d5 --- /dev/null +++ b/core/tauri/src/menu/builders/check.rs @@ -0,0 +1,90 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{menu::CheckMenuItem, menu::MenuId, Manager, Runtime}; + +/// A builder type for [`CheckMenuItem`] +pub struct CheckMenuItemBuilder { + id: Option, + text: String, + enabled: bool, + checked: bool, + accelerator: Option, +} + +impl CheckMenuItemBuilder { + /// Create a new menu item builder. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new>(text: S) -> Self { + Self { + id: None, + text: text.as_ref().to_string(), + enabled: true, + checked: true, + accelerator: None, + } + } + + /// Create a new menu item builder with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, S: AsRef>(id: I, text: S) -> Self { + Self { + id: Some(id.into()), + text: text.as_ref().to_string(), + enabled: true, + checked: true, + accelerator: None, + } + } + + /// Set the id for this menu item. + pub fn id>(mut self, id: I) -> Self { + self.id.replace(id.into()); + self + } + + /// Set the enabled state for this menu item. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Set the checked state for this menu item. + pub fn checked(mut self, checked: bool) -> Self { + self.checked = checked; + self + } + + /// Set the accelerator for this menu item. + pub fn accelerator>(mut self, accelerator: S) -> Self { + self.accelerator.replace(accelerator.as_ref().to_string()); + self + } + + /// Build the menu item + pub fn build>(self, manager: &M) -> CheckMenuItem { + if let Some(id) = self.id { + CheckMenuItem::with_id( + manager, + id, + self.text, + self.enabled, + self.checked, + self.accelerator, + ) + } else { + CheckMenuItem::new( + manager, + self.text, + self.enabled, + self.checked, + self.accelerator, + ) + } + } +} diff --git a/core/tauri/src/menu/builders/icon.rs b/core/tauri/src/menu/builders/icon.rs new file mode 100644 index 00000000000..a6a24360c62 --- /dev/null +++ b/core/tauri/src/menu/builders/icon.rs @@ -0,0 +1,129 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + menu::{IconMenuItem, MenuId, NativeIcon}, + Icon, Manager, Runtime, +}; + +/// A builder type for [`IconMenuItem`] +pub struct IconMenuItemBuilder { + id: Option, + text: String, + enabled: bool, + icon: Option, + native_icon: Option, + accelerator: Option, +} + +impl IconMenuItemBuilder { + /// Create a new menu item builder. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new>(text: S) -> Self { + Self { + id: None, + text: text.as_ref().to_string(), + enabled: true, + icon: None, + native_icon: None, + accelerator: None, + } + } + + /// Create a new menu item builder with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, S: AsRef>(id: I, text: S) -> Self { + Self { + id: Some(id.into()), + text: text.as_ref().to_string(), + enabled: true, + icon: None, + native_icon: None, + accelerator: None, + } + } + + /// Set the id for this menu item. + pub fn id>(mut self, id: I) -> Self { + self.id.replace(id.into()); + self + } + + /// Set the enabled state for this menu item. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Set the accelerator for this menu item. + pub fn accelerator>(mut self, accelerator: S) -> Self { + self.accelerator.replace(accelerator.as_ref().to_string()); + self + } + + /// Set the icon for this menu item. + /// + /// **Note:** This method conflicts with [`Self::native_icon`] + /// so calling one of them, will reset the other. + pub fn icon(mut self, icon: Icon) -> Self { + self.icon.replace(icon); + self.native_icon = None; + self + } + + /// Set the icon for this menu item. + /// + /// **Note:** This method conflicts with [`Self::icon`] + /// so calling one of them, will reset the other. + pub fn native_icon(mut self, icon: NativeIcon) -> Self { + self.native_icon.replace(icon); + self.icon = None; + self + } + + /// Build the menu item + pub fn build>(self, manager: &M) -> IconMenuItem { + if self.icon.is_some() { + if let Some(id) = self.id { + IconMenuItem::with_id( + manager, + id, + self.text, + self.enabled, + self.icon, + self.accelerator, + ) + } else { + IconMenuItem::new( + manager, + self.text, + self.enabled, + self.icon, + self.accelerator, + ) + } + } else if let Some(id) = self.id { + IconMenuItem::with_id_and_native_icon( + manager, + id, + self.text, + self.enabled, + self.native_icon, + self.accelerator, + ) + } else { + IconMenuItem::with_native_icon( + manager, + self.text, + self.enabled, + self.native_icon, + self.accelerator, + ) + } + } +} diff --git a/core/tauri/src/menu/builders/menu.rs b/core/tauri/src/menu/builders/menu.rs new file mode 100644 index 00000000000..f8674b86ede --- /dev/null +++ b/core/tauri/src/menu/builders/menu.rs @@ -0,0 +1,326 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{menu::*, Icon, Manager, Runtime}; + +/// A builder type for [`Menu`] +/// +/// # Example +/// +/// ```no_run +/// use tauri::menu::*; +/// tauri::Builder::default() +/// .setup(move |app| { +/// let handle = app.handle(); +/// # let icon1 = tauri::Icon::Rgba { +/// # rgba: Vec::new(), +/// # width: 0, +/// # height: 0, +/// # }; +/// let menu = MenuBuilder::new(handle) +/// .item(&MenuItem::new(handle, "MenuItem 1", true, None)) +/// .items(&[ +/// &CheckMenuItem::new(handle, "CheckMenuItem 1", true, true, None), +/// &IconMenuItem::new(handle, "IconMenuItem 1", true, Some(icon1), None), +/// ]) +/// .separator() +/// .cut() +/// .copy() +/// .paste() +/// .separator() +/// .text("item2", "MenuItem 2") +/// .check("checkitem2", "CheckMenuItem 2") +/// .icon("iconitem2", "IconMenuItem 2", app.default_window_icon().cloned().unwrap()) +/// .build()?; +/// app.set_menu(menu); +/// Ok(()) +/// }); +/// ``` +pub struct MenuBuilder<'m, R: Runtime, M: Manager> { + id: Option, + manager: &'m M, + items: Vec>, +} + +impl<'m, R: Runtime, M: Manager> MenuBuilder<'m, R, M> { + /// Create a new menu builder. + pub fn new(manager: &'m M) -> Self { + Self { + id: None, + items: Vec::new(), + manager, + } + } + + /// Create a new menu builder with the specified id. + pub fn with_id>(manager: &'m M, id: I) -> Self { + Self { + id: Some(id.into()), + items: Vec::new(), + manager, + } + } + + /// Set the id for this menu. + pub fn id>(mut self, id: I) -> Self { + self.id.replace(id.into()); + self + } + + /// Add this item to the menu. + pub fn item(mut self, item: &dyn IsMenuItem) -> Self { + self.items.push(item.kind()); + self + } + + /// Add these items to the menu. + pub fn items(mut self, items: &[&dyn IsMenuItem]) -> Self { + for item in items { + self = self.item(*item); + } + self + } + + /// Add a [MenuItem] to the menu. + pub fn text, S: AsRef>(mut self, id: I, text: S) -> Self { + self + .items + .push(MenuItem::with_id(self.manager, id, text, true, None).kind()); + self + } + + /// Add a [CheckMenuItem] to the menu. + pub fn check, S: AsRef>(mut self, id: I, text: S) -> Self { + self + .items + .push(CheckMenuItem::with_id(self.manager, id, text, true, true, None).kind()); + self + } + + /// Add an [IconMenuItem] to the menu. + pub fn icon, S: AsRef>(mut self, id: I, text: S, icon: Icon) -> Self { + self + .items + .push(IconMenuItem::with_id(self.manager, id, text, true, Some(icon), None).kind()); + self + } + + /// Add an [IconMenuItem] with a native icon to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn native_icon, S: AsRef>( + mut self, + id: I, + text: S, + icon: NativeIcon, + ) -> Self { + self.items.push( + IconMenuItem::with_id_and_native_icon(self.manager, id, text, true, Some(icon), None).kind(), + ); + self + } + + /// Add Separator menu item to the menu. + pub fn separator(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::separator(self.manager).kind()); + self + } + + /// Add Copy menu item to the menu. + pub fn copy(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::copy(self.manager, None).kind()); + self + } + + /// Add Cut menu item to the menu. + pub fn cut(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::cut(self.manager, None).kind()); + self + } + + /// Add Paste menu item to the menu. + pub fn paste(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::paste(self.manager, None).kind()); + self + } + + /// Add SelectAll menu item to the menu. + pub fn select_all(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::select_all(self.manager, None).kind()); + self + } + + /// Add Undo menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn undo(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::undo(self.manager, None).kind()); + self + } + /// Add Redo menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn redo(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::redo(self.manager, None).kind()); + self + } + + /// Add Minimize window menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn minimize(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::minimize(self.manager, None).kind()); + self + } + + /// Add Maximize window menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn maximize(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::maximize(self.manager, None).kind()); + self + } + + /// Add Fullscreen menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn fullscreen(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::fullscreen(self.manager, None).kind()); + self + } + + /// Add Hide window menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::hide(self.manager, None).kind()); + self + } + + /// Add Hide other windows menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide_others(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::hide_others(self.manager, None).kind()); + self + } + + /// Add Show all app windows menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn show_all(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::show_all(self.manager, None).kind()); + self + } + + /// Add Close window menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn close_window(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::close_window(self.manager, None).kind()); + self + } + + /// Add Quit app menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn quit(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::quit(self.manager, None).kind()); + self + } + + /// Add About app menu item to the menu. + pub fn about(mut self, metadata: Option) -> Self { + self + .items + .push(PredefinedMenuItem::about(self.manager, None, metadata).kind()); + self + } + + /// Add Services menu item to the menu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn services(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::services(self.manager, None).kind()); + self + } + + /// Builds this menu + pub fn build(self) -> crate::Result> { + if self.items.is_empty() { + Ok(if let Some(id) = self.id { + Menu::with_id(self.manager, id) + } else { + Menu::new(self.manager) + }) + } else { + let items = self + .items + .iter() + .map(|i| i as &dyn IsMenuItem) + .collect::>(); + if let Some(id) = self.id { + Menu::with_id_and_items(self.manager, id, &items) + } else { + Menu::with_items(self.manager, &items) + } + } + } +} diff --git a/core/tauri/src/menu/builders/mod.rs b/core/tauri/src/menu/builders/mod.rs new file mode 100644 index 00000000000..f86baee6b27 --- /dev/null +++ b/core/tauri/src/menu/builders/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg(desktop)] + +//! A module containting menu builder types + +pub use muda::AboutMetadataBuilder; + +mod menu; +pub use menu::MenuBuilder; +mod normal; +pub use normal::MenuItemBuilder; +mod submenu; +pub use submenu::SubmenuBuilder; +mod check; +pub use check::CheckMenuItemBuilder; +mod icon; +pub use icon::IconMenuItemBuilder; diff --git a/core/tauri/src/menu/builders/normal.rs b/core/tauri/src/menu/builders/normal.rs new file mode 100644 index 00000000000..f70abe388db --- /dev/null +++ b/core/tauri/src/menu/builders/normal.rs @@ -0,0 +1,68 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{menu::MenuId, menu::MenuItem, Manager, Runtime}; + +/// A builder type for [`MenuItem`] +pub struct MenuItemBuilder { + id: Option, + text: String, + enabled: bool, + accelerator: Option, +} + +impl MenuItemBuilder { + /// Create a new menu item builder. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new>(text: S) -> Self { + Self { + id: None, + text: text.as_ref().to_string(), + enabled: true, + accelerator: None, + } + } + + /// Create a new menu item builder with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, S: AsRef>(id: I, text: S) -> Self { + Self { + id: Some(id.into()), + text: text.as_ref().to_string(), + enabled: true, + accelerator: None, + } + } + + /// Set the id for this menu item. + pub fn id>(mut self, id: I) -> Self { + self.id.replace(id.into()); + self + } + + /// Set the enabled state for this menu item. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Set the accelerator for this menu item. + pub fn accelerator>(mut self, accelerator: S) -> Self { + self.accelerator.replace(accelerator.as_ref().to_string()); + self + } + + /// Build the menu item + pub fn build>(self, manager: &M) -> MenuItem { + if let Some(id) = self.id { + MenuItem::with_id(manager, id, self.text, self.enabled, self.accelerator) + } else { + MenuItem::new(manager, self.text, self.enabled, self.accelerator) + } + } +} diff --git a/core/tauri/src/menu/builders/submenu.rs b/core/tauri/src/menu/builders/submenu.rs new file mode 100644 index 00000000000..719ff1fb533 --- /dev/null +++ b/core/tauri/src/menu/builders/submenu.rs @@ -0,0 +1,347 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{menu::*, Icon, Manager, Runtime}; + +/// A builder type for [`Submenu`] +/// +/// # Example +/// +/// ```no_run +/// use tauri::menu::*; +/// tauri::Builder::default() +/// .setup(move |app| { +/// let handle = app.handle(); +/// # let icon1 = tauri::Icon::Rgba { +/// # rgba: Vec::new(), +/// # width: 0, +/// # height: 0, +/// # }; +/// # let icon2 = icon1.clone(); +/// let menu = Menu::new(handle); +/// let submenu = SubmenuBuilder::new(handle, "File") +/// .item(&MenuItem::new(handle, "MenuItem 1", true, None)) +/// .items(&[ +/// &CheckMenuItem::new(handle, "CheckMenuItem 1", true, true, None), +/// &IconMenuItem::new(handle, "IconMenuItem 1", true, Some(icon1), None), +/// ]) +/// .separator() +/// .cut() +/// .copy() +/// .paste() +/// .separator() +/// .text("item2", "MenuItem 2") +/// .check("checkitem2", "CheckMenuItem 2") +/// .icon("iconitem2", "IconMenuItem 2", app.default_window_icon().cloned().unwrap()) +/// .build()?; +/// menu.append(&submenu)?; +/// app.set_menu(menu); +/// Ok(()) +/// }); +/// ``` +pub struct SubmenuBuilder<'m, R: Runtime, M: Manager> { + id: Option, + manager: &'m M, + text: String, + enabled: bool, + items: Vec>, +} + +impl<'m, R: Runtime, M: Manager> SubmenuBuilder<'m, R, M> { + /// Create a new submenu builder. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new>(manager: &'m M, text: S) -> Self { + Self { + id: None, + items: Vec::new(), + text: text.as_ref().to_string(), + enabled: true, + manager, + } + } + + /// Create a new submenu builder with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, S: AsRef>(manager: &'m M, id: I, text: S) -> Self { + Self { + id: Some(id.into()), + text: text.as_ref().to_string(), + enabled: true, + items: Vec::new(), + manager, + } + } + + /// Set the id for this submenu. + pub fn id>(mut self, id: I) -> Self { + self.id.replace(id.into()); + self + } + + /// Set the enabled state for the submenu. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Add this item to the submenu. + pub fn item(mut self, item: &dyn IsMenuItem) -> Self { + self.items.push(item.kind()); + self + } + + /// Add these items to the submenu. + pub fn items(mut self, items: &[&dyn IsMenuItem]) -> Self { + for item in items { + self = self.item(*item); + } + self + } + + /// Add a [MenuItem] to the submenu. + pub fn text, S: AsRef>(mut self, id: I, text: S) -> Self { + self + .items + .push(MenuItem::with_id(self.manager, id, text, true, None).kind()); + self + } + + /// Add a [CheckMenuItem] to the submenu. + pub fn check, S: AsRef>(mut self, id: I, text: S) -> Self { + self + .items + .push(CheckMenuItem::with_id(self.manager, id, text, true, true, None).kind()); + self + } + + /// Add an [IconMenuItem] to the submenu. + pub fn icon, S: AsRef>(mut self, id: I, text: S, icon: Icon) -> Self { + self + .items + .push(IconMenuItem::with_id(self.manager, id, text, true, Some(icon), None).kind()); + self + } + + /// Add an [IconMenuItem] with a native icon to the submenu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn native_icon, S: AsRef>( + mut self, + id: I, + text: S, + icon: NativeIcon, + ) -> Self { + self.items.push( + IconMenuItem::with_id_and_native_icon(self.manager, id, text, true, Some(icon), None).kind(), + ); + self + } + + /// Add Separator menu item to the submenu. + pub fn separator(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::separator(self.manager).kind()); + self + } + + /// Add Copy menu item to the submenu. + pub fn copy(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::copy(self.manager, None).kind()); + self + } + + /// Add Cut menu item to the submenu. + pub fn cut(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::cut(self.manager, None).kind()); + self + } + + /// Add Paste menu item to the submenu. + pub fn paste(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::paste(self.manager, None).kind()); + self + } + + /// Add SelectAll menu item to the submenu. + pub fn select_all(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::select_all(self.manager, None).kind()); + self + } + + /// Add Undo menu item to the submenu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn undo(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::undo(self.manager, None).kind()); + self + } + /// Add Redo menu item to the submenu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn redo(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::redo(self.manager, None).kind()); + self + } + + /// Add Minimize window menu item to the submenu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn minimize(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::minimize(self.manager, None).kind()); + self + } + + /// Add Maximize window menu item to the submenu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn maximize(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::maximize(self.manager, None).kind()); + self + } + + /// Add Fullscreen menu item to the submenu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn fullscreen(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::fullscreen(self.manager, None).kind()); + self + } + + /// Add Hide window menu item to the submenu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::hide(self.manager, None).kind()); + self + } + + /// Add Hide other windows menu item to the submenu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide_others(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::hide_others(self.manager, None).kind()); + self + } + + /// Add Show all app windows menu item to the submenu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn show_all(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::show_all(self.manager, None).kind()); + self + } + + /// Add Close window menu item to the submenu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn close_window(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::close_window(self.manager, None).kind()); + self + } + + /// Add Quit app menu item to the submenu. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn quit(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::quit(self.manager, None).kind()); + self + } + + /// Add About app menu item to the submenu. + pub fn about(mut self, metadata: Option) -> Self { + self + .items + .push(PredefinedMenuItem::about(self.manager, None, metadata).kind()); + self + } + + /// Add Services menu item to the submenu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn services(mut self) -> Self { + self + .items + .push(PredefinedMenuItem::services(self.manager, None).kind()); + self + } + + /// Builds this submenu + pub fn build(self) -> crate::Result> { + if self.items.is_empty() { + Ok(if let Some(id) = self.id { + Submenu::with_id(self.manager, id, self.text, self.enabled) + } else { + Submenu::new(self.manager, self.text, self.enabled) + }) + } else { + let items = self + .items + .iter() + .map(|i| i as &dyn IsMenuItem) + .collect::>(); + if let Some(id) = self.id { + Submenu::with_id_and_items(self.manager, id, self.text, self.enabled, &items) + } else { + Submenu::with_items(self.manager, self.text, self.enabled, &items) + } + } + } +} diff --git a/core/tauri/src/menu/check.rs b/core/tauri/src/menu/check.rs new file mode 100644 index 00000000000..28d90990f2e --- /dev/null +++ b/core/tauri/src/menu/check.rs @@ -0,0 +1,148 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{menu::MenuId, run_main_thread, AppHandle, Manager, Runtime}; + +/// A menu item inside a [`Menu`] or [`Submenu`] and contains only text. +/// +/// [`Menu`]: super::Menu +/// [`Submenu`]: super::Submenu +pub struct CheckMenuItem { + pub(crate) id: MenuId, + pub(crate) inner: muda::CheckMenuItem, + pub(crate) app_handle: AppHandle, +} + +impl Clone for CheckMenuItem { + fn clone(&self) -> Self { + Self { + id: self.id.clone(), + inner: self.inner.clone(), + app_handle: self.app_handle.clone(), + } + } +} + +/// # Safety +/// +/// We make sure it always runs on the main thread. +unsafe impl Sync for CheckMenuItem {} +unsafe impl Send for CheckMenuItem {} + +impl super::sealed::IsMenuItemBase for CheckMenuItem { + fn inner(&self) -> &dyn muda::IsMenuItem { + &self.inner + } +} + +impl super::IsMenuItem for CheckMenuItem { + fn kind(&self) -> super::MenuItemKind { + super::MenuItemKind::Check(self.clone()) + } + + fn id(&self) -> &MenuId { + &self.id + } +} + +impl CheckMenuItem { + /// Create a new menu item. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new, S: AsRef>( + manager: &M, + text: S, + enabled: bool, + checked: bool, + acccelerator: Option, + ) -> Self { + let item = muda::CheckMenuItem::new( + text, + enabled, + checked, + acccelerator.and_then(|s| s.as_ref().parse().ok()), + ); + Self { + id: item.id().clone(), + inner: item, + app_handle: manager.app_handle().clone(), + } + } + + /// Create a new menu item with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, I: Into, S: AsRef>( + manager: &M, + id: I, + text: S, + enabled: bool, + checked: bool, + acccelerator: Option, + ) -> Self { + let item = muda::CheckMenuItem::with_id( + id, + text, + enabled, + checked, + acccelerator.and_then(|s| s.as_ref().parse().ok()), + ); + Self { + id: item.id().clone(), + inner: item, + app_handle: manager.app_handle().clone(), + } + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.app_handle + } + + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + &self.id + } + + /// Get the text for this menu item. + pub fn text(&self) -> crate::Result { + run_main_thread!(self, |self_: Self| self_.inner.text()) + } + + /// Set the text for this menu item. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_main_thread!(self, |self_: Self| self_.inner.set_text(text)) + } + + /// Get whether this menu item is enabled or not. + pub fn is_enabled(&self) -> crate::Result { + run_main_thread!(self, |self_: Self| self_.inner.is_enabled()) + } + + /// Enable or disable this menu item. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + run_main_thread!(self, |self_: Self| self_.inner.set_enabled(enabled)) + } + + /// Set this menu item accelerator. + pub fn set_accelerator>(&self, acccelerator: Option) -> crate::Result<()> { + let accel = acccelerator.and_then(|s| s.as_ref().parse().ok()); + run_main_thread!(self, |self_: Self| self_.inner.set_accelerator(accel))?.map_err(Into::into) + } + + /// Get whether this check menu item is checked or not. + pub fn is_checked(&self) -> crate::Result { + run_main_thread!(self, |self_: Self| self_.inner.is_checked()) + } + + /// Check or Uncheck this check menu item. + pub fn set_checked(&self, checked: bool) -> crate::Result<()> { + run_main_thread!(self, |self_: Self| self_.inner.set_checked(checked)) + } +} diff --git a/core/tauri/src/menu/icon.rs b/core/tauri/src/menu/icon.rs new file mode 100644 index 00000000000..699a0ea9f49 --- /dev/null +++ b/core/tauri/src/menu/icon.rs @@ -0,0 +1,216 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::NativeIcon; +use crate::{menu::MenuId, run_main_thread, AppHandle, Icon, Manager, Runtime}; + +/// A menu item inside a [`Menu`] or [`Submenu`] and contains only text. +/// +/// [`Menu`]: super::Menu +/// [`Submenu`]: super::Submenu +pub struct IconMenuItem { + pub(crate) id: MenuId, + pub(crate) inner: muda::IconMenuItem, + pub(crate) app_handle: AppHandle, +} + +impl Clone for IconMenuItem { + fn clone(&self) -> Self { + Self { + id: self.id.clone(), + inner: self.inner.clone(), + app_handle: self.app_handle.clone(), + } + } +} + +/// # Safety +/// +/// We make sure it always runs on the main thread. +unsafe impl Sync for IconMenuItem {} +unsafe impl Send for IconMenuItem {} + +impl super::sealed::IsMenuItemBase for IconMenuItem { + fn inner(&self) -> &dyn muda::IsMenuItem { + &self.inner + } +} + +impl super::IsMenuItem for IconMenuItem { + fn kind(&self) -> super::MenuItemKind { + super::MenuItemKind::Icon(self.clone()) + } + + fn id(&self) -> &MenuId { + &self.id + } +} + +impl IconMenuItem { + /// Create a new menu item. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new, S: AsRef>( + manager: &M, + text: S, + enabled: bool, + icon: Option, + acccelerator: Option, + ) -> Self { + let item = muda::IconMenuItem::new( + text, + enabled, + icon.and_then(|i| i.try_into().ok()), + acccelerator.and_then(|s| s.as_ref().parse().ok()), + ); + Self { + id: item.id().clone(), + inner: item, + app_handle: manager.app_handle().clone(), + } + } + + /// Create a new menu item with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, I: Into, S: AsRef>( + manager: &M, + id: I, + text: S, + enabled: bool, + icon: Option, + acccelerator: Option, + ) -> Self { + let item = muda::IconMenuItem::with_id( + id, + text, + enabled, + icon.and_then(|i| i.try_into().ok()), + acccelerator.and_then(|s| s.as_ref().parse().ok()), + ); + Self { + id: item.id().clone(), + inner: item, + app_handle: manager.app_handle().clone(), + } + } + + /// Create a new icon menu item but with a native icon. + /// + /// See [`IconMenuItem::new`] for more info. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn with_native_icon, S: AsRef>( + manager: &M, + text: S, + enabled: bool, + native_icon: Option, + acccelerator: Option, + ) -> Self { + let item = muda::IconMenuItem::with_native_icon( + text, + enabled, + native_icon.map(Into::into), + acccelerator.and_then(|s| s.as_ref().parse().ok()), + ); + Self { + id: item.id().clone(), + inner: item, + app_handle: manager.app_handle().clone(), + } + } + + /// Create a new icon menu item with the specified id but with a native icon. + /// + /// See [`IconMenuItem::new`] for more info. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn with_id_and_native_icon, I: Into, S: AsRef>( + manager: &M, + id: I, + text: S, + enabled: bool, + native_icon: Option, + acccelerator: Option, + ) -> Self { + let item = muda::IconMenuItem::with_id_and_native_icon( + id, + text, + enabled, + native_icon.map(Into::into), + acccelerator.and_then(|s| s.as_ref().parse().ok()), + ); + Self { + id: item.id().clone(), + inner: item, + app_handle: manager.app_handle().clone(), + } + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.app_handle + } + + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + &self.id + } + + /// Get the text for this menu item. + pub fn text(&self) -> crate::Result { + run_main_thread!(self, |self_: Self| self_.inner.text()) + } + + /// Set the text for this menu item. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_main_thread!(self, |self_: Self| self_.inner.set_text(text)) + } + + /// Get whether this menu item is enabled or not. + pub fn is_enabled(&self) -> crate::Result { + run_main_thread!(self, |self_: Self| self_.inner.is_enabled()) + } + + /// Enable or disable this menu item. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + run_main_thread!(self, |self_: Self| self_.inner.set_enabled(enabled)) + } + + /// Set this menu item accelerator. + pub fn set_accelerator>(&self, acccelerator: Option) -> crate::Result<()> { + let accel = acccelerator.and_then(|s| s.as_ref().parse().ok()); + run_main_thread!(self, |self_: Self| self_.inner.set_accelerator(accel))?.map_err(Into::into) + } + + /// Change this menu item icon or remove it. + pub fn set_icon(&self, icon: Option) -> crate::Result<()> { + run_main_thread!(self, |self_: Self| self_ + .inner + .set_icon(icon.and_then(|i| i.try_into().ok()))) + } + + /// Change this menu item icon to a native image or remove it. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn set_native_icon(&mut self, _icon: Option) -> crate::Result<()> { + #[cfg(target_os = "macos")] + return run_main_thread!(self, |mut self_: Self| self_ + .inner + .set_native_icon(_icon.map(Into::into))); + #[allow(unreachable_code)] + Ok(()) + } +} diff --git a/core/tauri/src/menu/menu.rs b/core/tauri/src/menu/menu.rs new file mode 100644 index 00000000000..a29ecb3300d --- /dev/null +++ b/core/tauri/src/menu/menu.rs @@ -0,0 +1,397 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::sealed::ContextMenuBase; +use super::{AboutMetadata, IsMenuItem, MenuItemKind, PredefinedMenuItem, Submenu}; +use crate::Window; +use crate::{run_main_thread, AppHandle, Manager, Position, Runtime}; +use muda::ContextMenu; +use muda::MenuId; + +/// Expected submenu id of the Window menu for macOS. +pub const WINDOW_SUBMENU_ID: &str = "__tauri_window_menu__"; +/// Expected submenu id of the Help menu for macOS. +pub const HELP_SUBMENU_ID: &str = "__tauri_help_menu__"; + +/// A type that is either a menu bar on the window +/// on Windows and Linux or as a global menu in the menubar on macOS. +pub struct Menu { + pub(crate) id: MenuId, + pub(crate) inner: muda::Menu, + pub(crate) app_handle: AppHandle, +} + +/// # Safety +/// +/// We make sure it always runs on the main thread. +unsafe impl Sync for Menu {} +unsafe impl Send for Menu {} + +impl Clone for Menu { + fn clone(&self) -> Self { + Self { + id: self.id.clone(), + inner: self.inner.clone(), + app_handle: self.app_handle.clone(), + } + } +} + +impl super::ContextMenu for Menu { + fn popup(&self, window: Window) -> crate::Result<()> { + self.popup_inner(window, None::) + } + + fn popup_at>( + &self, + window: Window, + position: P, + ) -> crate::Result<()> { + self.popup_inner(window, Some(position)) + } +} + +impl ContextMenuBase for Menu { + fn popup_inner>( + &self, + window: crate::Window, + position: Option

, + ) -> crate::Result<()> { + let position = position.map(Into::into).map(super::into_position); + run_main_thread!(self, move |self_: Self| { + #[cfg(target_os = "macos")] + if let Ok(view) = window.ns_view() { + self_ + .inner() + .show_context_menu_for_nsview(view as _, position); + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(w) = window.gtk_window() { + self_.inner().show_context_menu_for_gtk_window(&w, position); + } + + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + self_.inner().show_context_menu_for_hwnd(hwnd.0, position) + } + }) + } + fn inner(&self) -> &dyn muda::ContextMenu { + &self.inner + } + + fn inner_owned(&self) -> Box { + Box::new(self.clone().inner) + } +} + +impl Menu { + /// Creates a new menu. + pub fn new>(manager: &M) -> Self { + let menu = muda::Menu::new(); + Self { + id: menu.id().clone(), + inner: menu, + app_handle: manager.app_handle().clone(), + } + } + + /// Creates a new menu with the specified id. + pub fn with_id, I: Into>(manager: &M, id: I) -> Self { + let menu = muda::Menu::with_id(id); + Self { + id: menu.id().clone(), + inner: menu, + app_handle: manager.app_handle().clone(), + } + } + + /// Creates a new menu with given `items`. It calls [`Menu::new`] and [`Menu::append_items`] internally. + pub fn with_items>( + manager: &M, + items: &[&dyn IsMenuItem], + ) -> crate::Result { + let menu = Self::new(manager); + menu.append_items(items)?; + Ok(menu) + } + + /// Creates a new menu with the specified id and given `items`. + /// It calls [`Menu::new`] and [`Menu::append_items`] internally. + pub fn with_id_and_items, I: Into>( + manager: &M, + id: I, + items: &[&dyn IsMenuItem], + ) -> crate::Result { + let menu = Self::with_id(manager, id); + menu.append_items(items)?; + Ok(menu) + } + + /// Creates a menu filled with default menu items and submenus. + pub fn default(app_handle: &AppHandle) -> crate::Result { + let pkg_info = app_handle.package_info(); + let config = app_handle.config(); + let about_metadata = AboutMetadata { + name: Some(pkg_info.name.clone()), + version: Some(pkg_info.version.to_string()), + copyright: config.tauri.bundle.copyright.clone(), + authors: config.tauri.bundle.publisher.clone().map(|p| vec![p]), + ..Default::default() + }; + + let window_menu = Submenu::with_id_and_items( + app_handle, + WINDOW_SUBMENU_ID, + "Window", + true, + &[ + &PredefinedMenuItem::minimize(app_handle, None), + &PredefinedMenuItem::maximize(app_handle, None), + #[cfg(target_os = "macos")] + &PredefinedMenuItem::separator(app_handle), + &PredefinedMenuItem::close_window(app_handle, None), + ], + )?; + + let help_menu = Submenu::with_id_and_items( + app_handle, + HELP_SUBMENU_ID, + "Help", + true, + &[ + #[cfg(not(target_os = "macos"))] + &PredefinedMenuItem::about(app_handle, None, Some(about_metadata)), + ], + )?; + + let menu = Menu::with_items( + app_handle, + &[ + #[cfg(target_os = "macos")] + &Submenu::with_items( + app_handle, + pkg_info.name.clone(), + true, + &[ + &PredefinedMenuItem::about(app_handle, None, Some(about_metadata)), + &PredefinedMenuItem::separator(app_handle), + &PredefinedMenuItem::services(app_handle, None), + &PredefinedMenuItem::separator(app_handle), + &PredefinedMenuItem::hide(app_handle, None), + &PredefinedMenuItem::hide_others(app_handle, None), + &PredefinedMenuItem::separator(app_handle), + &PredefinedMenuItem::quit(app_handle, None), + ], + )?, + #[cfg(not(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + )))] + &Submenu::with_items( + app_handle, + "File", + true, + &[ + &PredefinedMenuItem::close_window(app_handle, None), + #[cfg(not(target_os = "macos"))] + &PredefinedMenuItem::quit(app_handle, None), + ], + )?, + &Submenu::with_items( + app_handle, + "Edit", + true, + &[ + &PredefinedMenuItem::undo(app_handle, None), + &PredefinedMenuItem::redo(app_handle, None), + &PredefinedMenuItem::separator(app_handle), + &PredefinedMenuItem::cut(app_handle, None), + &PredefinedMenuItem::copy(app_handle, None), + &PredefinedMenuItem::paste(app_handle, None), + &PredefinedMenuItem::select_all(app_handle, None), + ], + )?, + #[cfg(target_os = "macos")] + &Submenu::with_items( + app_handle, + "View", + true, + &[&PredefinedMenuItem::fullscreen(app_handle, None)], + )?, + &window_menu, + &help_menu, + ], + )?; + + Ok(menu) + } + + pub(crate) fn inner(&self) -> &muda::Menu { + &self.inner + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.app_handle + } + + /// Returns a unique identifier associated with this menu. + pub fn id(&self) -> &MenuId { + &self.id + } + + /// Add a menu item to the end of this menu. + /// + /// ## Platform-spcific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu. + /// + /// [`Submenu`]: super::Submenu + pub fn append(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_main_thread!(self, |self_: Self| self_.inner.append(kind.inner().inner()))? + .map_err(Into::into) + } + + /// Add menu items to the end of this menu. It calls [`Menu::append`] in a loop internally. + /// + /// ## Platform-spcific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn append_items(&self, items: &[&dyn IsMenuItem]) -> crate::Result<()> { + for item in items { + self.append(*item)? + } + + Ok(()) + } + + /// Add a menu item to the beginning of this menu. + /// + /// ## Platform-spcific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn prepend(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_main_thread!(self, |self_: Self| self_ + .inner + .prepend(kind.inner().inner()))? + .map_err(Into::into) + } + + /// Add menu items to the beginning of this menu. It calls [`Menu::insert_items`] with position of `0` internally. + /// + /// ## Platform-spcific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn prepend_items(&self, items: &[&dyn IsMenuItem]) -> crate::Result<()> { + self.insert_items(items, 0) + } + + /// Insert a menu item at the specified `postion` in the menu. + /// + /// ## Platform-spcific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn insert(&self, item: &dyn IsMenuItem, position: usize) -> crate::Result<()> { + let kind = item.kind(); + run_main_thread!(self, |self_: Self| self_ + .inner + .insert(kind.inner().inner(), position))? + .map_err(Into::into) + } + + /// Insert menu items at the specified `postion` in the menu. + /// + /// ## Platform-spcific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: super::Submenu + pub fn insert_items(&self, items: &[&dyn IsMenuItem], position: usize) -> crate::Result<()> { + for (i, item) in items.iter().enumerate() { + self.insert(*item, position + i)? + } + + Ok(()) + } + + /// Remove a menu item from this menu. + pub fn remove(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_main_thread!(self, |self_: Self| self_.inner.remove(kind.inner().inner()))? + .map_err(Into::into) + } + + /// Retrieves the menu item matching the given identifier. + pub fn get<'a, I>(&self, id: &'a I) -> Option> + where + I: ?Sized, + MenuId: PartialEq<&'a I>, + { + self + .items() + .unwrap_or_default() + .into_iter() + .find(|i| i.id() == &id) + } + + /// Returns a list of menu items that has been added to this menu. + pub fn items(&self) -> crate::Result>> { + let handle = self.app_handle.clone(); + run_main_thread!(self, |self_: Self| self_ + .inner + .items() + .into_iter() + .map(|i| match i { + muda::MenuItemKind::MenuItem(i) => super::MenuItemKind::MenuItem(super::MenuItem { + id: i.id().clone(), + inner: i, + app_handle: handle.clone(), + }), + muda::MenuItemKind::Submenu(i) => super::MenuItemKind::Submenu(super::Submenu { + id: i.id().clone(), + inner: i, + app_handle: handle.clone(), + }), + muda::MenuItemKind::Predefined(i) => { + super::MenuItemKind::Predefined(super::PredefinedMenuItem { + id: i.id().clone(), + inner: i, + app_handle: handle.clone(), + }) + } + muda::MenuItemKind::Check(i) => super::MenuItemKind::Check(super::CheckMenuItem { + id: i.id().clone(), + inner: i, + app_handle: handle.clone(), + }), + muda::MenuItemKind::Icon(i) => super::MenuItemKind::Icon(super::IconMenuItem { + id: i.id().clone(), + inner: i, + app_handle: handle.clone(), + }), + }) + .collect::>()) + } +} diff --git a/core/tauri/src/menu/mod.rs b/core/tauri/src/menu/mod.rs new file mode 100644 index 00000000000..bdfef026836 --- /dev/null +++ b/core/tauri/src/menu/mod.rs @@ -0,0 +1,636 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg(desktop)] + +//! Menu types and utility functions + +// TODO(muda-migration): figure out js events + +mod builders; +mod check; +mod icon; +#[allow(clippy::module_inception)] +mod menu; +mod normal; +mod predefined; +mod submenu; +pub use builders::*; +pub use check::CheckMenuItem; +pub use icon::IconMenuItem; +pub use menu::{Menu, HELP_SUBMENU_ID, WINDOW_SUBMENU_ID}; +pub use normal::MenuItem; +pub use predefined::PredefinedMenuItem; +pub use submenu::Submenu; + +use crate::{Icon, Runtime}; +pub use muda::MenuId; + +/// Describes a menu event emitted when a menu item is activated +#[derive(Debug, Clone)] +pub struct MenuEvent { + /// Id of the menu item which triggered this event + pub id: MenuId, +} + +impl MenuEvent { + /// Returns the id of the menu item which triggered this event + pub fn id(&self) -> &MenuId { + &self.id + } +} + +impl From for MenuEvent { + fn from(value: muda::MenuEvent) -> Self { + Self { id: value.id } + } +} + +/// Application metadata for the [`PredefinedMenuItem::about`](crate::PredefinedMenuItem::about). +#[derive(Debug, Clone, Default)] +pub struct AboutMetadata { + /// Sets the application name. + pub name: Option, + /// The application version. + pub version: Option, + /// The short version, e.g. "1.0". + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Appended to the end of `version` in parentheses. + pub short_version: Option, + /// The authors of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub authors: Option>, + /// Application comments. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub comments: Option, + /// The copyright of the application. + pub copyright: Option, + /// The license of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub license: Option, + /// The application website. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub website: Option, + /// The website label. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub website_label: Option, + /// The credits. + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Unsupported. + pub credits: Option, + /// The application icon. + /// + /// ## Platform-specific + /// + /// - **Windows:** Unsupported. + pub icon: Option, +} + +/// A builder type for [`AboutMetadata`]. +#[derive(Clone, Debug, Default)] +pub struct AboutMetadataBuilder(AboutMetadata); + +impl AboutMetadataBuilder { + /// Create a new about metdata builder. + pub fn new() -> Self { + Default::default() + } + + /// Sets the application name. + pub fn name>(mut self, name: Option) -> Self { + self.0.name = name.map(|s| s.into()); + self + } + /// Sets the application version. + pub fn version>(mut self, version: Option) -> Self { + self.0.version = version.map(|s| s.into()); + self + } + /// Sets the short version, e.g. "1.0". + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Appended to the end of `version` in parentheses. + pub fn short_version>(mut self, short_version: Option) -> Self { + self.0.short_version = short_version.map(|s| s.into()); + self + } + /// Sets the authors of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn authors(mut self, authors: Option>) -> Self { + self.0.authors = authors; + self + } + /// Application comments. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn comments>(mut self, comments: Option) -> Self { + self.0.comments = comments.map(|s| s.into()); + self + } + /// Sets the copyright of the application. + pub fn copyright>(mut self, copyright: Option) -> Self { + self.0.copyright = copyright.map(|s| s.into()); + self + } + /// Sets the license of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn license>(mut self, license: Option) -> Self { + self.0.license = license.map(|s| s.into()); + self + } + /// Sets the application website. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn website>(mut self, website: Option) -> Self { + self.0.website = website.map(|s| s.into()); + self + } + /// Sets the website label. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn website_label>(mut self, website_label: Option) -> Self { + self.0.website_label = website_label.map(|s| s.into()); + self + } + /// Sets the credits. + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Unsupported. + pub fn credits>(mut self, credits: Option) -> Self { + self.0.credits = credits.map(|s| s.into()); + self + } + /// Sets the application icon. + /// + /// ## Platform-specific + /// + /// - **Windows:** Unsupported. + pub fn icon(mut self, icon: Option) -> Self { + self.0.icon = icon; + self + } + + /// Construct the final [`AboutMetadata`] + pub fn build(self) -> AboutMetadata { + self.0 + } +} + +impl From for muda::AboutMetadata { + fn from(value: AboutMetadata) -> Self { + Self { + authors: value.authors, + name: value.name, + version: value.version, + short_version: value.short_version, + comments: value.comments, + copyright: value.copyright, + license: value.license, + website: value.website, + website_label: value.website_label, + credits: value.credits, + icon: value.icon.and_then(|i| i.try_into().ok()), + } + } +} + +/// A native Icon to be used for the menu item +/// +/// ## Platform-specific: +/// +/// - **Windows / Linux**: Unsupported. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeIcon { + /// An add item template image. + Add, + /// Advanced preferences toolbar icon for the preferences window. + Advanced, + /// A Bluetooth template image. + Bluetooth, + /// Bookmarks image suitable for a template. + Bookmarks, + /// A caution image. + Caution, + /// A color panel toolbar icon. + ColorPanel, + /// A column view mode template image. + ColumnView, + /// A computer icon. + Computer, + /// An enter full-screen mode template image. + EnterFullScreen, + /// Permissions for all users. + Everyone, + /// An exit full-screen mode template image. + ExitFullScreen, + /// A cover flow view mode template image. + FlowView, + /// A folder image. + Folder, + /// A burnable folder icon. + FolderBurnable, + /// A smart folder icon. + FolderSmart, + /// A link template image. + FollowLinkFreestanding, + /// A font panel toolbar icon. + FontPanel, + /// A `go back` template image. + GoLeft, + /// A `go forward` template image. + GoRight, + /// Home image suitable for a template. + Home, + /// An iChat Theater template image. + IChatTheater, + /// An icon view mode template image. + IconView, + /// An information toolbar icon. + Info, + /// A template image used to denote invalid data. + InvalidDataFreestanding, + /// A generic left-facing triangle template image. + LeftFacingTriangle, + /// A list view mode template image. + ListView, + /// A locked padlock template image. + LockLocked, + /// An unlocked padlock template image. + LockUnlocked, + /// A horizontal dash, for use in menus. + MenuMixedState, + /// A check mark template image, for use in menus. + MenuOnState, + /// A MobileMe icon. + MobileMe, + /// A drag image for multiple items. + MultipleDocuments, + /// A network icon. + Network, + /// A path button template image. + Path, + /// General preferences toolbar icon for the preferences window. + PreferencesGeneral, + /// A Quick Look template image. + QuickLook, + /// A refresh template image. + RefreshFreestanding, + /// A refresh template image. + Refresh, + /// A remove item template image. + Remove, + /// A reveal contents template image. + RevealFreestanding, + /// A generic right-facing triangle template image. + RightFacingTriangle, + /// A share view template image. + Share, + /// A slideshow template image. + Slideshow, + /// A badge for a `smart` item. + SmartBadge, + /// Small green indicator, similar to iChat’s available image. + StatusAvailable, + /// Small clear indicator. + StatusNone, + /// Small yellow indicator, similar to iChat’s idle image. + StatusPartiallyAvailable, + /// Small red indicator, similar to iChat’s unavailable image. + StatusUnavailable, + /// A stop progress template image. + StopProgressFreestanding, + /// A stop progress button template image. + StopProgress, + /// An image of the empty trash can. + TrashEmpty, + /// An image of the full trash can. + TrashFull, + /// Permissions for a single user. + User, + /// User account toolbar icon for the preferences window. + UserAccounts, + /// Permissions for a group of users. + UserGroup, + /// Permissions for guests. + UserGuest, +} + +impl From for muda::NativeIcon { + fn from(value: NativeIcon) -> Self { + match value { + NativeIcon::Add => muda::NativeIcon::Add, + NativeIcon::Advanced => muda::NativeIcon::Advanced, + NativeIcon::Bluetooth => muda::NativeIcon::Bluetooth, + NativeIcon::Bookmarks => muda::NativeIcon::Bookmarks, + NativeIcon::Caution => muda::NativeIcon::Caution, + NativeIcon::ColorPanel => muda::NativeIcon::ColorPanel, + NativeIcon::ColumnView => muda::NativeIcon::ColumnView, + NativeIcon::Computer => muda::NativeIcon::Computer, + NativeIcon::EnterFullScreen => muda::NativeIcon::EnterFullScreen, + NativeIcon::Everyone => muda::NativeIcon::Everyone, + NativeIcon::ExitFullScreen => muda::NativeIcon::ExitFullScreen, + NativeIcon::FlowView => muda::NativeIcon::FlowView, + NativeIcon::Folder => muda::NativeIcon::Folder, + NativeIcon::FolderBurnable => muda::NativeIcon::FolderBurnable, + NativeIcon::FolderSmart => muda::NativeIcon::FolderSmart, + NativeIcon::FollowLinkFreestanding => muda::NativeIcon::FollowLinkFreestanding, + NativeIcon::FontPanel => muda::NativeIcon::FontPanel, + NativeIcon::GoLeft => muda::NativeIcon::GoLeft, + NativeIcon::GoRight => muda::NativeIcon::GoRight, + NativeIcon::Home => muda::NativeIcon::Home, + NativeIcon::IChatTheater => muda::NativeIcon::IChatTheater, + NativeIcon::IconView => muda::NativeIcon::IconView, + NativeIcon::Info => muda::NativeIcon::Info, + NativeIcon::InvalidDataFreestanding => muda::NativeIcon::InvalidDataFreestanding, + NativeIcon::LeftFacingTriangle => muda::NativeIcon::LeftFacingTriangle, + NativeIcon::ListView => muda::NativeIcon::ListView, + NativeIcon::LockLocked => muda::NativeIcon::LockLocked, + NativeIcon::LockUnlocked => muda::NativeIcon::LockUnlocked, + NativeIcon::MenuMixedState => muda::NativeIcon::MenuMixedState, + NativeIcon::MenuOnState => muda::NativeIcon::MenuOnState, + NativeIcon::MobileMe => muda::NativeIcon::MobileMe, + NativeIcon::MultipleDocuments => muda::NativeIcon::MultipleDocuments, + NativeIcon::Network => muda::NativeIcon::Network, + NativeIcon::Path => muda::NativeIcon::Path, + NativeIcon::PreferencesGeneral => muda::NativeIcon::PreferencesGeneral, + NativeIcon::QuickLook => muda::NativeIcon::QuickLook, + NativeIcon::RefreshFreestanding => muda::NativeIcon::RefreshFreestanding, + NativeIcon::Refresh => muda::NativeIcon::Refresh, + NativeIcon::Remove => muda::NativeIcon::Remove, + NativeIcon::RevealFreestanding => muda::NativeIcon::RevealFreestanding, + NativeIcon::RightFacingTriangle => muda::NativeIcon::RightFacingTriangle, + NativeIcon::Share => muda::NativeIcon::Share, + NativeIcon::Slideshow => muda::NativeIcon::Slideshow, + NativeIcon::SmartBadge => muda::NativeIcon::SmartBadge, + NativeIcon::StatusAvailable => muda::NativeIcon::StatusAvailable, + NativeIcon::StatusNone => muda::NativeIcon::StatusNone, + NativeIcon::StatusPartiallyAvailable => muda::NativeIcon::StatusPartiallyAvailable, + NativeIcon::StatusUnavailable => muda::NativeIcon::StatusUnavailable, + NativeIcon::StopProgressFreestanding => muda::NativeIcon::StopProgressFreestanding, + NativeIcon::StopProgress => muda::NativeIcon::StopProgress, + NativeIcon::TrashEmpty => muda::NativeIcon::TrashEmpty, + NativeIcon::TrashFull => muda::NativeIcon::TrashFull, + NativeIcon::User => muda::NativeIcon::User, + NativeIcon::UserAccounts => muda::NativeIcon::UserAccounts, + NativeIcon::UserGroup => muda::NativeIcon::UserGroup, + NativeIcon::UserGuest => muda::NativeIcon::UserGuest, + } + } +} + +/// An enumeration of all menu item kinds that could be added to +/// a [`Menu`] or [`Submenu`] +pub enum MenuItemKind { + /// Normal menu item + MenuItem(MenuItem), + /// Submenu menu item + Submenu(Submenu), + /// Predefined menu item + Predefined(PredefinedMenuItem), + /// Check menu item + Check(CheckMenuItem), + /// Icon menu item + Icon(IconMenuItem), +} + +impl MenuItemKind { + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + match self { + MenuItemKind::MenuItem(i) => i.id(), + MenuItemKind::Submenu(i) => i.id(), + MenuItemKind::Predefined(i) => i.id(), + MenuItemKind::Check(i) => i.id(), + MenuItemKind::Icon(i) => i.id(), + } + } + + pub(crate) fn inner(&self) -> &dyn IsMenuItem { + match self { + MenuItemKind::MenuItem(i) => i, + MenuItemKind::Submenu(i) => i, + MenuItemKind::Predefined(i) => i, + MenuItemKind::Check(i) => i, + MenuItemKind::Icon(i) => i, + } + } + + /// Casts this item to a [`MenuItem`], and returns `None` if it wasn't. + pub fn as_menuitem(&self) -> Option<&MenuItem> { + match self { + MenuItemKind::MenuItem(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`MenuItem`], and panics if it wasn't. + pub fn as_menuitem_unchecked(&self) -> &MenuItem { + match self { + MenuItemKind::MenuItem(i) => i, + _ => panic!("Not a MenuItem"), + } + } + + /// Casts this item to a [`Submenu`], and returns `None` if it wasn't. + pub fn as_submenu(&self) -> Option<&Submenu> { + match self { + MenuItemKind::Submenu(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`Submenu`], and panics if it wasn't. + pub fn as_submenu_unchecked(&self) -> &Submenu { + match self { + MenuItemKind::Submenu(i) => i, + _ => panic!("Not a Submenu"), + } + } + + /// Casts this item to a [`PredefinedMenuItem`], and returns `None` if it wasn't. + pub fn as_predefined_menuitem(&self) -> Option<&PredefinedMenuItem> { + match self { + MenuItemKind::Predefined(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`PredefinedMenuItem`], and panics if it wasn't. + pub fn as_predefined_menuitem_unchecked(&self) -> &PredefinedMenuItem { + match self { + MenuItemKind::Predefined(i) => i, + _ => panic!("Not a PredefinedMenuItem"), + } + } + + /// Casts this item to a [`CheckMenuItem`], and returns `None` if it wasn't. + pub fn as_check_menuitem(&self) -> Option<&CheckMenuItem> { + match self { + MenuItemKind::Check(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`CheckMenuItem`], and panics if it wasn't. + pub fn as_check_menuitem_unchecked(&self) -> &CheckMenuItem { + match self { + MenuItemKind::Check(i) => i, + _ => panic!("Not a CheckMenuItem"), + } + } + + /// Casts this item to a [`IconMenuItem`], and returns `None` if it wasn't. + pub fn as_icon_menuitem(&self) -> Option<&IconMenuItem> { + match self { + MenuItemKind::Icon(i) => Some(i), + _ => None, + } + } + + /// Casts this item to a [`IconMenuItem`], and panics if it wasn't. + pub fn as_icon_menuitem_unchecked(&self) -> &IconMenuItem { + match self { + MenuItemKind::Icon(i) => i, + _ => panic!("Not an IconMenuItem"), + } + } +} + +impl Clone for MenuItemKind { + fn clone(&self) -> Self { + match self { + Self::MenuItem(i) => Self::MenuItem(i.clone()), + Self::Submenu(i) => Self::Submenu(i.clone()), + Self::Predefined(i) => Self::Predefined(i.clone()), + Self::Check(i) => Self::Check(i.clone()), + Self::Icon(i) => Self::Icon(i.clone()), + } + } +} + +impl sealed::IsMenuItemBase for MenuItemKind { + fn inner(&self) -> &dyn muda::IsMenuItem { + self.inner().inner() + } +} + +impl IsMenuItem for MenuItemKind { + fn kind(&self) -> MenuItemKind { + self.clone() + } + + fn id(&self) -> &MenuId { + self.id() + } +} + +/// A trait that defines a generic item in a menu, which may be one of [`MenuItemKind`] +/// +/// # Safety +/// +/// This trait is ONLY meant to be implemented internally by the crate. +pub trait IsMenuItem: sealed::IsMenuItemBase { + /// Returns the kind of this menu item. + fn kind(&self) -> MenuItemKind; + + /// Returns a unique identifier associated with this menu. + fn id(&self) -> &MenuId; +} + +/// A helper trait with methods to help creating a context menu. +/// +/// # Safety +/// +/// This trait is ONLY meant to be implemented internally by the crate. +pub trait ContextMenu: sealed::ContextMenuBase + Send + Sync { + /// Popup this menu as a context menu on the specified window at the cursor position. + fn popup(&self, window: crate::Window) -> crate::Result<()>; + + /// Popup this menu as a context menu on the specified window at the specified position. + /// + /// The position is relative to the window's top-left corner. + fn popup_at>( + &self, + window: crate::Window, + position: P, + ) -> crate::Result<()>; +} + +pub(crate) mod sealed { + + pub trait IsMenuItemBase { + fn inner(&self) -> &dyn muda::IsMenuItem; + } + + pub trait ContextMenuBase { + fn inner(&self) -> &dyn muda::ContextMenu; + fn inner_owned(&self) -> Box; + fn popup_inner>( + &self, + window: crate::Window, + position: Option

, + ) -> crate::Result<()>; + } +} + +impl TryFrom for muda::Icon { + type Error = crate::Error; + + fn try_from(value: crate::Icon) -> Result { + let value: crate::runtime::Icon = value.try_into()?; + muda::Icon::from_rgba(value.rgba, value.width, value.height).map_err(Into::into) + } +} + +pub(crate) fn into_logical_position( + p: crate::LogicalPosition

, +) -> muda::LogicalPosition

{ + muda::LogicalPosition { x: p.x, y: p.y } +} + +pub(crate) fn into_physical_position( + p: crate::PhysicalPosition

, +) -> muda::PhysicalPosition

{ + muda::PhysicalPosition { x: p.x, y: p.y } +} + +pub(crate) fn into_position(p: crate::Position) -> muda::Position { + match p { + crate::Position::Physical(p) => muda::Position::Physical(into_physical_position(p)), + crate::Position::Logical(p) => muda::Position::Logical(into_logical_position(p)), + } +} diff --git a/core/tauri/src/menu/normal.rs b/core/tauri/src/menu/normal.rs new file mode 100644 index 00000000000..a7eeb3e6b71 --- /dev/null +++ b/core/tauri/src/menu/normal.rs @@ -0,0 +1,134 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{menu::MenuId, run_main_thread, AppHandle, Manager, Runtime}; + +/// A menu item inside a [`Menu`] or [`Submenu`] and contains only text. +/// +/// [`Menu`]: super::Menu +/// [`Submenu`]: super::Submenu +pub struct MenuItem { + pub(crate) id: MenuId, + pub(crate) inner: muda::MenuItem, + pub(crate) app_handle: AppHandle, +} + +impl Clone for MenuItem { + fn clone(&self) -> Self { + Self { + id: self.id.clone(), + inner: self.inner.clone(), + app_handle: self.app_handle.clone(), + } + } +} + +/// # Safety +/// +/// We make sure it always runs on the main thread. +unsafe impl Sync for MenuItem {} +unsafe impl Send for MenuItem {} + +impl super::sealed::IsMenuItemBase for MenuItem { + fn inner(&self) -> &dyn muda::IsMenuItem { + &self.inner + } +} + +impl super::IsMenuItem for MenuItem { + fn kind(&self) -> super::MenuItemKind { + super::MenuItemKind::MenuItem(self.clone()) + } + + fn id(&self) -> &MenuId { + &self.id + } +} + +impl MenuItem { + /// Create a new menu item. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn new, S: AsRef>( + manager: &M, + text: S, + enabled: bool, + acccelerator: Option, + ) -> Self { + let item = muda::MenuItem::new( + text, + enabled, + acccelerator.and_then(|s| s.as_ref().parse().ok()), + ); + Self { + id: item.id().clone(), + inner: item, + app_handle: manager.app_handle().clone(), + } + } + + /// Create a new menu item with the specified id. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn with_id, I: Into, S: AsRef>( + manager: &M, + id: I, + text: S, + enabled: bool, + acccelerator: Option, + ) -> Self { + let item = muda::MenuItem::with_id( + id, + text, + enabled, + acccelerator.and_then(|s| s.as_ref().parse().ok()), + ); + Self { + id: item.id().clone(), + inner: item, + app_handle: manager.app_handle().clone(), + } + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.app_handle + } + + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + &self.id + } + + /// Get the text for this menu item. + pub fn text(&self) -> crate::Result { + run_main_thread!(self, |self_: Self| self_.inner.text()) + } + + /// Set the text for this menu item. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_main_thread!(self, |self_: Self| self_.inner.set_text(text)) + } + + /// Get whether this menu item is enabled or not. + pub fn is_enabled(&self) -> crate::Result { + run_main_thread!(self, |self_: Self| self_.inner.is_enabled()) + } + + /// Enable or disable this menu item. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + run_main_thread!(self, |self_: Self| self_.inner.set_enabled(enabled)) + } + + /// Set this menu item accelerator. + pub fn set_accelerator>(&self, acccelerator: Option) -> crate::Result<()> { + let accel = acccelerator.and_then(|s| s.as_ref().parse().ok()); + run_main_thread!(self, |self_: Self| self_.inner.set_accelerator(accel))?.map_err(Into::into) + } +} diff --git a/core/tauri/src/menu/predefined.rs b/core/tauri/src/menu/predefined.rs new file mode 100644 index 00000000000..44c6aff01d6 --- /dev/null +++ b/core/tauri/src/menu/predefined.rs @@ -0,0 +1,287 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::AboutMetadata; +use crate::{menu::MenuId, run_main_thread, AppHandle, Manager, Runtime}; + +/// A predefined (native) menu item which has a predfined behavior by the OS or by this crate. +pub struct PredefinedMenuItem { + pub(crate) id: MenuId, + pub(crate) inner: muda::PredefinedMenuItem, + pub(crate) app_handle: AppHandle, +} + +impl Clone for PredefinedMenuItem { + fn clone(&self) -> Self { + Self { + id: self.id.clone(), + inner: self.inner.clone(), + app_handle: self.app_handle.clone(), + } + } +} + +/// # Safety +/// +/// We make sure it always runs on the main thread. +unsafe impl Sync for PredefinedMenuItem {} +unsafe impl Send for PredefinedMenuItem {} + +impl super::sealed::IsMenuItemBase for PredefinedMenuItem { + fn inner(&self) -> &dyn muda::IsMenuItem { + &self.inner + } +} + +impl super::IsMenuItem for PredefinedMenuItem { + fn kind(&self) -> super::MenuItemKind { + super::MenuItemKind::Predefined(self.clone()) + } + + fn id(&self) -> &MenuId { + self.id() + } +} + +impl PredefinedMenuItem { + /// Separator menu item + pub fn separator>(manager: &M) -> Self { + let inner = muda::PredefinedMenuItem::separator(); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// Copy menu item + pub fn copy>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::copy(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// Cut menu item + pub fn cut>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::cut(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// Paste menu item + pub fn paste>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::paste(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// SelectAll menu item + pub fn select_all>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::select_all(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// Undo menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn undo>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::undo(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + /// Redo menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn redo>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::redo(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// Minimize window menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn minimize>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::minimize(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// Maximize window menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn maximize>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::maximize(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// Fullscreen menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn fullscreen>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::fullscreen(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// Hide window menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::hide(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// Hide other windows menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn hide_others>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::hide_others(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// Show all app windows menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn show_all>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::show_all(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// Close window menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn close_window>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::show_all(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// Quit app menu item + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn quit>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::quit(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// About app menu item + pub fn about>( + manager: &M, + text: Option<&str>, + metadata: Option, + ) -> Self { + let inner = muda::PredefinedMenuItem::about(text, metadata.map(Into::into)); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// Services menu item + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported. + pub fn services>(manager: &M, text: Option<&str>) -> Self { + let inner = muda::PredefinedMenuItem::services(text); + Self { + id: inner.id().clone(), + inner, + app_handle: manager.app_handle().clone(), + } + } + + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> &MenuId { + &self.id + } + + /// Get the text for this menu item. + pub fn text(&self) -> crate::Result { + run_main_thread!(self, |self_: Self| self_.inner.text()) + } + + /// Set the text for this menu item. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_main_thread!(self, |self_: Self| self_.inner.set_text(text)) + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.app_handle + } +} diff --git a/core/tauri/src/menu/submenu.rs b/core/tauri/src/menu/submenu.rs new file mode 100644 index 00000000000..905f04830c9 --- /dev/null +++ b/core/tauri/src/menu/submenu.rs @@ -0,0 +1,328 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{sealed::ContextMenuBase, IsMenuItem, MenuItemKind}; +use crate::{run_main_thread, AppHandle, Manager, Position, Runtime, Window}; +use muda::{ContextMenu, MenuId}; + +/// A type that is a submenu inside a [`Menu`] or [`Submenu`] +/// +/// [`Menu`]: super::Menu +/// [`Submenu`]: super::Submenu +pub struct Submenu { + pub(crate) id: MenuId, + pub(crate) inner: muda::Submenu, + pub(crate) app_handle: AppHandle, +} + +/// # Safety +/// +/// We make sure it always runs on the main thread. +unsafe impl Sync for Submenu {} +unsafe impl Send for Submenu {} + +impl Clone for Submenu { + fn clone(&self) -> Self { + Self { + id: self.id.clone(), + inner: self.inner.clone(), + app_handle: self.app_handle.clone(), + } + } +} + +impl super::sealed::IsMenuItemBase for Submenu { + fn inner(&self) -> &dyn muda::IsMenuItem { + &self.inner + } +} + +impl super::IsMenuItem for Submenu { + fn kind(&self) -> super::MenuItemKind { + super::MenuItemKind::Submenu(self.clone()) + } + + fn id(&self) -> &MenuId { + &self.id + } +} + +impl super::ContextMenu for Submenu { + fn popup(&self, window: Window) -> crate::Result<()> { + self.popup_inner(window, None::) + } + + fn popup_at>( + &self, + window: Window, + position: P, + ) -> crate::Result<()> { + self.popup_inner(window, Some(position)) + } +} + +impl ContextMenuBase for Submenu { + fn popup_inner>( + &self, + window: crate::Window, + position: Option

, + ) -> crate::Result<()> { + let position = position.map(Into::into).map(super::into_position); + run_main_thread!(self, move |self_: Self| { + #[cfg(target_os = "macos")] + if let Ok(view) = window.ns_view() { + self_ + .inner() + .show_context_menu_for_nsview(view as _, position); + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(w) = window.gtk_window() { + self_.inner().show_context_menu_for_gtk_window(&w, position); + } + + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + self_.inner().show_context_menu_for_hwnd(hwnd.0, position) + } + }) + } + + fn inner(&self) -> &dyn muda::ContextMenu { + &self.inner + } + + fn inner_owned(&self) -> Box { + Box::new(self.clone().inner) + } +} + +impl Submenu { + /// Creates a new submenu. + pub fn new, S: AsRef>(manager: &M, text: S, enabled: bool) -> Self { + let submenu = muda::Submenu::new(text, enabled); + Self { + id: submenu.id().clone(), + inner: submenu, + app_handle: manager.app_handle().clone(), + } + } + + /// Creates a new submenu with the specified id. + pub fn with_id, I: Into, S: AsRef>( + manager: &M, + id: I, + text: S, + enabled: bool, + ) -> Self { + let menu = muda::Submenu::with_id(id, text, enabled); + Self { + id: menu.id().clone(), + inner: menu, + app_handle: manager.app_handle().clone(), + } + } + + /// Creates a new menu with given `items`. It calls [`Submenu::new`] and [`Submenu::append_items`] internally. + pub fn with_items, S: AsRef>( + manager: &M, + text: S, + enabled: bool, + items: &[&dyn IsMenuItem], + ) -> crate::Result { + let menu = Self::new(manager, text, enabled); + menu.append_items(items)?; + Ok(menu) + } + + /// Creates a new menu with the specified id and given `items`. + /// It calls [`Submenu::new`] and [`Submenu::append_items`] internally. + pub fn with_id_and_items, I: Into, S: AsRef>( + manager: &M, + id: I, + text: S, + enabled: bool, + items: &[&dyn IsMenuItem], + ) -> crate::Result { + let menu = Self::with_id(manager, id, text, enabled); + menu.append_items(items)?; + Ok(menu) + } + + pub(crate) fn inner(&self) -> &muda::Submenu { + &self.inner + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.app_handle + } + + /// Returns a unique identifier associated with this submenu. + pub fn id(&self) -> &MenuId { + &self.id + } + + /// Add a menu item to the end of this submenu. + pub fn append(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_main_thread!(self, |self_: Self| self_.inner.append(kind.inner().inner()))? + .map_err(Into::into) + } + + /// Add menu items to the end of this submenu. It calls [`Submenu::append`] in a loop internally. + pub fn append_items(&self, items: &[&dyn IsMenuItem]) -> crate::Result<()> { + for item in items { + self.append(*item)? + } + + Ok(()) + } + + /// Add a menu item to the beginning of this submenu. + pub fn prepend(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_main_thread!(self, |self_: Self| { + self_.inner.prepend(kind.inner().inner()) + })? + .map_err(Into::into) + } + + /// Add menu items to the beginning of this submenu. It calls [`Submenu::insert_items`] with position of `0` internally. + pub fn prepend_items(&self, items: &[&dyn IsMenuItem]) -> crate::Result<()> { + self.insert_items(items, 0) + } + + /// Insert a menu item at the specified `postion` in this submenu. + pub fn insert(&self, item: &dyn IsMenuItem, position: usize) -> crate::Result<()> { + let kind = item.kind(); + run_main_thread!(self, |self_: Self| { + self_.inner.insert(kind.inner().inner(), position) + })? + .map_err(Into::into) + } + + /// Insert menu items at the specified `postion` in this submenu. + pub fn insert_items(&self, items: &[&dyn IsMenuItem], position: usize) -> crate::Result<()> { + for (i, item) in items.iter().enumerate() { + self.insert(*item, position + i)? + } + + Ok(()) + } + + /// Remove a menu item from this submenu. + pub fn remove(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + let kind = item.kind(); + run_main_thread!(self, |self_: Self| self_.inner.remove(kind.inner().inner()))? + .map_err(Into::into) + } + + /// Returns a list of menu items that has been added to this submenu. + pub fn items(&self) -> crate::Result>> { + let handle = self.app_handle.clone(); + run_main_thread!(self, |self_: Self| { + self_ + .inner + .items() + .into_iter() + .map(|i| match i { + muda::MenuItemKind::MenuItem(i) => super::MenuItemKind::MenuItem(super::MenuItem { + id: i.id().clone(), + inner: i, + app_handle: handle.clone(), + }), + muda::MenuItemKind::Submenu(i) => super::MenuItemKind::Submenu(super::Submenu { + id: i.id().clone(), + inner: i, + app_handle: handle.clone(), + }), + muda::MenuItemKind::Predefined(i) => { + super::MenuItemKind::Predefined(super::PredefinedMenuItem { + id: i.id().clone(), + inner: i, + app_handle: handle.clone(), + }) + } + muda::MenuItemKind::Check(i) => super::MenuItemKind::Check(super::CheckMenuItem { + id: i.id().clone(), + inner: i, + app_handle: handle.clone(), + }), + muda::MenuItemKind::Icon(i) => super::MenuItemKind::Icon(super::IconMenuItem { + id: i.id().clone(), + inner: i, + app_handle: handle.clone(), + }), + }) + .collect::>() + }) + } + + /// Get the text for this submenu. + pub fn text(&self) -> crate::Result { + run_main_thread!(self, |self_: Self| self_.inner.text()) + } + + /// Set the text for this submenu. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this submenu. To display a `&` without assigning a mnemenonic, use `&&`. + pub fn set_text>(&self, text: S) -> crate::Result<()> { + let text = text.as_ref().to_string(); + run_main_thread!(self, |self_: Self| self_.inner.set_text(text)) + } + + /// Get whether this submenu is enabled or not. + pub fn is_enabled(&self) -> crate::Result { + run_main_thread!(self, |self_: Self| self_.inner.is_enabled()) + } + + /// Enable or disable this submenu. + pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { + run_main_thread!(self, |self_: Self| self_.inner.set_enabled(enabled)) + } + + /// Set this submenu as the Window menu for the application on macOS. + /// + /// This will cause macOS to automatically add window-switching items and + /// certain other items to the menu. + #[cfg(target_os = "macos")] + pub fn set_as_windows_menu_for_nsapp(&self) -> crate::Result<()> { + run_main_thread!(self, |self_: Self| self_ + .inner + .set_as_windows_menu_for_nsapp())?; + Ok(()) + } + + /// Set this submenu as the Help menu for the application on macOS. + /// + /// This will cause macOS to automatically add a search box to the menu. + /// + /// If no menu is set as the Help menu, macOS will automatically use any menu + /// which has a title matching the localized word "Help". + pub fn set_as_help_menu_for_nsapp(&self) -> crate::Result<()> { + #[cfg(target_os = "macos")] + run_main_thread!(self, |self_: Self| self_.inner.set_as_help_menu_for_nsapp())?; + Ok(()) + } + + /// Retrieves the menu item matching the given identifier. + pub fn get<'a, I>(&self, id: &'a I) -> Option> + where + I: ?Sized, + MenuId: PartialEq<&'a I>, + { + self + .items() + .unwrap_or_default() + .into_iter() + .find(|i| i.id() == &id) + } +} diff --git a/core/tauri/src/path/android.rs b/core/tauri/src/path/android.rs new file mode 100644 index 00000000000..730e9f10ba6 --- /dev/null +++ b/core/tauri/src/path/android.rs @@ -0,0 +1,120 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::Result; +use crate::{plugin::PluginHandle, Runtime}; +use std::path::PathBuf; + +/// A helper class to access the mobile path APIs. +pub struct PathResolver(pub(crate) PluginHandle); + +#[derive(serde::Deserialize)] +struct PathResponse { + path: PathBuf, +} + +impl PathResolver { + fn call_resolve(&self, dir: &str) -> Result { + self + .0 + .run_mobile_plugin::(dir, ()) + .map(|r| r.path) + .map_err(Into::into) + } + + /// Returns the path to the user's audio directory. + pub fn audio_dir(&self) -> Result { + self.call_resolve("getAudioDir") + } + + /// Returns the path to the user's cache directory. + pub fn cache_dir(&self) -> Result { + self.call_resolve("getExternalCacheDir") + } + + /// Returns the path to the user's config directory. + pub fn config_dir(&self) -> Result { + self.call_resolve("getConfigDir") + } + + /// Returns the path to the user's data directory. + pub fn data_dir(&self) -> Result { + self.call_resolve("getDataDir") + } + + /// Returns the path to the user's local data directory. + pub fn local_data_dir(&self) -> Result { + self.call_resolve("getDataDir") + } + + /// Returns the path to the user's document directory. + pub fn document_dir(&self) -> Result { + self.call_resolve("getDocumentDir") + } + + /// Returns the path to the user's download directory. + pub fn download_dir(&self) -> Result { + self.call_resolve("getDownloadDir") + } + + /// Returns the path to the user's picture directory. + pub fn picture_dir(&self) -> Result { + self.call_resolve("getPictureDir") + } + + /// Returns the path to the user's public directory. + pub fn public_dir(&self) -> Result { + self.call_resolve("getPublicDir") + } + + /// Returns the path to the user's video dir + pub fn video_dir(&self) -> Result { + self.call_resolve("getVideoDir") + } + + /// Returns the path to the resource directory of this app. + pub fn resource_dir(&self) -> Result { + self.call_resolve("getResourcesDir") + } + + /// Returns the path to the suggested directory for your app's config files. + /// + /// Resolves to [`config_dir`]`/${bundle_identifier}`. + pub fn app_config_dir(&self) -> Result { + self.call_resolve("getConfigDir") + } + + /// Returns the path to the suggested directory for your app's data files. + /// + /// Resolves to [`data_dir`]`/${bundle_identifier}`. + pub fn app_data_dir(&self) -> Result { + self.call_resolve("getDataDir") + } + + /// Returns the path to the suggested directory for your app's local data files. + /// + /// Resolves to [`local_data_dir`]`/${bundle_identifier}`. + pub fn app_local_data_dir(&self) -> Result { + self.call_resolve("getDataDir") + } + + /// Returns the path to the suggested directory for your app's cache files. + /// + /// Resolves to [`cache_dir`]`/${bundle_identifier}`. + pub fn app_cache_dir(&self) -> Result { + self.call_resolve("getCacheDir") + } + + /// Returns the path to the suggested directory for your app's log files. + pub fn app_log_dir(&self) -> Result { + self + .call_resolve("getConfigDir") + .map(|dir| dir.join("logs")) + } + + /// A temporary directory. Resolves to [`std::env::temp_dir`]. + pub fn temp_dir(&self) -> Result { + Ok(std::env::temp_dir()) + } +} diff --git a/core/tauri/src/path/commands.rs b/core/tauri/src/path/commands.rs new file mode 100644 index 00000000000..8a343c5fec5 --- /dev/null +++ b/core/tauri/src/path/commands.rs @@ -0,0 +1,193 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR}; + +use super::{BaseDirectory, Error, PathResolver, Result}; +use crate::{command, AppHandle, Runtime, State}; + +/// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util. +/// https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106 +fn normalize_path(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + } + } + ret +} + +/// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util but +/// slightly modified to not resolve absolute paths. +/// https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106 +fn normalize_path_no_absolute(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + // Using PathBuf::push here will replace the whole path if an absolute path is encountered + // which is not the intended behavior, so instead of that, convert the current resolved path + // to a string and do simple string concatenation with the current component then convert it + // back to a PathBuf + let mut p = ret.to_string_lossy().to_string(); + // Only add a separator if it doesn't have one already or if current normalized path is empty, + // this ensures it won't have an unwanted leading separator + if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') { + p.push(MAIN_SEPARATOR); + } + if let Some(c) = c.to_str() { + p.push_str(c); + } + ret = PathBuf::from(p); + } + } + } + ret +} + +#[command(root = "crate")] +pub fn resolve_directory( + _app: AppHandle, + resolver: State<'_, PathResolver>, + directory: BaseDirectory, + path: Option, +) -> Result { + super::resolve_path(&resolver, directory, path) +} + +#[command(root = "crate")] +pub fn resolve(paths: Vec) -> Result { + // Start with current directory then start adding paths from the vector one by one using `PathBuf.push()` which + // will ensure that if an absolute path is encountered in the iteration, it will be used as the current full path. + // + // examples: + // 1. `vec!["."]` or `vec![]` will be equal to `std::env::current_dir()` + // 2. `vec!["/foo/bar", "/tmp/file", "baz"]` will be equal to `PathBuf::from("/tmp/file/baz")` + let mut path = std::env::current_dir().map_err(Error::CurrentDir)?; + for p in paths { + path.push(p); + } + Ok(normalize_path(&path)) +} + +#[command(root = "crate")] +pub fn normalize(path: String) -> String { + let mut p = normalize_path_no_absolute(Path::new(&path)) + .to_string_lossy() + .to_string(); + + // Node.js behavior is to return `".."` for `normalize("..")` + // and `"."` for `normalize("")` or `normalize(".")` + if p.is_empty() && path == ".." { + "..".into() + } else if p.is_empty() && path == "." { + ".".into() + } else { + // Add a trailing separator if the path passed to this functions had a trailing separator. That's how Node.js behaves. + if (path.ends_with('/') || path.ends_with('\\')) && (!p.ends_with('/') || !p.ends_with('\\')) { + p.push(MAIN_SEPARATOR); + } + p + } +} + +#[command(root = "crate")] +pub fn join(mut paths: Vec) -> String { + let path = PathBuf::from( + paths + .iter_mut() + .map(|p| { + // Add a `MAIN_SEPARATOR` if it doesn't already have one. + // Doing this to ensure that the vector elements are separated in + // the resulting string so path.components() can work correctly when called + // in `normalize_path_no_absolute()` later on. + if !p.ends_with('/') && !p.ends_with('\\') { + p.push(MAIN_SEPARATOR); + } + p.to_string() + }) + .collect::(), + ); + + let p = normalize_path_no_absolute(&path) + .to_string_lossy() + .to_string(); + if p.is_empty() { + ".".into() + } else { + p + } +} + +#[command(root = "crate")] +pub fn dirname(path: String) -> Result { + match Path::new(&path).parent() { + Some(p) => Ok(p.to_path_buf()), + None => Err(Error::NoParent), + } +} + +#[command(root = "crate")] +pub fn extname(path: String) -> Result { + match Path::new(&path) + .extension() + .and_then(std::ffi::OsStr::to_str) + { + Some(p) => Ok(p.to_string()), + None => Err(Error::NoExtension), + } +} + +#[command(root = "crate")] +pub fn basename(path: String, ext: Option) -> Result { + match Path::new(&path) + .file_name() + .and_then(std::ffi::OsStr::to_str) + { + Some(p) => Ok(if let Some(ext) = ext { + p.replace(ext.as_str(), "") + } else { + p.to_string() + }), + None => Err(Error::NoBasename), + } +} + +#[command(root = "crate")] +pub fn is_absolute(path: String) -> bool { + Path::new(&path).is_absolute() +} diff --git a/core/tauri/src/path/desktop.rs b/core/tauri/src/path/desktop.rs new file mode 100644 index 00000000000..5522c0e1ff3 --- /dev/null +++ b/core/tauri/src/path/desktop.rs @@ -0,0 +1,262 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{Error, Result}; +use crate::{AppHandle, Manager, Runtime}; +use std::path::PathBuf; + +/// A helper class to access the mobile camera APIs. +pub struct PathResolver(pub(crate) AppHandle); + +impl PathResolver { + /// Returns the path to the user's audio directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_MUSIC_DIR`. + /// - **macOS:** Resolves to `$HOME/Music`. + /// - **Windows:** Resolves to `{FOLDERID_Music}`. + pub fn audio_dir(&self) -> Result { + dirs_next::audio_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's cache directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_CACHE_HOME` or `$HOME/.cache`. + /// - **macOS:** Resolves to `$HOME/Library/Caches`. + /// - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. + pub fn cache_dir(&self) -> Result { + dirs_next::cache_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's config directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_CONFIG_HOME` or `$HOME/.config`. + /// - **macOS:** Resolves to `$HOME/Library/Application Support`. + /// - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. + pub fn config_dir(&self) -> Result { + dirs_next::config_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's data directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. + /// - **macOS:** Resolves to `$HOME/Library/Application Support`. + /// - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`. + pub fn data_dir(&self) -> Result { + dirs_next::data_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's local data directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`. + /// - **macOS:** Resolves to `$HOME/Library/Application Support`. + /// - **Windows:** Resolves to `{FOLDERID_LocalAppData}`. + pub fn local_data_dir(&self) -> Result { + dirs_next::data_local_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's desktop directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DESKTOP_DIR`. + /// - **macOS:** Resolves to `$HOME/Desktop`. + /// - **Windows:** Resolves to `{FOLDERID_Desktop}`. + pub fn desktop_dir(&self) -> Result { + dirs_next::desktop_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's document directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOCUMENTS_DIR`. + /// - **macOS:** Resolves to `$HOME/Documents`. + /// - **Windows:** Resolves to `{FOLDERID_Documents}`. + pub fn document_dir(&self) -> Result { + dirs_next::document_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's download directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOWNLOAD_DIR`. + /// - **macOS:** Resolves to `$HOME/Downloads`. + /// - **Windows:** Resolves to `{FOLDERID_Downloads}`. + pub fn download_dir(&self) -> Result { + dirs_next::download_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's executable directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_BIN_HOME/../bin` or `$XDG_DATA_HOME/../bin` or `$HOME/.local/bin`. + /// - **macOS:** Not supported. + /// - **Windows:** Not supported. + pub fn executable_dir(&self) -> Result { + dirs_next::executable_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's font directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_DATA_HOME/fonts` or `$HOME/.local/share/fonts`. + /// - **macOS:** Resolves to `$HOME/Library/Fonts`. + /// - **Windows:** Not supported. + pub fn font_dir(&self) -> Result { + dirs_next::font_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's home directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$HOME`. + /// - **macOS:** Resolves to `$HOME`. + /// - **Windows:** Resolves to `{FOLDERID_Profile}`. + pub fn home_dir(&self) -> Result { + dirs_next::home_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's picture directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PICTURES_DIR`. + /// - **macOS:** Resolves to `$HOME/Pictures`. + /// - **Windows:** Resolves to `{FOLDERID_Pictures}`. + pub fn picture_dir(&self) -> Result { + dirs_next::picture_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's public directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PUBLICSHARE_DIR`. + /// - **macOS:** Resolves to `$HOME/Public`. + /// - **Windows:** Resolves to `{FOLDERID_Public}`. + pub fn public_dir(&self) -> Result { + dirs_next::public_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's runtime directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to `$XDG_RUNTIME_DIR`. + /// - **macOS:** Not supported. + /// - **Windows:** Not supported. + pub fn runtime_dir(&self) -> Result { + dirs_next::runtime_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's template directory. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_TEMPLATES_DIR`. + /// - **macOS:** Not supported. + /// - **Windows:** Resolves to `{FOLDERID_Templates}`. + pub fn template_dir(&self) -> Result { + dirs_next::template_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the user's video dir + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_VIDEOS_DIR`. + /// - **macOS:** Resolves to `$HOME/Movies`. + /// - **Windows:** Resolves to `{FOLDERID_Videos}`. + pub fn video_dir(&self) -> Result { + dirs_next::video_dir().ok_or(Error::UnknownPath) + } + + /// Returns the path to the resource directory of this app. + pub fn resource_dir(&self) -> Result { + crate::utils::platform::resource_dir(self.0.package_info(), &self.0.env()) + .map_err(|_| Error::UnknownPath) + } + + /// Returns the path to the suggested directory for your app's config files. + /// + /// Resolves to [`config_dir`](self.config_dir)`/${bundle_identifier}`. + pub fn app_config_dir(&self) -> Result { + dirs_next::config_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join(&self.0.config().tauri.bundle.identifier)) + } + + /// Returns the path to the suggested directory for your app's data files. + /// + /// Resolves to [`data_dir`](self.data_dir)`/${bundle_identifier}`. + pub fn app_data_dir(&self) -> Result { + dirs_next::data_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join(&self.0.config().tauri.bundle.identifier)) + } + + /// Returns the path to the suggested directory for your app's local data files. + /// + /// Resolves to [`local_data_dir`](self.local_data_dir)`/${bundle_identifier}`. + pub fn app_local_data_dir(&self) -> Result { + dirs_next::data_local_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join(&self.0.config().tauri.bundle.identifier)) + } + + /// Returns the path to the suggested directory for your app's cache files. + /// + /// Resolves to [`cache_dir`](self.cache_dir)`/${bundle_identifier}`. + pub fn app_cache_dir(&self) -> Result { + dirs_next::cache_dir() + .ok_or(Error::UnknownPath) + .map(|dir| dir.join(&self.0.config().tauri.bundle.identifier)) + } + + /// Returns the path to the suggested directory for your app's log files. + /// + /// ## Platform-specific + /// + /// - **Linux:** Resolves to [`data_local_dir`](self.data_local_dir)`/${bundle_identifier}/logs`. + /// - **macOS:** Resolves to [`home_dir`](self.home_dir)`/Library/Logs/${bundle_identifier}` + /// - **Windows:** Resolves to [`data_local_dir`](self.data_local_dir)`/${bundle_identifier}/logs`. + pub fn app_log_dir(&self) -> Result { + #[cfg(target_os = "macos")] + let path = dirs_next::home_dir().ok_or(Error::UnknownPath).map(|dir| { + dir + .join("Library/Logs") + .join(&self.0.config().tauri.bundle.identifier) + }); + + #[cfg(not(target_os = "macos"))] + let path = dirs_next::data_local_dir() + .ok_or(Error::UnknownPath) + .map(|dir| { + dir + .join(&self.0.config().tauri.bundle.identifier) + .join("logs") + }); + + path + } + + /// A temporary directory. Resolves to [`std::env::temp_dir`]. + pub fn temp_dir(&self) -> Result { + Ok(std::env::temp_dir()) + } +} diff --git a/core/tauri/src/path/error.rs b/core/tauri/src/path/error.rs new file mode 100644 index 00000000000..d6a03646dfc --- /dev/null +++ b/core/tauri/src/path/error.rs @@ -0,0 +1,42 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{ser::Serializer, Serialize}; + +/// Path result. +pub type Result = std::result::Result; + +/// Path error. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Path does not have a parent. + #[error("path does not have a parent")] + NoParent, + /// Path does not have an extension. + #[error("path does not have an extension")] + NoExtension, + /// Path does not have a basename. + #[error("path does not have a basename")] + NoBasename, + /// Cannot resolve current directory. + #[error("failed to read current dir: {0}")] + CurrentDir(std::io::Error), + /// Unknown path. + #[cfg(not(target_os = "android"))] + #[error("unknown path")] + UnknownPath, + /// Failed to invoke mobile plugin. + #[cfg(target_os = "android")] + #[error(transparent)] + PluginInvoke(#[from] crate::plugin::mobile::PluginInvokeError), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/core/tauri/src/path/init.js b/core/tauri/src/path/init.js new file mode 100644 index 00000000000..4de1c2d1118 --- /dev/null +++ b/core/tauri/src/path/init.js @@ -0,0 +1,10 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +if (!('path' in window.__TAURI__)) { + window.__TAURI__.path = {} +} + +window.__TAURI__.path.__sep = __TEMPLATE_sep__ +window.__TAURI__.path.__delimiter = __TEMPLATE_delimiter__ diff --git a/core/tauri/src/path/mod.rs b/core/tauri/src/path/mod.rs new file mode 100644 index 00000000000..9c4b89ea336 --- /dev/null +++ b/core/tauri/src/path/mod.rs @@ -0,0 +1,398 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::{Component, Display, Path, PathBuf}; + +use crate::{ + plugin::{Builder, TauriPlugin}, + Manager, Runtime, +}; + +use serde::{de::Error as DeError, Deserialize, Deserializer}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use serialize_to_javascript::{default_template, DefaultTemplate, Template}; + +mod commands; +mod error; +pub use error::*; + +#[cfg(target_os = "android")] +mod android; +#[cfg(not(target_os = "android"))] +mod desktop; + +#[cfg(target_os = "android")] +pub use android::PathResolver; +#[cfg(not(target_os = "android"))] +pub use desktop::PathResolver; + +/// A wrapper for [`PathBuf`] that prevents path traversal. +#[derive(Clone, Debug)] +pub struct SafePathBuf(PathBuf); + +impl SafePathBuf { + /// Validates the path for directory traversal vulnerabilities and returns a new [`SafePathBuf`] instance if it is safe. + pub fn new(path: PathBuf) -> std::result::Result { + if path.components().any(|x| matches!(x, Component::ParentDir)) { + Err("cannot traverse directory, rewrite the path without the use of `../`") + } else { + Ok(Self(path)) + } + } + + /// Returns an object that implements [`std::fmt::Display`] for safely printing paths. + /// + /// See [`PathBuf#method.display`] for more information. + pub fn display(&self) -> Display<'_> { + self.0.display() + } +} + +impl AsRef for SafePathBuf { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl<'de> Deserialize<'de> for SafePathBuf { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let path = PathBuf::deserialize(deserializer)?; + SafePathBuf::new(path).map_err(DeError::custom) + } +} + +/// A base directory for a path. +/// +/// The base directory is the optional root of a file system operation. +/// If informed by the API call, all paths will be relative to the path of the given directory. +/// +/// For more information, check the [`dirs_next` documentation](https://docs.rs/dirs_next/). +#[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)] +#[repr(u16)] +#[non_exhaustive] +pub enum BaseDirectory { + /// The Audio directory. + Audio = 1, + /// The Cache directory. + Cache, + /// The Config directory. + Config, + /// The Data directory. + Data, + /// The LocalData directory. + LocalData, + /// The Document directory. + Document, + /// The Download directory. + Download, + /// The Picture directory. + Picture, + /// The Public directory. + Public, + /// The Video directory. + Video, + /// The Resource directory. + Resource, + /// A temporary directory. Resolves to [`std::env::temp_dir`]. + Temp, + /// The default app config directory. + /// Resolves to [`BaseDirectory::Config`]`/{bundle_identifier}`. + AppConfig, + /// The default app data directory. + /// Resolves to [`BaseDirectory::Data`]`/{bundle_identifier}`. + AppData, + /// The default app local data directory. + /// Resolves to [`BaseDirectory::LocalData`]`/{bundle_identifier}`. + AppLocalData, + /// The default app cache directory. + /// Resolves to [`BaseDirectory::Cache`]`/{bundle_identifier}`. + AppCache, + /// The default app log directory. + /// Resolves to [`BaseDirectory::Home`]`/Library/Logs/{bundle_identifier}` on macOS + /// and [`BaseDirectory::Config`]`/{bundle_identifier}/logs` on linux and Windows. + AppLog, + + /// The Desktop directory. + #[cfg(not(target_os = "android"))] + Desktop, + /// The Executable directory. + #[cfg(not(target_os = "android"))] + Executable, + /// The Font directory. + #[cfg(not(target_os = "android"))] + Font, + /// The Home directory. + #[cfg(not(target_os = "android"))] + Home, + /// The Runtime directory. + #[cfg(not(target_os = "android"))] + Runtime, + /// The Template directory. + #[cfg(not(target_os = "android"))] + Template, +} + +impl BaseDirectory { + /// Gets the variable that represents this [`BaseDirectory`] for string paths. + pub fn variable(self) -> &'static str { + match self { + Self::Audio => "$AUDIO", + Self::Cache => "$CACHE", + Self::Config => "$CONFIG", + Self::Data => "$DATA", + Self::LocalData => "$LOCALDATA", + Self::Document => "$DOCUMENT", + Self::Download => "$DOWNLOAD", + Self::Picture => "$PICTURE", + Self::Public => "$PUBLIC", + Self::Video => "$VIDEO", + Self::Resource => "$RESOURCE", + Self::Temp => "$TEMP", + Self::AppConfig => "$APPCONFIG", + Self::AppData => "$APPDATA", + Self::AppLocalData => "$APPLOCALDATA", + Self::AppCache => "$APPCACHE", + Self::AppLog => "$APPLOG", + + #[cfg(not(target_os = "android"))] + Self::Desktop => "$DESKTOP", + #[cfg(not(target_os = "android"))] + Self::Executable => "$EXE", + #[cfg(not(target_os = "android"))] + Self::Font => "$FONT", + #[cfg(not(target_os = "android"))] + Self::Home => "$HOME", + #[cfg(not(target_os = "android"))] + Self::Runtime => "$RUNTIME", + #[cfg(not(target_os = "android"))] + Self::Template => "$TEMPLATE", + } + } + + /// Gets the [`BaseDirectory`] associated with the given variable, or [`None`] if the variable doesn't match any. + pub fn from_variable(variable: &str) -> Option { + let res = match variable { + "$AUDIO" => Self::Audio, + "$CACHE" => Self::Cache, + "$CONFIG" => Self::Config, + "$DATA" => Self::Data, + "$LOCALDATA" => Self::LocalData, + "$DOCUMENT" => Self::Document, + "$DOWNLOAD" => Self::Download, + + "$PICTURE" => Self::Picture, + "$PUBLIC" => Self::Public, + "$VIDEO" => Self::Video, + "$RESOURCE" => Self::Resource, + "$TEMP" => Self::Temp, + "$APPCONFIG" => Self::AppConfig, + "$APPDATA" => Self::AppData, + "$APPLOCALDATA" => Self::AppLocalData, + "$APPCACHE" => Self::AppCache, + "$APPLOG" => Self::AppLog, + + #[cfg(not(target_os = "android"))] + "$DESKTOP" => Self::Desktop, + #[cfg(not(target_os = "android"))] + "$EXE" => Self::Executable, + #[cfg(not(target_os = "android"))] + "$FONT" => Self::Font, + #[cfg(not(target_os = "android"))] + "$HOME" => Self::Home, + #[cfg(not(target_os = "android"))] + "$RUNTIME" => Self::Runtime, + #[cfg(not(target_os = "android"))] + "$TEMPLATE" => Self::Template, + + _ => return None, + }; + Some(res) + } +} + +impl PathResolver { + /// Resolves the path with the base directory. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::{path::BaseDirectory, Manager}; + /// tauri::Builder::default() + /// .setup(|app| { + /// let path = app.path().resolve("path/to/something", BaseDirectory::Config)?; + /// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.config/path/to/something"); + /// Ok(()) + /// }); + /// ``` + pub fn resolve>(&self, path: P, base_directory: BaseDirectory) -> Result { + resolve_path::(self, base_directory, Some(path.as_ref().to_path_buf())) + } + + /// Parse the given path, resolving a [`BaseDirectory`] variable if the path starts with one. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::Manager; + /// tauri::Builder::default() + /// .setup(|app| { + /// let path = app.path().parse("$HOME/.bashrc")?; + /// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.bashrc"); + /// Ok(()) + /// }); + /// ``` + pub fn parse>(&self, path: P) -> Result { + let mut p = PathBuf::new(); + let mut components = path.as_ref().components(); + match components.next() { + Some(Component::Normal(str)) => { + if let Some(base_directory) = BaseDirectory::from_variable(&str.to_string_lossy()) { + p.push(resolve_path::(self, base_directory, None)?); + } else { + p.push(str); + } + } + Some(component) => p.push(component), + None => (), + } + + for component in components { + if let Component::ParentDir = component { + continue; + } + p.push(component); + } + + Ok(p) + } +} + +fn resolve_path( + resolver: &PathResolver, + directory: BaseDirectory, + path: Option, +) -> Result { + let resolve_resource = matches!(directory, BaseDirectory::Resource); + let mut base_dir_path = match directory { + BaseDirectory::Audio => resolver.audio_dir(), + BaseDirectory::Cache => resolver.cache_dir(), + BaseDirectory::Config => resolver.config_dir(), + BaseDirectory::Data => resolver.data_dir(), + BaseDirectory::LocalData => resolver.local_data_dir(), + BaseDirectory::Document => resolver.document_dir(), + BaseDirectory::Download => resolver.download_dir(), + BaseDirectory::Picture => resolver.picture_dir(), + BaseDirectory::Public => resolver.public_dir(), + BaseDirectory::Video => resolver.video_dir(), + BaseDirectory::Resource => resolver.resource_dir(), + BaseDirectory::Temp => resolver.temp_dir(), + BaseDirectory::AppConfig => resolver.app_config_dir(), + BaseDirectory::AppData => resolver.app_data_dir(), + BaseDirectory::AppLocalData => resolver.app_local_data_dir(), + BaseDirectory::AppCache => resolver.app_cache_dir(), + BaseDirectory::AppLog => resolver.app_log_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Desktop => resolver.desktop_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Executable => resolver.executable_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Font => resolver.font_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Home => resolver.home_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Runtime => resolver.runtime_dir(), + #[cfg(not(target_os = "android"))] + BaseDirectory::Template => resolver.template_dir(), + }?; + + if let Some(path) = path { + // use the same path resolution mechanism as the bundler's resource injection algorithm + if resolve_resource { + let mut resource_path = PathBuf::new(); + for component in path.components() { + match component { + Component::Prefix(_) => {} + Component::RootDir => resource_path.push("_root_"), + Component::CurDir => {} + Component::ParentDir => resource_path.push("_up_"), + Component::Normal(p) => resource_path.push(p), + } + } + base_dir_path.push(resource_path); + } else { + base_dir_path.push(path); + } + } + + Ok(base_dir_path) +} + +#[derive(Template)] +#[default_template("./init.js")] +struct InitJavascript { + sep: &'static str, + delimiter: &'static str, +} + +/// Initializes the plugin. +pub(crate) fn init() -> TauriPlugin { + #[cfg(windows)] + let (sep, delimiter) = ("\\", ";"); + #[cfg(not(windows))] + let (sep, delimiter) = ("/", ":"); + + let init_js = InitJavascript { sep, delimiter } + .render_default(&Default::default()) + // this will never fail with the above sep and delimiter values + .unwrap(); + + Builder::new("path") + .invoke_handler(crate::generate_handler![ + commands::resolve_directory, + commands::resolve, + commands::normalize, + commands::join, + commands::dirname, + commands::extname, + commands::basename, + commands::is_absolute + ]) + .js_init_script(init_js.to_string()) + .setup(|app, _api| { + #[cfg(target_os = "android")] + { + let handle = _api.register_android_plugin("app.tauri", "PathPlugin")?; + app.manage(PathResolver(handle)); + } + + #[cfg(not(target_os = "android"))] + { + app.manage(PathResolver(app.clone())); + } + + Ok(()) + }) + .build() +} + +#[cfg(test)] +mod test { + use super::SafePathBuf; + use quickcheck::{Arbitrary, Gen}; + + use std::path::PathBuf; + + impl Arbitrary for SafePathBuf { + fn arbitrary(g: &mut Gen) -> Self { + Self(PathBuf::arbitrary(g)) + } + + fn shrink(&self) -> Box> { + Box::new(self.0.shrink().map(SafePathBuf)) + } + } +} diff --git a/core/tauri/src/pattern.rs b/core/tauri/src/pattern.rs index 8bf953871a2..2318e5972ea 100644 --- a/core/tauri/src/pattern.rs +++ b/core/tauri/src/pattern.rs @@ -109,7 +109,7 @@ pub(crate) struct PatternJavascript { #[allow(dead_code)] pub(crate) fn format_real_schema(schema: &str) -> String { - if cfg!(windows) { + if cfg!(windows) || cfg!(target_os = "android") { format!("https://{schema}.{ISOLATION_IFRAME_SRC_DOMAIN}") } else { format!("{schema}://{ISOLATION_IFRAME_SRC_DOMAIN}") diff --git a/core/tauri/src/plugin.rs b/core/tauri/src/plugin.rs index d27fa5f42c3..048b1935478 100644 --- a/core/tauri/src/plugin.rs +++ b/core/tauri/src/plugin.rs @@ -5,17 +5,24 @@ //! The Tauri plugin extension to expand Tauri functionality. use crate::{ - utils::config::PluginConfig, AppHandle, Invoke, InvokeHandler, PageLoadPayload, RunEvent, - Runtime, Window, + app::PageLoadPayload, + ipc::{Invoke, InvokeHandler}, + utils::config::PluginConfig, + AppHandle, RunEvent, Runtime, Window, }; use serde::de::DeserializeOwned; use serde_json::Value as JsonValue; use tauri_macros::default_runtime; +use url::Url; -use std::{collections::HashMap, fmt}; +use std::{fmt, result::Result as StdResult, sync::Arc}; + +/// Mobile APIs. +#[cfg(mobile)] +pub mod mobile; /// The result type of Tauri plugin module. -pub type Result = std::result::Result>; +pub type Result = StdResult>; /// The plugin interface. pub trait Plugin: Send { @@ -44,6 +51,12 @@ pub trait Plugin: Send { #[allow(unused_variables)] fn created(&mut self, window: Window) {} + /// Callback invoked when webview tries to navigate to the given Url. Returning falses cancels navigation. + #[allow(unused_variables)] + fn on_navigation(&mut self, window: &Window, url: &Url) -> bool { + true + } + /// Callback invoked when the webview performs a navigation to a page. #[allow(unused_variables)] fn on_page_load(&mut self, window: Window, payload: PageLoadPayload) {} @@ -54,16 +67,64 @@ pub trait Plugin: Send { /// Extend commands to [`crate::Builder::invoke_handler`]. #[allow(unused_variables)] - fn extend_api(&mut self, invoke: Invoke) {} + fn extend_api(&mut self, invoke: Invoke) -> bool { + false + } } -type SetupHook = dyn FnOnce(&AppHandle) -> Result<()> + Send; -type SetupWithConfigHook = dyn FnOnce(&AppHandle, T) -> Result<()> + Send; +type SetupHook = dyn FnOnce(&AppHandle, PluginApi) -> Result<()> + Send; type OnWebviewReady = dyn FnMut(Window) + Send; type OnEvent = dyn FnMut(&AppHandle, &RunEvent) + Send; +type OnNavigation = dyn Fn(&Window, &Url) -> bool + Send; type OnPageLoad = dyn FnMut(Window, PageLoadPayload) + Send; type OnDrop = dyn FnOnce(AppHandle) + Send; +/// A handle to a plugin. +#[derive(Debug)] +#[allow(dead_code)] +pub struct PluginHandle { + name: &'static str, + handle: AppHandle, +} + +impl Clone for PluginHandle { + fn clone(&self) -> Self { + Self { + name: self.name, + handle: self.handle.clone(), + } + } +} + +impl PluginHandle { + /// Returns the application handle. + pub fn app(&self) -> &AppHandle { + &self.handle + } +} + +/// Api exposed to the plugin setup hook. +#[derive(Clone)] +#[allow(dead_code)] +pub struct PluginApi { + handle: AppHandle, + name: &'static str, + raw_config: Arc, + config: C, +} + +impl PluginApi { + /// Returns the plugin configuration. + pub fn config(&self) -> &C { + &self.config + } + + /// Returns the application handle. + pub fn app(&self) -> &AppHandle { + &self.handle + } +} + /// Builds a [`TauriPlugin`]. /// /// This Builder offers a more concise way to construct Tauri plugins than implementing the Plugin trait directly. @@ -126,7 +187,7 @@ type OnDrop = dyn FnOnce(AppHandle) + Send; /// /// pub fn build(self) -> TauriPlugin { /// PluginBuilder::new("example") -/// .setup(move |app_handle| { +/// .setup(move |app_handle, api| { /// // use the options here to do stuff /// println!("a: {}, b: {}, c: {}", self.option_a, self.option_b, self.option_c); /// @@ -139,9 +200,9 @@ type OnDrop = dyn FnOnce(AppHandle) + Send; pub struct Builder { name: &'static str, invoke_handler: Box>, - setup: Option>>, - setup_with_config: Option>>, + setup: Option>>, js_init_script: Option, + on_navigation: Box>, on_page_load: Box>, on_webview_ready: Box>, on_event: Box>, @@ -154,9 +215,9 @@ impl Builder { Self { name, setup: None, - setup_with_config: None, js_init_script: None, - invoke_handler: Box::new(|_| ()), + invoke_handler: Box::new(|_| false), + on_navigation: Box::new(|_, _| true), on_page_load: Box::new(|_, _| ()), on_webview_ready: Box::new(|_| ()), on_event: Box::new(|_, _| ()), @@ -190,7 +251,7 @@ impl Builder { #[must_use] pub fn invoke_handler(mut self, invoke_handler: F) -> Self where - F: Fn(Invoke) + Send + Sync + 'static, + F: Fn(Invoke) -> bool + Send + Sync + 'static, { self.invoke_handler = Box::new(invoke_handler); self @@ -234,10 +295,6 @@ impl Builder { /// Define a closure that runs when the plugin is registered. /// - /// This is a convenience function around [setup_with_config], without the need to specify a configuration object. - /// - /// The closure gets called before the [setup_with_config] closure. - /// /// # Examples /// /// ```rust @@ -251,58 +308,45 @@ impl Builder { /// /// fn init() -> TauriPlugin { /// Builder::new("example") - /// .setup(|app_handle| { - /// app_handle.manage(PluginState::default()); + /// .setup(|app, api| { + /// app.manage(PluginState::default()); /// /// Ok(()) /// }) /// .build() /// } /// ``` - /// - /// [setup_with_config]: struct.Builder.html#method.setup_with_config #[must_use] pub fn setup(mut self, setup: F) -> Self where - F: FnOnce(&AppHandle) -> Result<()> + Send + 'static, + F: FnOnce(&AppHandle, PluginApi) -> Result<()> + Send + 'static, { self.setup.replace(Box::new(setup)); self } - /// Define a closure that runs when the plugin is registered, accepting a configuration object set on `tauri.conf.json > plugins > yourPluginName`. - /// - /// If your plugin is not pulling a configuration object from `tauri.conf.json`, use [setup]. - /// - /// The closure gets called after the [setup] closure. + /// Callback invoked when the webview tries to navigate to a URL. Returning false cancels the navigation. /// - /// # Examples + /// #Example /// - /// ```rust,no_run - /// #[derive(serde::Deserialize)] - /// struct Config { - /// api_url: String, - /// } + /// ``` + /// use tauri::{plugin::{Builder, TauriPlugin}, Runtime}; /// - /// fn init() -> tauri::plugin::TauriPlugin { - /// tauri::plugin::Builder::::new("api") - /// .setup_with_config(|_app_handle, config| { - /// println!("config: {:?}", config.api_url); - /// Ok(()) + /// fn init() -> TauriPlugin { + /// Builder::new("example") + /// .on_navigation(|window, url| { + /// // allow the production URL or localhost on dev + /// url.scheme() == "tauri" || (cfg!(dev) && url.host_str() == Some("localhost")) /// }) /// .build() /// } - /// - /// tauri::Builder::default().plugin(init()); /// ``` - /// - /// [setup]: struct.Builder.html#method.setup #[must_use] - pub fn setup_with_config(mut self, setup_with_config: F) -> Self + pub fn on_navigation(mut self, on_navigation: F) -> Self where - F: FnOnce(&AppHandle, C) -> Result<()> + Send + 'static, + F: Fn(&Window, &Url) -> bool + Send + 'static, { - self.setup_with_config.replace(Box::new(setup_with_config)); + self.on_navigation = Box::new(on_navigation); self } @@ -418,8 +462,8 @@ impl Builder { app: None, invoke_handler: self.invoke_handler, setup: self.setup, - setup_with_config: self.setup_with_config, js_init_script: self.js_init_script, + on_navigation: self.on_navigation, on_page_load: self.on_page_load, on_webview_ready: self.on_webview_ready, on_event: self.on_event, @@ -433,9 +477,9 @@ pub struct TauriPlugin { name: &'static str, app: Option>, invoke_handler: Box>, - setup: Option>>, - setup_with_config: Option>>, + setup: Option>>, js_init_script: Option, + on_navigation: Box>, on_page_load: Box>, on_webview_ready: Box>, on_event: Box>, @@ -458,10 +502,15 @@ impl Plugin for TauriPlugin { fn initialize(&mut self, app: &AppHandle, config: JsonValue) -> Result<()> { self.app.replace(app.clone()); if let Some(s) = self.setup.take() { - (s)(app)?; - } - if let Some(s) = self.setup_with_config.take() { - (s)(app, serde_json::from_value(config)?)?; + (s)( + app, + PluginApi { + name: self.name, + handle: app.clone(), + raw_config: Arc::new(config.clone()), + config: serde_json::from_value(config)?, + }, + )?; } Ok(()) } @@ -474,6 +523,10 @@ impl Plugin for TauriPlugin { (self.on_webview_ready)(window) } + fn on_navigation(&mut self, window: &Window, url: &Url) -> bool { + (self.on_navigation)(window, url) + } + fn on_page_load(&mut self, window: Window, payload: PageLoadPayload) { (self.on_page_load)(window, payload) } @@ -482,7 +535,7 @@ impl Plugin for TauriPlugin { (self.on_event)(app, event) } - fn extend_api(&mut self, invoke: Invoke) { + fn extend_api(&mut self, invoke: Invoke) -> bool { (self.invoke_handler)(invoke) } } @@ -490,22 +543,21 @@ impl Plugin for TauriPlugin { /// Plugin collection type. #[default_runtime(crate::Wry, wry)] pub(crate) struct PluginStore { - store: HashMap<&'static str, Box>>, + store: Vec>>, } impl fmt::Debug for PluginStore { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let plugins: Vec<&str> = self.store.iter().map(|plugins| plugins.name()).collect(); f.debug_struct("PluginStore") - .field("plugins", &self.store.keys()) + .field("plugins", &plugins) .finish() } } impl Default for PluginStore { fn default() -> Self { - Self { - store: HashMap::new(), - } + Self { store: Vec::new() } } } @@ -514,12 +566,18 @@ impl PluginStore { /// /// Returns `true` if a plugin with the same name is already in the store. pub fn register + 'static>(&mut self, plugin: P) -> bool { - self.store.insert(plugin.name(), Box::new(plugin)).is_some() + let len = self.store.len(); + self.store.retain(|p| p.name() != plugin.name()); + let result = len != self.store.len(); + self.store.push(Box::new(plugin)); + result } /// Removes the plugin with the given name from the store. pub fn unregister(&mut self, plugin: &'static str) -> bool { - self.store.remove(plugin).is_some() + let len = self.store.len(); + self.store.retain(|p| p.name() != plugin); + len != self.store.len() } /// Initializes all plugins in the store. @@ -528,7 +586,7 @@ impl PluginStore { app: &AppHandle, config: &PluginConfig, ) -> crate::Result<()> { - self.store.values_mut().try_for_each(|plugin| { + self.store.iter_mut().try_for_each(|plugin| { plugin .initialize( app, @@ -542,7 +600,7 @@ impl PluginStore { pub(crate) fn initialization_script(&self) -> String { self .store - .values() + .iter() .filter_map(|p| p.initialization_script()) .fold(String::new(), |acc, script| { format!("{acc}\n(function () {{ {script} }})();") @@ -553,15 +611,24 @@ impl PluginStore { pub(crate) fn created(&mut self, window: Window) { self .store - .values_mut() + .iter_mut() .for_each(|plugin| plugin.created(window.clone())) } + pub(crate) fn on_navigation(&mut self, window: &Window, url: &Url) -> bool { + for plugin in self.store.iter_mut() { + if !plugin.on_navigation(window, url) { + return false; + } + } + true + } + /// Runs the on_page_load hook for all plugins in the store. pub(crate) fn on_page_load(&mut self, window: Window, payload: PageLoadPayload) { self .store - .values_mut() + .iter_mut() .for_each(|plugin| plugin.on_page_load(window.clone(), payload.clone())) } @@ -569,24 +636,20 @@ impl PluginStore { pub(crate) fn on_event(&mut self, app: &AppHandle, event: &RunEvent) { self .store - .values_mut() + .iter_mut() .for_each(|plugin| plugin.on_event(app, event)) } - pub(crate) fn extend_api(&mut self, mut invoke: Invoke) { - let command = invoke.message.command.replace("plugin:", ""); - let mut tokens = command.split('|'); - // safe to unwrap: split always has a least one item - let target = tokens.next().unwrap(); - - if let Some(plugin) = self.store.get_mut(target) { - invoke.message.command = tokens - .next() - .map(|c| c.to_string()) - .unwrap_or_else(String::new); - plugin.extend_api(invoke); - } else { - invoke.resolver.reject(format!("plugin {target} not found")); + /// Runs the plugin `extend_api` hook if it exists. Returns whether the invoke message was handled or not. + /// + /// The message is not handled when the plugin exists **and** the command does not. + pub(crate) fn extend_api(&mut self, plugin: &str, invoke: Invoke) -> bool { + for p in self.store.iter_mut() { + if p.name() == plugin { + return p.extend_api(invoke); + } } + invoke.resolver.reject(format!("plugin {plugin} not found")); + true } } diff --git a/core/tauri/src/plugin/mobile.rs b/core/tauri/src/plugin/mobile.rs new file mode 100644 index 00000000000..4aa726e3609 --- /dev/null +++ b/core/tauri/src/plugin/mobile.rs @@ -0,0 +1,455 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::{PluginApi, PluginHandle}; + +use crate::{ipc::Channel, AppHandle, Runtime}; +#[cfg(target_os = "android")] +use crate::{ + runtime::RuntimeHandle, + sealed::{ManagerBase, RuntimeOrDispatch}, +}; + +use once_cell::sync::OnceCell; +use serde::{de::DeserializeOwned, Serialize}; + +use std::{ + collections::HashMap, + fmt, + sync::{mpsc::channel, Mutex}, +}; + +type PluginResponse = Result; + +type PendingPluginCallHandler = Box; + +static PENDING_PLUGIN_CALLS: OnceCell>> = + OnceCell::new(); +static CHANNELS: OnceCell>> = OnceCell::new(); + +/// Possible errors when invoking a plugin. +#[derive(Debug, thiserror::Error)] +pub enum PluginInvokeError { + /// Failed to reach platform webview handle. + #[error("the webview is unreachable")] + UnreachableWebview, + /// JNI error. + #[cfg(target_os = "android")] + #[error("jni error: {0}")] + Jni(#[from] jni::errors::Error), + /// Error returned from direct mobile plugin invoke. + #[error(transparent)] + InvokeRejected(#[from] ErrorResponse), + /// Failed to deserialize response. + #[error("failed to deserialize response: {0}")] + CannotDeserializeResponse(serde_json::Error), + /// Failed to serialize request payload. + #[error("failed to serialize payload: {0}")] + CannotSerializePayload(serde_json::Error), +} + +pub(crate) fn register_channel(channel: Channel) { + CHANNELS + .get_or_init(Default::default) + .lock() + .unwrap() + .insert(channel.id(), channel); +} + +/// Glue between Rust and the Kotlin code that sends the plugin response back. +#[cfg(target_os = "android")] +pub fn handle_android_plugin_response( + env: &mut jni::JNIEnv<'_>, + id: i32, + success: jni::objects::JString<'_>, + error: jni::objects::JString<'_>, +) { + let (payload, is_ok): (serde_json::Value, bool) = match ( + env + .is_same_object(&success, jni::objects::JObject::default()) + .unwrap_or_default(), + env + .is_same_object(&error, jni::objects::JObject::default()) + .unwrap_or_default(), + ) { + // both null + (true, true) => (serde_json::Value::Null, true), + // error null + (false, true) => ( + serde_json::from_str(env.get_string(&success).unwrap().to_str().unwrap()).unwrap(), + true, + ), + // success null + (true, false) => ( + serde_json::from_str(env.get_string(&error).unwrap().to_str().unwrap()).unwrap(), + false, + ), + // both are set - impossible in the Kotlin code + (false, false) => unreachable!(), + }; + + if let Some(handler) = PENDING_PLUGIN_CALLS + .get_or_init(Default::default) + .lock() + .unwrap() + .remove(&id) + { + handler(if is_ok { Ok(payload) } else { Err(payload) }); + } +} + +/// Glue between Rust and the Kotlin code that sends the channel data. +#[cfg(target_os = "android")] +pub fn send_channel_data( + env: &mut jni::JNIEnv<'_>, + channel_id: i64, + data_str: jni::objects::JString<'_>, +) { + let data: serde_json::Value = + serde_json::from_str(env.get_string(&data_str).unwrap().to_str().unwrap()).unwrap(); + + if let Some(channel) = CHANNELS + .get_or_init(Default::default) + .lock() + .unwrap() + .get(&(channel_id as u32)) + { + let _ = channel.send(data); + } +} + +/// Error response from the Kotlin and Swift backends. +#[derive(Debug, thiserror::Error, Clone, serde::Deserialize)] +pub struct ErrorResponse { + /// Error code. + pub code: Option, + /// Error message. + pub message: Option, + /// Optional error data. + #[serde(flatten)] + pub data: T, +} + +impl fmt::Display for ErrorResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(code) = &self.code { + write!(f, "[{code}]")?; + if self.message.is_some() { + write!(f, " - ")?; + } + } + if let Some(message) = &self.message { + write!(f, "{message}")?; + } + Ok(()) + } +} + +impl PluginApi { + /// Registers an iOS plugin. + #[cfg(target_os = "ios")] + pub fn register_ios_plugin( + &self, + init_fn: unsafe fn() -> *const std::ffi::c_void, + ) -> Result, PluginInvokeError> { + if let Some(window) = self.handle.manager.windows().values().next() { + let (tx, rx) = channel(); + let name = self.name; + let config = self.raw_config.clone(); + window + .with_webview(move |w| { + unsafe { + crate::ios::register_plugin( + &name.into(), + init_fn(), + crate::ios::json_to_dictionary(&config) as _, + w.inner() as _, + ) + }; + tx.send(()).unwrap(); + }) + .map_err(|_| PluginInvokeError::UnreachableWebview)?; + rx.recv().unwrap(); + } else { + unsafe { + crate::ios::register_plugin( + &self.name.into(), + init_fn(), + crate::ios::json_to_dictionary(&self.raw_config) as _, + std::ptr::null(), + ) + }; + } + Ok(PluginHandle { + name: self.name, + handle: self.handle.clone(), + }) + } + + /// Registers an Android plugin. + #[cfg(target_os = "android")] + pub fn register_android_plugin( + &self, + plugin_identifier: &str, + class_name: &str, + ) -> Result, PluginInvokeError> { + use jni::{errors::Error as JniError, objects::JObject, JNIEnv}; + + fn initialize_plugin( + env: &mut JNIEnv<'_>, + activity: &JObject<'_>, + webview: &JObject<'_>, + runtime_handle: &R::Handle, + plugin_name: &'static str, + plugin_class: String, + plugin_config: &serde_json::Value, + ) -> Result<(), JniError> { + // instantiate plugin + let plugin_class = runtime_handle.find_class(env, activity, plugin_class)?; + let plugin = env.new_object( + plugin_class, + "(Landroid/app/Activity;)V", + &[activity.into()], + )?; + + // load plugin + + let plugin_manager = env + .call_method( + activity, + "getPluginManager", + "()Lapp/tauri/plugin/PluginManager;", + &[], + )? + .l()?; + + let plugin_name = env.new_string(plugin_name)?; + let config = + crate::jni_helpers::to_jsobject::(env, activity, &runtime_handle, plugin_config)?; + env.call_method( + plugin_manager, + "load", + "(Landroid/webkit/WebView;Ljava/lang/String;Lapp/tauri/plugin/Plugin;Lapp/tauri/plugin/JSObject;)V", + &[ + webview.into(), + (&plugin_name).into(), + (&plugin).into(), + config.borrow() + ], + )?; + + Ok(()) + } + + let plugin_class = format!("{}/{}", plugin_identifier.replace('.', "/"), class_name); + let plugin_name = self.name; + let plugin_config = self.raw_config.clone(); + let runtime_handle = self.handle.runtime_handle.clone(); + let (tx, rx) = channel(); + self + .handle + .runtime_handle + .run_on_android_context(move |env, activity, webview| { + let result = initialize_plugin::( + env, + activity, + webview, + &runtime_handle, + plugin_name, + plugin_class, + &plugin_config, + ); + tx.send(result).unwrap(); + }); + + rx.recv().unwrap()?; + + Ok(PluginHandle { + name: self.name, + handle: self.handle.clone(), + }) + } +} + +impl PluginHandle { + /// Executes the given mobile command. + pub fn run_mobile_plugin( + &self, + command: impl AsRef, + payload: impl Serialize, + ) -> Result { + let (tx, rx) = channel(); + run_command( + self.name, + &self.handle, + command, + serde_json::to_value(payload).map_err(PluginInvokeError::CannotSerializePayload)?, + move |response| { + tx.send(response).unwrap(); + }, + )?; + + let response = rx.recv().unwrap(); + match response { + Ok(r) => serde_json::from_value(r).map_err(PluginInvokeError::CannotDeserializeResponse), + Err(r) => Err( + serde_json::from_value::(r) + .map(Into::into) + .map_err(PluginInvokeError::CannotDeserializeResponse)?, + ), + } + } +} + +#[cfg(target_os = "ios")] +pub(crate) fn run_command, F: FnOnce(PluginResponse) + Send + 'static>( + name: &str, + _handle: &AppHandle, + command: C, + payload: serde_json::Value, + handler: F, +) -> Result<(), PluginInvokeError> { + use std::{ + ffi::CStr, + os::raw::{c_char, c_int, c_ulonglong}, + }; + + let id: i32 = rand::random(); + PENDING_PLUGIN_CALLS + .get_or_init(Default::default) + .lock() + .unwrap() + .insert(id, Box::new(handler)); + + unsafe { + extern "C" fn plugin_command_response_handler( + id: c_int, + success: c_int, + payload: *const c_char, + ) { + let payload = unsafe { + assert!(!payload.is_null()); + CStr::from_ptr(payload) + }; + + if let Some(handler) = PENDING_PLUGIN_CALLS + .get_or_init(Default::default) + .lock() + .unwrap() + .remove(&id) + { + let payload = serde_json::from_str(payload.to_str().unwrap()).unwrap(); + handler(if success == 1 { + Ok(payload) + } else { + Err(payload) + }); + } + } + + extern "C" fn send_channel_data_handler(id: c_ulonglong, payload: *const c_char) { + let payload = unsafe { + assert!(!payload.is_null()); + CStr::from_ptr(payload) + }; + + if let Some(channel) = CHANNELS + .get_or_init(Default::default) + .lock() + .unwrap() + .get(&(id as u32)) + { + let payload: serde_json::Value = serde_json::from_str(payload.to_str().unwrap()).unwrap(); + let _ = channel.send(payload); + } + } + + crate::ios::run_plugin_command( + id, + &name.into(), + &command.as_ref().into(), + crate::ios::json_to_dictionary(&payload) as _, + crate::ios::PluginMessageCallback(plugin_command_response_handler), + crate::ios::ChannelSendDataCallback(send_channel_data_handler), + ); + } + + Ok(()) +} + +#[cfg(target_os = "android")] +pub(crate) fn run_command< + R: Runtime, + C: AsRef, + F: FnOnce(PluginResponse) + Send + Clone + 'static, +>( + name: &str, + handle: &AppHandle, + command: C, + payload: serde_json::Value, + handler: F, +) -> Result<(), PluginInvokeError> { + use jni::{errors::Error as JniError, objects::JObject, JNIEnv}; + + fn run( + id: i32, + plugin: &str, + command: String, + payload: &serde_json::Value, + runtime_handle: R::Handle, + env: &mut JNIEnv<'_>, + activity: &JObject<'_>, + ) -> Result<(), JniError> { + let plugin = env.new_string(plugin)?; + let command = env.new_string(&command)?; + let data = crate::jni_helpers::to_jsobject::(env, activity, &runtime_handle, payload)?; + let plugin_manager = env + .call_method( + activity, + "getPluginManager", + "()Lapp/tauri/plugin/PluginManager;", + &[], + )? + .l()?; + + env.call_method( + plugin_manager, + "runCommand", + "(ILjava/lang/String;Ljava/lang/String;Lapp/tauri/plugin/JSObject;)V", + &[ + id.into(), + (&plugin).into(), + (&command).into(), + data.borrow(), + ], + )?; + + Ok(()) + } + + let handle = match handle.runtime() { + RuntimeOrDispatch::Runtime(r) => r.handle(), + RuntimeOrDispatch::RuntimeHandle(h) => h, + _ => unreachable!(), + }; + + let id: i32 = rand::random(); + let plugin_name = name.to_string(); + let command = command.as_ref().to_string(); + let handle_ = handle.clone(); + + PENDING_PLUGIN_CALLS + .get_or_init(Default::default) + .lock() + .unwrap() + .insert(id, Box::new(handler.clone())); + + handle.run_on_android_context(move |env, activity, _webview| { + if let Err(e) = run::(id, &plugin_name, command, &payload, handle_, env, activity) { + handler(Err(e.to_string().into())); + } + }); + + Ok(()) +} diff --git a/core/tauri/src/api/process.rs b/core/tauri/src/process.rs similarity index 86% rename from core/tauri/src/api/process.rs rename to core/tauri/src/process.rs index 1cb4c0e0885..7eef25939ac 100644 --- a/core/tauri/src/api/process.rs +++ b/core/tauri/src/process.rs @@ -8,13 +8,6 @@ use crate::Env; use std::path::PathBuf; -#[cfg(feature = "process-command-api")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "process-command-api")))] -mod command; -#[cfg(feature = "process-command-api")] -#[cfg_attr(doc_cfg, doc(cfg(feature = "process-command-api")))] -pub use command::*; - /// Finds the current running binary's path. /// /// With exception to any following platform-specific behavior, the path is cached as soon as @@ -41,7 +34,7 @@ pub use command::*; /// # Examples /// /// ```rust,no_run -/// use tauri::{api::process::current_binary, Env, Manager}; +/// use tauri::{process::current_binary, Env, Manager}; /// let current_binary_path = current_binary(&Env::default()).unwrap(); /// /// tauri::Builder::default() @@ -70,7 +63,7 @@ pub fn current_binary(_env: &Env) -> std::io::Result { /// # Examples /// /// ```rust,no_run -/// use tauri::{api::process::restart, Env, Manager}; +/// use tauri::{process::restart, Env, Manager}; /// /// tauri::Builder::default() /// .setup(|app| { diff --git a/core/tauri/src/scope/fs.rs b/core/tauri/src/scope/fs.rs index 52bf2d23e38..8a1bdc2306d 100644 --- a/core/tauri/src/scope/fs.rs +++ b/core/tauri/src/scope/fs.rs @@ -10,14 +10,9 @@ use std::{ }; pub use glob::Pattern; -use tauri_utils::{ - config::{Config, FsAllowlistScope}, - Env, PackageInfo, -}; +use tauri_utils::config::FsScope; use uuid::Uuid; -use crate::api::path::parse as parse_path; - /// Scope change event. #[derive(Debug, Clone)] pub enum Event { @@ -85,15 +80,14 @@ fn push_pattern, F: Fn(&str) -> Result>( + manager: &M, + scope: &tauri_utils::config::FsScope, ) -> crate::Result { let mut allowed_patterns = HashSet::new(); for path in scope.allowed_paths() { - if let Ok(path) = parse_path(config, package_info, env, path) { + if let Ok(path) = manager.path().parse(path) { push_pattern(&mut allowed_patterns, path, Pattern::new)?; } } @@ -101,14 +95,14 @@ impl Scope { let mut forbidden_patterns = HashSet::new(); if let Some(forbidden_paths) = scope.forbidden_paths() { for path in forbidden_paths { - if let Ok(path) = parse_path(config, package_info, env, path) { + if let Ok(path) = manager.path().parse(path) { push_pattern(&mut forbidden_patterns, path, Pattern::new)?; } } } let require_literal_leading_dot = match scope { - FsAllowlistScope::Scope { + FsScope::Scope { require_literal_leading_dot: Some(require), .. } => *require, diff --git a/core/tauri/src/scope/http.rs b/core/tauri/src/scope/http.rs deleted file mode 100644 index fe2bdb38b0c..00000000000 --- a/core/tauri/src/scope/http.rs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use glob::Pattern; -use tauri_utils::config::HttpAllowlistScope; - -/// Scope for filesystem access. -#[derive(Debug, Clone)] -pub struct Scope { - allowed_urls: Vec, -} - -impl Scope { - /// Creates a new scope from the allowlist's `http` scope configuration. - #[allow(dead_code)] - pub(crate) fn for_http_api(scope: &HttpAllowlistScope) -> Self { - Self { - allowed_urls: scope - .0 - .iter() - .flat_map(|url| { - [ - glob::Pattern::new(url.as_str()) - .unwrap_or_else(|_| panic!("scoped URL is not a valid glob pattern: `{url}`")), - glob::Pattern::new( - url - .as_str() - .strip_suffix('/') - .unwrap_or_else(|| url.as_str()), - ) - .unwrap_or_else(|_| panic!("scoped URL is not a valid glob pattern: `{url}`")), - ] - }) - .collect(), - } - } - - /// Determines if the given URL is allowed on this scope. - pub fn is_allowed(&self, url: &url::Url) -> bool { - self - .allowed_urls - .iter() - .any(|allowed| allowed.matches(url.as_str())) - } -} - -#[cfg(test)] -mod tests { - use tauri_utils::config::HttpAllowlistScope; - - #[test] - fn is_allowed() { - // plain URL - let scope = super::Scope::for_http_api(&HttpAllowlistScope(vec!["http://localhost:8080" - .parse() - .unwrap()])); - assert!(scope.is_allowed(&"http://localhost:8080".parse().unwrap())); - assert!(scope.is_allowed(&"http://localhost:8080/".parse().unwrap())); - - assert!(!scope.is_allowed(&"http://localhost:8080/file".parse().unwrap())); - assert!(!scope.is_allowed(&"http://localhost:8080/path/to/asset.png".parse().unwrap())); - assert!(!scope.is_allowed(&"https://localhost:8080".parse().unwrap())); - assert!(!scope.is_allowed(&"http://localhost:8081".parse().unwrap())); - assert!(!scope.is_allowed(&"http://local:8080".parse().unwrap())); - - // URL with fixed path - let scope = - super::Scope::for_http_api(&HttpAllowlistScope(vec!["http://localhost:8080/file.png" - .parse() - .unwrap()])); - - assert!(scope.is_allowed(&"http://localhost:8080/file.png".parse().unwrap())); - - assert!(!scope.is_allowed(&"http://localhost:8080".parse().unwrap())); - assert!(!scope.is_allowed(&"http://localhost:8080/file".parse().unwrap())); - assert!(!scope.is_allowed(&"http://localhost:8080/file.png/other.jpg".parse().unwrap())); - - // URL with glob pattern - let scope = - super::Scope::for_http_api(&HttpAllowlistScope(vec!["http://localhost:8080/*.png" - .parse() - .unwrap()])); - - assert!(scope.is_allowed(&"http://localhost:8080/file.png".parse().unwrap())); - assert!(scope.is_allowed(&"http://localhost:8080/assets/file.png".parse().unwrap())); - - assert!(!scope.is_allowed(&"http://localhost:8080/file.jpeg".parse().unwrap())); - - let scope = super::Scope::for_http_api(&HttpAllowlistScope(vec!["http://*".parse().unwrap()])); - - assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); - assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); - assert!(!scope.is_allowed(&"https://something.else".parse().unwrap())); - - let scope = super::Scope::for_http_api(&HttpAllowlistScope(vec!["http://**".parse().unwrap()])); - - assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); - assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); - } -} diff --git a/core/tauri/src/scope/ipc.rs b/core/tauri/src/scope/ipc.rs index c127fce98e0..b0eea0bbd17 100644 --- a/core/tauri/src/scope/ipc.rs +++ b/core/tauri/src/scope/ipc.rs @@ -14,7 +14,6 @@ pub struct RemoteDomainAccessScope { domain: String, windows: Vec, plugins: Vec, - enable_tauri_api: bool, } impl RemoteDomainAccessScope { @@ -25,7 +24,6 @@ impl RemoteDomainAccessScope { domain: domain.into(), windows: Vec::new(), plugins: Vec::new(), - enable_tauri_api: false, } } @@ -47,9 +45,13 @@ impl RemoteDomainAccessScope { self } - /// Enables access to the Tauri API. - pub fn enable_tauri_api(mut self) -> Self { - self.enable_tauri_api = true; + /// Adds the given list of plugins to the allowed plugin list. + pub fn add_plugins(mut self, plugins: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.plugins.extend(plugins.into_iter().map(Into::into)); self } @@ -67,11 +69,6 @@ impl RemoteDomainAccessScope { pub fn plugins(&self) -> &Vec { &self.plugins } - - /// Whether this scope enables Tauri API access or not. - pub fn enables_tauri_api(&self) -> bool { - self.enable_tauri_api - } } pub(crate) struct RemoteAccessError { @@ -99,7 +96,6 @@ impl Scope { domain: s.domain, windows: s.windows, plugins: s.plugins, - enable_tauri_api: s.enable_tauri_api, }) .collect(); @@ -119,7 +115,7 @@ impl Scope { /// app.ipc_scope().configure_remote_access( /// RemoteDomainAccessScope::new("tauri.app") /// .add_window("main") - /// .enable_tauri_api() + /// .add_plugins(["path", "event"]) /// ); /// Ok(()) /// }); @@ -172,16 +168,19 @@ impl Scope { mod tests { use super::RemoteDomainAccessScope; use crate::{ - api::ipc::CallbackFn, + ipc::CallbackFn, test::{assert_ipc_response, mock_app, MockRuntime}, - App, InvokePayload, Manager, Window, + window::InvokeRequest, + App, Manager, Window, WindowBuilder, }; const PLUGIN_NAME: &str = "test"; fn test_context(scopes: Vec) -> (App, Window) { let app = mock_app(); - let window = app.get_window("main").unwrap(); + let window = WindowBuilder::new(&app, "main", Default::default()) + .build() + .unwrap(); for scope in scopes { app.ipc_scope().configure_remote_access(scope); @@ -190,37 +189,35 @@ mod tests { (app, window) } - fn app_version_payload() -> InvokePayload { + fn path_is_absolute_request() -> InvokeRequest { let callback = CallbackFn(0); let error = CallbackFn(1); let mut payload = serde_json::Map::new(); - let mut msg = serde_json::Map::new(); - msg.insert( - "cmd".into(), - serde_json::Value::String("getAppVersion".into()), + payload.insert( + "path".into(), + serde_json::Value::String(std::env::current_dir().unwrap().display().to_string()), ); - payload.insert("message".into(), serde_json::Value::Object(msg)); - InvokePayload { - cmd: "".into(), - tauri_module: Some("App".into()), + InvokeRequest { + cmd: "plugin:path|is_absolute".into(), callback, error, - inner: serde_json::Value::Object(payload), + body: serde_json::Value::Object(payload).into(), + headers: Default::default(), } } - fn plugin_test_payload() -> InvokePayload { + fn plugin_test_request() -> InvokeRequest { let callback = CallbackFn(0); let error = CallbackFn(1); - InvokePayload { + InvokeRequest { cmd: format!("plugin:{PLUGIN_NAME}|doSomething"), - tauri_module: None, callback, error, - inner: Default::default(), + body: Default::default(), + headers: Default::default(), } } @@ -228,12 +225,12 @@ mod tests { fn scope_not_defined() { let (_app, mut window) = test_context(vec![RemoteDomainAccessScope::new("app.tauri.app") .add_window("other") - .enable_tauri_api()]); + .add_plugin("path")]); window.navigate("https://tauri.app".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + path_is_absolute_request(), Err(&crate::window::ipc_scope_not_found_error_message( "main", "https://tauri.app/", @@ -245,12 +242,12 @@ mod tests { fn scope_not_defined_for_window() { let (_app, mut window) = test_context(vec![RemoteDomainAccessScope::new("tauri.app") .add_window("second") - .enable_tauri_api()]); + .add_plugin("path")]); window.navigate("https://tauri.app".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + path_is_absolute_request(), Err(&crate::window::ipc_scope_window_error_message("main")), ); } @@ -259,12 +256,12 @@ mod tests { fn scope_not_defined_for_url() { let (_app, mut window) = test_context(vec![RemoteDomainAccessScope::new("github.com") .add_window("main") - .enable_tauri_api()]); + .add_plugin("path")]); window.navigate("https://tauri.app".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + path_is_absolute_request(), Err(&crate::window::ipc_scope_domain_error_message( "https://tauri.app/", )), @@ -273,43 +270,35 @@ mod tests { #[test] fn subdomain_is_not_allowed() { - let (app, mut window) = test_context(vec![ + let (_app, mut window) = test_context(vec![ RemoteDomainAccessScope::new("tauri.app") .add_window("main") - .enable_tauri_api(), + .add_plugin("path"), RemoteDomainAccessScope::new("sub.tauri.app") .add_window("main") - .enable_tauri_api(), + .add_plugin("path"), ]); window.navigate("https://tauri.app".parse().unwrap()); - assert_ipc_response( - &window, - app_version_payload(), - Ok(app.package_info().version.to_string().as_str()), - ); + assert_ipc_response(&window, path_is_absolute_request(), Ok(true)); window.navigate("https://blog.tauri.app".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + path_is_absolute_request(), Err(&crate::window::ipc_scope_domain_error_message( "https://blog.tauri.app/", )), ); window.navigate("https://sub.tauri.app".parse().unwrap()); - assert_ipc_response( - &window, - app_version_payload(), - Ok(app.package_info().version.to_string().as_str()), - ); + assert_ipc_response(&window, path_is_absolute_request(), Ok(true)); window.window.label = "test".into(); window.navigate("https://dev.tauri.app".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + path_is_absolute_request(), Err(&crate::window::ipc_scope_not_found_error_message( "test", "https://dev.tauri.app/", @@ -319,16 +308,12 @@ mod tests { #[test] fn subpath_is_allowed() { - let (app, mut window) = test_context(vec![RemoteDomainAccessScope::new("tauri.app") + let (_app, mut window) = test_context(vec![RemoteDomainAccessScope::new("tauri.app") .add_window("main") - .enable_tauri_api()]); + .add_plugin("path")]); window.navigate("https://tauri.app/inner/path".parse().unwrap()); - assert_ipc_response( - &window, - app_version_payload(), - Ok(app.package_info().version.to_string().as_str()), - ); + assert_ipc_response(&window, path_is_absolute_request(), Ok(true)); } #[test] @@ -340,7 +325,7 @@ mod tests { window.navigate("https://tauri.app".parse().unwrap()); assert_ipc_response( &window, - app_version_payload(), + path_is_absolute_request(), Err(crate::window::IPC_SCOPE_DOES_NOT_ALLOW), ); } @@ -354,7 +339,7 @@ mod tests { window.navigate("https://tauri.app".parse().unwrap()); assert_ipc_response( &window, - plugin_test_payload(), + plugin_test_request(), Err(&format!("plugin {PLUGIN_NAME} not found")), ); } @@ -368,7 +353,7 @@ mod tests { window.navigate("https://tauri.app".parse().unwrap()); assert_ipc_response( &window, - plugin_test_payload(), + plugin_test_request(), Err(crate::window::IPC_SCOPE_DOES_NOT_ALLOW), ); } diff --git a/core/tauri/src/scope/mod.rs b/core/tauri/src/scope/mod.rs index 72243c29e49..3bb818d384b 100644 --- a/core/tauri/src/scope/mod.rs +++ b/core/tauri/src/scope/mod.rs @@ -3,48 +3,42 @@ // SPDX-License-Identifier: MIT mod fs; -mod http; /// IPC scope. pub mod ipc; -#[cfg(shell_scope)] -mod shell; -pub use self::http::Scope as HttpScope; pub use self::ipc::Scope as IpcScope; pub use fs::{Event as FsScopeEvent, Pattern as GlobPattern, Scope as FsScope}; -#[cfg(shell_scope)] -pub use shell::{ - ExecuteArgs, Scope as ShellScope, ScopeAllowedArg as ShellScopeAllowedArg, - ScopeAllowedCommand as ShellScopeAllowedCommand, ScopeConfig as ShellScopeConfig, - ScopeError as ShellScopeError, -}; use std::path::Path; -pub(crate) struct Scopes { - pub ipc: IpcScope, - pub fs: FsScope, - #[cfg(protocol_asset)] - pub asset_protocol: FsScope, - #[cfg(http_request)] - pub http: HttpScope, - #[cfg(shell_scope)] - pub shell: ShellScope, +/// Managed state for all the core scopes in a tauri application. +pub struct Scopes { + pub(crate) ipc: IpcScope, + #[cfg(feature = "protocol-asset")] + pub(crate) asset_protocol: FsScope, } impl Scopes { - #[allow(dead_code)] - pub(crate) fn allow_directory(&self, path: &Path, recursive: bool) -> crate::Result<()> { - self.fs.allow_directory(path, recursive)?; - #[cfg(protocol_asset)] + /// Allows a directory on the scopes. + #[allow(unused)] + pub fn allow_directory>(&self, path: P, recursive: bool) -> crate::Result<()> { + #[cfg(feature = "protocol-asset")] self.asset_protocol.allow_directory(path, recursive)?; Ok(()) } - #[allow(dead_code)] - pub(crate) fn allow_file(&self, path: &Path) -> crate::Result<()> { - self.fs.allow_file(path)?; - #[cfg(protocol_asset)] + /// Allows a file on the scopes. + #[allow(unused)] + pub fn allow_file>(&self, path: P) -> crate::Result<()> { + #[cfg(feature = "protocol-asset")] self.asset_protocol.allow_file(path)?; Ok(()) } + + /// Forbids a file on the scopes. + #[allow(unused)] + pub fn forbid_file>(&self, path: P) -> crate::Result<()> { + #[cfg(feature = "protocol-asset")] + self.asset_protocol.forbid_file(path)?; + Ok(()) + } } diff --git a/core/tauri/src/scope/shell.rs b/core/tauri/src/scope/shell.rs deleted file mode 100644 index 978214bfc31..00000000000 --- a/core/tauri/src/scope/shell.rs +++ /dev/null @@ -1,325 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#[cfg(any(shell_execute, shell_sidecar))] -use crate::api::process::Command; -#[cfg(feature = "shell-open-api")] -use crate::api::shell::Program; - -use regex::Regex; -use tauri_utils::{config::Config, Env, PackageInfo}; - -use std::collections::HashMap; - -/// Allowed representation of `Execute` command arguments. -#[derive(Debug, Clone, serde::Deserialize)] -#[serde(untagged, deny_unknown_fields)] -#[non_exhaustive] -pub enum ExecuteArgs { - /// No arguments - None, - - /// A single string argument - Single(String), - - /// Multiple string arguments - List(Vec), -} - -impl ExecuteArgs { - /// Whether the argument list is empty or not. - pub fn is_empty(&self) -> bool { - match self { - Self::None => true, - Self::Single(s) if s.is_empty() => true, - Self::List(l) => l.is_empty(), - _ => false, - } - } -} - -impl From<()> for ExecuteArgs { - fn from(_: ()) -> Self { - Self::None - } -} - -impl From for ExecuteArgs { - fn from(string: String) -> Self { - Self::Single(string) - } -} - -impl From> for ExecuteArgs { - fn from(vec: Vec) -> Self { - Self::List(vec) - } -} - -/// Shell scope configuration. -#[derive(Debug, Clone)] -pub struct ScopeConfig { - /// The validation regex that `shell > open` paths must match against. - pub open: Option, - - /// All allowed commands, using their unique command name as the keys. - pub scopes: HashMap, -} - -/// A configured scoped shell command. -#[derive(Debug, Clone)] -pub struct ScopeAllowedCommand { - /// The shell command to be called. - pub command: std::path::PathBuf, - - /// The arguments the command is allowed to be called with. - pub args: Option>, - - /// If this command is a sidecar command. - pub sidecar: bool, -} - -/// A configured argument to a scoped shell command. -#[derive(Debug, Clone)] -pub enum ScopeAllowedArg { - /// A non-configurable argument. - Fixed(String), - - /// An argument with a value to be evaluated at runtime, must pass a regex validation. - Var { - /// The validation that the variable value must pass in order to be called. - validator: Regex, - }, -} - -impl ScopeAllowedArg { - /// If the argument is fixed. - pub fn is_fixed(&self) -> bool { - matches!(self, Self::Fixed(_)) - } - - /// If the argument is a variable value. - pub fn is_var(&self) -> bool { - matches!(self, Self::Var { .. }) - } -} - -/// Scope for filesystem access. -#[derive(Clone)] -pub struct Scope(ScopeConfig); - -/// All errors that can happen while validating a scoped command. -#[derive(Debug, thiserror::Error)] -pub enum ScopeError { - /// At least one argument did not pass input validation. - #[cfg(any(shell_execute, shell_sidecar))] - #[cfg_attr( - doc_cfg, - doc(cfg(any(feature = "shell-execute", feature = "shell-sidecar"))) - )] - #[error("The scoped command was called with the improper sidecar flag set")] - BadSidecarFlag, - - /// The sidecar program validated but failed to find the sidecar path. - /// - /// Note: This can be called on `shell-execute` feature too due to [`Scope::prepare`] checking if - /// it's a sidecar from the config. - #[cfg(any(shell_execute, shell_sidecar))] - #[cfg_attr( - doc_cfg, - doc(cfg(any(feature = "shell-execute", feature = "shell-sidecar"))) - )] - #[error( - "The scoped sidecar command was validated, but failed to create the path to the command: {0}" - )] - Sidecar(crate::Error), - - /// The named command was not found in the scoped config. - #[error("Scoped command {0} not found")] - #[cfg(any(shell_execute, shell_sidecar))] - #[cfg_attr( - doc_cfg, - doc(cfg(any(feature = "shell-execute", feature = "shell-sidecar"))) - )] - NotFound(String), - - /// A command variable has no value set in the arguments. - #[error( - "Scoped command argument at position {0} must match regex validation {1} but it was not found" - )] - #[cfg(any(shell_execute, shell_sidecar))] - #[cfg_attr( - doc_cfg, - doc(cfg(any(feature = "shell-execute", feature = "shell-sidecar"))) - )] - MissingVar(usize, String), - - /// At least one argument did not pass input validation. - #[cfg(shell_scope)] - #[cfg_attr( - doc_cfg, - doc(cfg(any(feature = "shell-execute", feature = "shell-open"))) - )] - #[error("Scoped command argument at position {index} was found, but failed regex validation {validation}")] - Validation { - /// Index of the variable. - index: usize, - - /// Regex that the variable value failed to match. - validation: String, - }, - - /// The format of the passed input does not match the expected shape. - /// - /// This can happen from passing a string or array of strings to a command that is expecting - /// named variables, and vice-versa. - #[cfg(any(shell_execute, shell_sidecar))] - #[cfg_attr( - doc_cfg, - doc(cfg(any(feature = "shell-execute", feature = "shell-sidecar"))) - )] - #[error("Scoped command {0} received arguments in an unexpected format")] - InvalidInput(String), - - /// A generic IO error that occurs while executing specified shell commands. - #[cfg(shell_scope)] - #[cfg_attr( - doc_cfg, - doc(cfg(any(feature = "shell-execute", feature = "shell-sidecar"))) - )] - #[error("Scoped shell IO error: {0}")] - Io(#[from] std::io::Error), -} - -impl Scope { - /// Creates a new shell scope. - pub(crate) fn new( - config: &Config, - package_info: &PackageInfo, - env: &Env, - mut scope: ScopeConfig, - ) -> Self { - for cmd in scope.scopes.values_mut() { - if let Ok(path) = crate::api::path::parse(config, package_info, env, &cmd.command) { - cmd.command = path; - } - } - Self(scope) - } - - /// Validates argument inputs and creates a Tauri sidecar [`Command`]. - #[cfg(shell_sidecar)] - pub fn prepare_sidecar( - &self, - command_name: &str, - command_script: &str, - args: ExecuteArgs, - ) -> Result { - self._prepare(command_name, args, Some(command_script)) - } - - /// Validates argument inputs and creates a Tauri [`Command`]. - #[cfg(shell_execute)] - pub fn prepare(&self, command_name: &str, args: ExecuteArgs) -> Result { - self._prepare(command_name, args, None) - } - - /// Validates argument inputs and creates a Tauri [`Command`]. - #[cfg(any(shell_execute, shell_sidecar))] - pub fn _prepare( - &self, - command_name: &str, - args: ExecuteArgs, - sidecar: Option<&str>, - ) -> Result { - let command = match self.0.scopes.get(command_name) { - Some(command) => command, - None => return Err(ScopeError::NotFound(command_name.into())), - }; - - if command.sidecar != sidecar.is_some() { - return Err(ScopeError::BadSidecarFlag); - } - - let args = match (&command.args, args) { - (None, ExecuteArgs::None) => Ok(vec![]), - (None, ExecuteArgs::List(list)) => Ok(list), - (None, ExecuteArgs::Single(string)) => Ok(vec![string]), - (Some(list), ExecuteArgs::List(args)) => list - .iter() - .enumerate() - .map(|(i, arg)| match arg { - ScopeAllowedArg::Fixed(fixed) => Ok(fixed.to_string()), - ScopeAllowedArg::Var { validator } => { - let value = args - .get(i) - .ok_or_else(|| ScopeError::MissingVar(i, validator.to_string()))? - .to_string(); - if validator.is_match(&value) { - Ok(value) - } else { - Err(ScopeError::Validation { - index: i, - validation: validator.to_string(), - }) - } - } - }) - .collect(), - (Some(list), arg) if arg.is_empty() && list.iter().all(ScopeAllowedArg::is_fixed) => list - .iter() - .map(|arg| match arg { - ScopeAllowedArg::Fixed(fixed) => Ok(fixed.to_string()), - _ => unreachable!(), - }) - .collect(), - (Some(list), _) if list.is_empty() => Err(ScopeError::InvalidInput(command_name.into())), - (Some(_), _) => Err(ScopeError::InvalidInput(command_name.into())), - }?; - - let command_s = sidecar - .map(|s| { - std::path::PathBuf::from(s) - .components() - .last() - .unwrap() - .as_os_str() - .to_string_lossy() - .into_owned() - }) - .unwrap_or_else(|| command.command.to_string_lossy().into_owned()); - let command = if command.sidecar { - Command::new_sidecar(command_s).map_err(ScopeError::Sidecar)? - } else { - Command::new(command_s) - }; - - Ok(command.args(args)) - } - - /// Open a path in the default (or specified) browser. - /// - /// The path is validated against the `tauri > allowlist > shell > open` validation regex, which - /// defaults to `^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`. - #[cfg(feature = "shell-open-api")] - pub fn open(&self, path: &str, with: Option) -> Result<(), ScopeError> { - // ensure we pass validation if the configuration has one - if let Some(regex) = &self.0.open { - if !regex.is_match(path) { - return Err(ScopeError::Validation { - index: 0, - validation: regex.as_str().into(), - }); - } - } - - // The prevention of argument escaping is handled by the usage of std::process::Command::arg by - // the `open` dependency. This behavior should be re-confirmed during upgrades of `open`. - match with.map(Program::name) { - Some(program) => ::open::with(path, program), - None => ::open::that(path), - } - .map_err(Into::into) - } -} diff --git a/core/tauri/src/state.rs b/core/tauri/src/state.rs index b13f0c0f3f4..09801423437 100644 --- a/core/tauri/src/state.rs +++ b/core/tauri/src/state.rs @@ -4,9 +4,10 @@ use crate::{ command::{CommandArg, CommandItem}, - InvokeError, Runtime, + ipc::InvokeError, + Runtime, }; -use state::Container; +use state::TypeMap; /// A guard for a state value. /// @@ -58,11 +59,11 @@ impl<'r, 'de: 'r, T: Send + Sync + 'static, R: Runtime> CommandArg<'de, R> for S /// The Tauri state manager. #[derive(Debug)] -pub struct StateManager(pub(crate) Container![Send + Sync]); +pub struct StateManager(pub(crate) TypeMap![Send + Sync]); impl StateManager { pub(crate) fn new() -> Self { - Self(::new()) + Self(::new()) } pub(crate) fn set(&self, state: T) -> bool { diff --git a/core/tauri/src/test/mock_runtime.rs b/core/tauri/src/test/mock_runtime.rs index 8fe083dfd49..b466de798ec 100644 --- a/core/tauri/src/test/mock_runtime.rs +++ b/core/tauri/src/test/mock_runtime.rs @@ -6,24 +6,20 @@ #![allow(missing_docs)] use tauri_runtime::{ - menu::{Menu, MenuUpdate}, monitor::Monitor, webview::{WindowBuilder, WindowBuilderBase}, window::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, - CursorIcon, DetachedWindow, MenuEvent, PendingWindow, WindowEvent, + CursorIcon, DetachedWindow, PendingWindow, RawWindow, WindowEvent, }, DeviceEventFilter, Dispatch, Error, EventLoopProxy, ExitRequestedEventAction, Icon, Result, - RunEvent, Runtime, RuntimeHandle, UserAttentionType, UserEvent, -}; -#[cfg(all(desktop, feature = "system-tray"))] -use tauri_runtime::{ - menu::{SystemTrayMenu, TrayHandle}, - SystemTray, SystemTrayEvent, TrayId, + RunEvent, Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType, UserEvent, }; + #[cfg(target_os = "macos")] use tauri_utils::TitleBarStyle; use tauri_utils::{config::WindowConfig, Theme}; +use url::Url; use uuid::Uuid; #[cfg(windows)] @@ -55,7 +51,6 @@ pub struct RuntimeContext { is_running: Arc, windows: Arc>>, shortcuts: Arc>, - clipboard: Arc>>, run_tx: SyncSender, } @@ -88,9 +83,7 @@ impl RuntimeContext { impl fmt::Debug for RuntimeContext { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RuntimeContext") - .field("clipboard", &self.clipboard) - .finish() + f.debug_struct("RuntimeContext").finish() } } @@ -107,9 +100,10 @@ impl RuntimeHandle for MockRuntimeHandle { } /// Create a new webview window. - fn create_window( + fn create_window) + Send + 'static>( &self, pending: PendingWindow, + _before_webview_creation: Option, ) -> Result> { let id = rand::random(); self.context.windows.borrow_mut().insert(id, Window); @@ -119,10 +113,8 @@ impl RuntimeHandle for MockRuntimeHandle { id, context: self.context.clone(), last_evaluated_script: Default::default(), - url: pending.url, + url: Arc::new(Mutex::new(pending.url)), }, - menu_ids: Default::default(), - js_event_listeners: Default::default(), }) } @@ -131,17 +123,6 @@ impl RuntimeHandle for MockRuntimeHandle { self.context.send_message(Message::Task(Box::new(f))) } - #[cfg(all(desktop, feature = "system-tray"))] - #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "system-tray"))))] - fn system_tray( - &self, - system_tray: SystemTray, - ) -> Result<>::TrayHandler> { - Ok(MockTrayHandler { - context: self.context.clone(), - }) - } - fn raw_display_handle(&self) -> raw_window_handle::RawDisplayHandle { #[cfg(target_os = "linux")] return raw_window_handle::RawDisplayHandle::Xlib(raw_window_handle::XlibDisplayHandle::empty()); @@ -157,6 +138,14 @@ impl RuntimeHandle for MockRuntimeHandle { return unimplemented!(); } + fn primary_monitor(&self) -> Option { + unimplemented!() + } + + fn available_monitors(&self) -> Vec { + unimplemented!() + } + /// Shows the application, but does not automatically focus it. #[cfg(target_os = "macos")] fn show(&self) -> Result<()> { @@ -168,13 +157,31 @@ impl RuntimeHandle for MockRuntimeHandle { fn hide(&self) -> Result<()> { Ok(()) } + + #[cfg(target_os = "android")] + fn find_class<'a>( + &'a self, + env: &'a mut jni::JNIEnv<'a>, + activity: &'a jni::objects::JObject<'a>, + name: impl Into, + ) -> std::result::Result, jni::errors::Error> { + todo!() + } + + #[cfg(target_os = "android")] + fn run_on_android_context(&self, f: F) + where + F: FnOnce(&mut jni::JNIEnv, &jni::objects::JObject, &jni::objects::JObject) + Send + 'static, + { + todo!() + } } #[derive(Debug, Clone)] pub struct MockDispatcher { id: WindowId, context: RuntimeContext, - url: String, + url: Arc>, last_evaluated_script: Arc>>, } @@ -184,64 +191,6 @@ impl MockDispatcher { } } -#[cfg(all(desktop, feature = "global-shortcut"))] -#[derive(Debug, Clone)] -pub struct MockGlobalShortcutManager { - context: RuntimeContext, -} - -#[cfg(all(desktop, feature = "global-shortcut"))] -impl tauri_runtime::GlobalShortcutManager for MockGlobalShortcutManager { - fn is_registered(&self, accelerator: &str) -> Result { - Ok( - self - .context - .shortcuts - .lock() - .unwrap() - .contains_key(accelerator), - ) - } - - fn register(&mut self, accelerator: &str, handler: F) -> Result<()> { - self - .context - .shortcuts - .lock() - .unwrap() - .insert(accelerator.into(), Box::new(handler)); - Ok(()) - } - - fn unregister_all(&mut self) -> Result<()> { - *self.context.shortcuts.lock().unwrap() = Default::default(); - Ok(()) - } - - fn unregister(&mut self, accelerator: &str) -> Result<()> { - self.context.shortcuts.lock().unwrap().remove(accelerator); - Ok(()) - } -} - -#[cfg(feature = "clipboard")] -#[derive(Debug, Clone)] -pub struct MockClipboardManager { - context: RuntimeContext, -} - -#[cfg(feature = "clipboard")] -impl tauri_runtime::ClipboardManager for MockClipboardManager { - fn write_text>(&mut self, text: T) -> Result<()> { - self.context.clipboard.lock().unwrap().replace(text.into()); - Ok(()) - } - - fn read_text(&self) -> Result> { - Ok(self.context.clipboard.lock().unwrap().clone()) - } -} - #[derive(Debug, Clone)] pub struct MockWindowBuilder {} @@ -256,10 +205,6 @@ impl WindowBuilder for MockWindowBuilder { Self {} } - fn menu(self, menu: Menu) -> Self { - self - } - fn center(self) -> Self { self } @@ -333,6 +278,10 @@ impl WindowBuilder for MockWindowBuilder { self } + fn visible_on_all_workspaces(self, visible_on_all_workspaces: bool) -> Self { + self + } + fn content_protected(self, protected: bool) -> Self { self } @@ -345,6 +294,10 @@ impl WindowBuilder for MockWindowBuilder { self } + fn shadow(self, enable: bool) -> Self { + self + } + #[cfg(windows)] fn parent_window(self, parent: HWND) -> Self { self @@ -382,10 +335,6 @@ impl WindowBuilder for MockWindowBuilder { fn has_icon(&self) -> bool { false } - - fn get_menu(&self) -> Option<&Menu> { - None - } } impl Dispatch for MockDispatcher { @@ -401,8 +350,8 @@ impl Dispatch for MockDispatcher { Uuid::new_v4() } - fn on_menu_event(&self, f: F) -> Uuid { - Uuid::new_v4() + fn with_webview) + Send + 'static>(&self, f: F) -> Result<()> { + Ok(()) } #[cfg(any(debug_assertions, feature = "devtools"))] @@ -417,7 +366,12 @@ impl Dispatch for MockDispatcher { } fn url(&self) -> Result { - self.url.parse().map_err(|_| Error::FailedToReceiveMessage) + self + .url + .lock() + .unwrap() + .parse() + .map_err(|_| Error::FailedToReceiveMessage) } fn scale_factor(&self) -> Result { @@ -490,10 +444,6 @@ impl Dispatch for MockDispatcher { Ok(String::new()) } - fn is_menu_visible(&self) -> Result { - Ok(true) - } - fn current_monitor(&self) -> Result> { Ok(None) } @@ -521,6 +471,17 @@ impl Dispatch for MockDispatcher { unimplemented!() } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + fn default_vbox(&self) -> Result { + unimplemented!() + } + fn raw_window_handle(&self) -> Result { #[cfg(target_os = "linux")] return Ok(raw_window_handle::RawWindowHandle::Xlib( @@ -550,9 +511,10 @@ impl Dispatch for MockDispatcher { Ok(()) } - fn create_window( + fn create_window) + Send + 'static>( &mut self, pending: PendingWindow, + _before_webview_creation: Option, ) -> Result> { let id = rand::random(); self.context.windows.borrow_mut().insert(id, Window); @@ -562,10 +524,8 @@ impl Dispatch for MockDispatcher { id, context: self.context.clone(), last_evaluated_script: Default::default(), - url: pending.url, + url: Arc::new(Mutex::new(pending.url)), }, - menu_ids: Default::default(), - js_event_listeners: Default::default(), }) } @@ -589,6 +549,11 @@ impl Dispatch for MockDispatcher { Ok(()) } + fn navigate(&self, url: Url) -> Result<()> { + *self.url.lock().unwrap() = url.to_string(); + Ok(()) + } + fn maximize(&self) -> Result<()> { Ok(()) } @@ -605,14 +570,6 @@ impl Dispatch for MockDispatcher { Ok(()) } - fn show_menu(&self) -> Result<()> { - Ok(()) - } - - fn hide_menu(&self) -> Result<()> { - Ok(()) - } - fn show(&self) -> Result<()> { Ok(()) } @@ -630,10 +587,18 @@ impl Dispatch for MockDispatcher { Ok(()) } + fn set_shadow(&self, shadow: bool) -> Result<()> { + Ok(()) + } + fn set_always_on_top(&self, always_on_top: bool) -> Result<()> { Ok(()) } + fn set_visible_on_all_workspaces(&self, visible_on_all_workspaces: bool) -> Result<()> { + Ok(()) + } + fn set_content_protected(&self, protected: bool) -> Result<()> { Ok(()) } @@ -702,46 +667,6 @@ impl Dispatch for MockDispatcher { .replace(script.into()); Ok(()) } - - fn update_menu_item(&self, id: u16, update: MenuUpdate) -> Result<()> { - Ok(()) - } -} - -#[cfg(all(desktop, feature = "system-tray"))] -#[derive(Debug, Clone)] -pub struct MockTrayHandler { - context: RuntimeContext, -} - -#[cfg(all(desktop, feature = "system-tray"))] -impl TrayHandle for MockTrayHandler { - fn set_icon(&self, icon: Icon) -> Result<()> { - Ok(()) - } - fn set_menu(&self, menu: SystemTrayMenu) -> Result<()> { - Ok(()) - } - fn update_item(&self, id: u16, update: MenuUpdate) -> Result<()> { - Ok(()) - } - #[cfg(target_os = "macos")] - fn set_icon_as_template(&self, is_template: bool) -> Result<()> { - Ok(()) - } - - #[cfg(target_os = "macos")] - fn set_title(&self, title: &str) -> tauri_runtime::Result<()> { - Ok(()) - } - - fn set_tooltip(&self, tooltip: &str) -> Result<()> { - Ok(()) - } - - fn destroy(&self) -> Result<()> { - Ok(()) - } } #[derive(Debug, Clone)] @@ -757,12 +682,6 @@ impl EventLoopProxy for EventProxy { pub struct MockRuntime { is_running: Arc, pub context: RuntimeContext, - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager: MockGlobalShortcutManager, - #[cfg(feature = "clipboard")] - clipboard_manager: MockClipboardManager, - #[cfg(all(desktop, feature = "system-tray"))] - tray_handler: MockTrayHandler, run_rx: Receiver, } @@ -774,23 +693,10 @@ impl MockRuntime { is_running: is_running.clone(), windows: Default::default(), shortcuts: Default::default(), - clipboard: Default::default(), run_tx: tx, }; Self { is_running, - #[cfg(all(desktop, feature = "global-shortcut"))] - global_shortcut_manager: MockGlobalShortcutManager { - context: context.clone(), - }, - #[cfg(feature = "clipboard")] - clipboard_manager: MockClipboardManager { - context: context.clone(), - }, - #[cfg(all(desktop, feature = "system-tray"))] - tray_handler: MockTrayHandler { - context: context.clone(), - }, context, run_rx: rx, } @@ -800,20 +706,14 @@ impl MockRuntime { impl Runtime for MockRuntime { type Dispatcher = MockDispatcher; type Handle = MockRuntimeHandle; - #[cfg(all(desktop, feature = "global-shortcut"))] - type GlobalShortcutManager = MockGlobalShortcutManager; - #[cfg(feature = "clipboard")] - type ClipboardManager = MockClipboardManager; - #[cfg(all(desktop, feature = "system-tray"))] - type TrayHandler = MockTrayHandler; type EventLoopProxy = EventProxy; - fn new() -> Result { + fn new(_args: RuntimeInitArgs) -> Result { Ok(Self::init()) } #[cfg(any(windows, target_os = "linux"))] - fn new_any_thread() -> Result { + fn new_any_thread(_args: RuntimeInitArgs) -> Result { Ok(Self::init()) } @@ -827,17 +727,11 @@ impl Runtime for MockRuntime { } } - #[cfg(all(desktop, feature = "global-shortcut"))] - fn global_shortcut_manager(&self) -> Self::GlobalShortcutManager { - self.global_shortcut_manager.clone() - } - - #[cfg(feature = "clipboard")] - fn clipboard_manager(&self) -> Self::ClipboardManager { - self.clipboard_manager.clone() - } - - fn create_window(&self, pending: PendingWindow) -> Result> { + fn create_window) + Send + 'static>( + &self, + pending: PendingWindow, + _before_webview_creation: Option, + ) -> Result> { let id = rand::random(); self.context.windows.borrow_mut().insert(id, Window); Ok(DetachedWindow { @@ -846,22 +740,18 @@ impl Runtime for MockRuntime { id, context: self.context.clone(), last_evaluated_script: Default::default(), - url: pending.url, + url: Arc::new(Mutex::new(pending.url)), }, - menu_ids: Default::default(), - js_event_listeners: Default::default(), }) } - #[cfg(all(desktop, feature = "system-tray"))] - #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] - fn system_tray(&self, system_tray: SystemTray) -> Result { - Ok(self.tray_handler.clone()) + fn primary_monitor(&self) -> Option { + unimplemented!() } - #[cfg(all(desktop, feature = "system-tray"))] - #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] - fn on_system_tray_event(&mut self, f: F) {} + fn available_monitors(&self) -> Vec { + unimplemented!() + } #[cfg(target_os = "macos")] #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))] diff --git a/core/tauri/src/test/mod.rs b/core/tauri/src/test/mod.rs index eaadae04f64..7b0a20e4385 100644 --- a/core/tauri/src/test/mod.rs +++ b/core/tauri/src/test/mod.rs @@ -42,12 +42,12 @@ //! // in this case we'll run the my_cmd command with no arguments //! tauri::test::assert_ipc_response( //! &window, -//! tauri::InvokePayload { +//! tauri::window::InvokeRequest { //! cmd: "my_cmd".into(), -//! tauri_module: None, -//! callback: tauri::api::ipc::CallbackFn(0), -//! error: tauri::api::ipc::CallbackFn(1), -//! inner: serde_json::Value::Null, +//! callback: tauri::ipc::CallbackFn(0), +//! error: tauri::ipc::CallbackFn(1), +//! body: serde_json::Value::Null.into(), +//! headers: Default::default(), //! }, //! Ok(()) //! ); @@ -60,26 +60,22 @@ mod mock_runtime; pub use mock_runtime::*; use serde::Serialize; -use serde_json::Value as JsonValue; use std::{ borrow::Cow, - collections::HashMap, fmt::Debug, hash::{Hash, Hasher}, - sync::{ - mpsc::{channel, Sender}, - Arc, Mutex, - }, + sync::Arc, }; -use crate::hooks::window_invoke_responder; -#[cfg(shell_scope)] -use crate::ShellScopeConfig; -use crate::{api::ipc::CallbackFn, App, Builder, Context, InvokePayload, Manager, Pattern, Window}; +use crate::{ + ipc::{CallbackFn, InvokeResponse}, + window::InvokeRequest, + App, Builder, Context, Pattern, Window, +}; use tauri_utils::{ assets::{AssetKey, Assets, CspHash}, - config::{CliConfig, Config, PatternKind, TauriConfig}, + config::{Config, PatternKind, TauriConfig}, }; #[derive(Eq, PartialEq)] @@ -95,8 +91,6 @@ impl Hash for IpcKey { } } -struct Ipc(Mutex>>>); - /// An empty [`Assets`] implementation. pub struct NoopAsset { csp_hashes: Vec>, @@ -127,20 +121,10 @@ pub fn mock_context(assets: A) -> crate::Context { package: Default::default(), tauri: TauriConfig { pattern: PatternKind::Brownfield, - windows: vec![Default::default()], - cli: Some(CliConfig { - description: None, - long_description: None, - before_help: None, - after_help: None, - args: None, - subcommands: None, - }), + windows: Vec::new(), bundle: Default::default(), - allowlist: Default::default(), security: Default::default(), - updater: Default::default(), - system_tray: None, + tray_icon: None, macos_private_api: false, }, build: Default::default(), @@ -149,20 +133,17 @@ pub fn mock_context(assets: A) -> crate::Context { assets: Arc::new(assets), default_window_icon: None, app_icon: None, - system_tray_icon: None, + #[cfg(all(desktop, feature = "tray-icon"))] + tray_icon: None, package_info: crate::PackageInfo { name: "test".into(), version: "0.1.0".parse().unwrap(), authors: "Tauri", description: "Tauri test", + crate_name: "test", }, _info_plist: (), pattern: Pattern::Brownfield(std::marker::PhantomData), - #[cfg(shell_scope)] - shell_scope: ShellScopeConfig { - open: None, - scopes: HashMap::new(), - }, } } @@ -182,20 +163,7 @@ pub fn mock_context(assets: A) -> crate::Context { /// } /// ``` pub fn mock_builder() -> Builder { - let mut builder = Builder::::new().manage(Ipc(Default::default())); - - builder.invoke_responder = Arc::new(|window, response, callback, error| { - let window_ = window.clone(); - let ipc = window_.state::(); - let mut ipc_ = ipc.0.lock().unwrap(); - if let Some(tx) = ipc_.remove(&IpcKey { callback, error }) { - tx.send(response.into_result()).unwrap(); - } else { - window_invoke_responder(window, response, callback, error) - } - }); - - builder + Builder::::new().enable_macos_default_menu(false) } /// Creates a new [`App`] for testing using the [`mock_context`] with a [`noop_assets`]. @@ -238,12 +206,12 @@ pub fn mock_app() -> App { /// // run the `ping` command and assert it returns `pong` /// tauri::test::assert_ipc_response( /// &window, -/// tauri::InvokePayload { +/// tauri::window::InvokeRequest { /// cmd: "ping".into(), -/// tauri_module: None, -/// callback: tauri::api::ipc::CallbackFn(0), -/// error: tauri::api::ipc::CallbackFn(1), -/// inner: serde_json::Value::Null, +/// callback: tauri::ipc::CallbackFn(0), +/// error: tauri::ipc::CallbackFn(1), +/// body: serde_json::Value::Null.into(), +/// headers: Default::default(), /// }, /// // the expected response is a success with the "pong" payload /// // we could also use Err("error message") here to ensure the command failed @@ -254,37 +222,26 @@ pub fn mock_app() -> App { /// ``` pub fn assert_ipc_response( window: &Window, - payload: InvokePayload, + request: InvokeRequest, expected: Result, ) { - let callback = payload.callback; - let error = payload.error; - let ipc = window.state::(); - let (tx, rx) = channel(); - ipc.0.lock().unwrap().insert(IpcKey { callback, error }, tx); - window.clone().on_message(payload).unwrap(); + let rx = window.clone().on_message(request); + let response = rx.recv().unwrap(); assert_eq!( - rx.recv().unwrap(), + match response { + InvokeResponse::Ok(b) => Ok(b.into_json()), + InvokeResponse::Err(e) => Err(e.0), + }, expected .map(|e| serde_json::to_value(e).unwrap()) .map_err(|e| serde_json::to_value(e).unwrap()) ); } -#[cfg(test)] -pub(crate) fn mock_invoke_context() -> crate::endpoints::InvokeContext { - let app = mock_app(); - crate::endpoints::InvokeContext { - window: app.get_window("main").unwrap(), - config: app.config(), - package_info: app.package_info().clone(), - } -} - #[cfg(test)] mod tests { - use crate::Manager; + use crate::WindowBuilder; use std::time::Duration; use super::mock_app; @@ -292,7 +249,11 @@ mod tests { #[test] fn run_app() { let app = mock_app(); - let w = app.get_window("main").unwrap(); + + let w = WindowBuilder::new(&app, "main", Default::default()) + .build() + .unwrap(); + std::thread::spawn(move || { std::thread::sleep(Duration::from_secs(1)); w.close().unwrap(); diff --git a/core/tauri/src/tray.rs b/core/tauri/src/tray.rs new file mode 100644 index 00000000000..be6ea773dc5 --- /dev/null +++ b/core/tauri/src/tray.rs @@ -0,0 +1,441 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg(all(desktop, feature = "tray-icon"))] + +//! Tray icon types and utility functions + +use crate::app::{GlobalMenuEventListener, GlobalTrayIconEventListener}; +use crate::menu::ContextMenu; +use crate::menu::MenuEvent; +use crate::{run_main_thread, AppHandle, Icon, Manager, Runtime}; +use std::path::Path; +pub use tray_icon::TrayIconId; + +// TODO(muda-migration): figure out js events + +/// Describes a rectangle including position (x - y axis) and size. +#[derive(Debug, PartialEq, Clone, Copy, Default)] +pub struct Rectangle { + /// The x-coordinate of the upper-left corner of the rectangle. + pub left: f64, + /// The y-coordinate of the upper-left corner of the rectangle. + pub top: f64, + /// The x-coordinate of the lower-right corner of the rectangle. + pub right: f64, + /// The y-coordinate of the lower-right corner of the rectangle. + pub bottom: f64, +} + +/// Describes the click type that triggered this tray icon event. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ClickType { + /// Left mouse click. + Left, + /// Right mouse click. + Right, + /// Double left mouse click. + Double, +} + +impl Default for ClickType { + fn default() -> Self { + Self::Left + } +} + +/// Describes a tray event emitted when a tray icon is clicked +/// +/// ## Platform-specific: +/// +/// - **Linux**: Unsupported. The event is not emmited even though the icon is shown, +/// the icon will still show a context menu on right click. +#[derive(Debug, Clone, Default)] +pub struct TrayIconEvent { + /// Id of the tray icon which triggered this event. + pub id: TrayIconId, + /// Physical X Position of the click the triggered this event. + pub x: f64, + /// Physical Y Position of the click the triggered this event. + pub y: f64, + /// Position and size of the tray icon + pub icon_rect: Rectangle, + /// The click type that triggered this event. + pub click_type: ClickType, +} + +impl TrayIconEvent { + /// Returns the id of the tray icon which triggered this event. + pub fn id(&self) -> &TrayIconId { + &self.id + } +} + +impl From for Rectangle { + fn from(value: tray_icon::Rectangle) -> Self { + Self { + bottom: value.bottom, + left: value.left, + top: value.top, + right: value.right, + } + } +} + +impl From for ClickType { + fn from(value: tray_icon::ClickType) -> Self { + match value { + tray_icon::ClickType::Left => Self::Left, + tray_icon::ClickType::Right => Self::Right, + tray_icon::ClickType::Double => Self::Double, + } + } +} + +impl From for TrayIconEvent { + fn from(value: tray_icon::TrayIconEvent) -> Self { + Self { + id: value.id, + x: value.x, + y: value.y, + icon_rect: value.icon_rect.into(), + click_type: value.click_type.into(), + } + } +} + +/// [`TrayIcon`] builder struct and associated methods. +#[derive(Default)] +pub struct TrayIconBuilder { + on_menu_event: Option>>, + on_tray_event: Option>>, + inner: tray_icon::TrayIconBuilder, +} + +impl TrayIconBuilder { + /// Creates a new tray icon builder. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. + /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + pub fn new() -> Self { + Self { + inner: tray_icon::TrayIconBuilder::new(), + on_menu_event: None, + on_tray_event: None, + } + } + + /// Creates a new tray icon builder with the specified id. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. + /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + pub fn with_id>(id: I) -> Self { + let mut builder = Self::new(); + builder.inner = builder.inner.with_id(id); + builder + } + + /// Set the a menu for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux**: once a menu is set, it cannot be removed or replaced but you can change its content. + pub fn menu(mut self, menu: &M) -> Self { + self.inner = self.inner.with_menu(menu.inner_owned()); + self + } + + /// Set an icon for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Sometimes the icon won't be visible unless a menu is set. + /// Setting an empty [`Menu`](crate::menu::Menu) is enough. + pub fn icon(mut self, icon: Icon) -> Self { + let icon = icon.try_into().ok(); + if let Some(icon) = icon { + self.inner = self.inner.with_icon(icon); + } + self + } + + /// Set a tooltip for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported. + pub fn tooltip>(mut self, s: S) -> Self { + self.inner = self.inner.with_tooltip(s); + self + } + + /// Set the tray icon title. + /// + /// ## Platform-specific + /// + /// - **Linux:** The title will not be shown unless there is an icon + /// as well. The title is useful for numerical and other frequently + /// updated information. In general, it shouldn't be shown unless a + /// user requests it as it can take up a significant amount of space + /// on the user's panel. This may not be shown in all visualizations. + /// - **Windows:** Unsupported. + pub fn title>(mut self, title: S) -> Self { + self.inner = self.inner.with_title(title); + self + } + + /// Set tray icon temp dir path. **Linux only**. + /// + /// On Linux, we need to write the icon to the disk and usually it will + /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + pub fn temp_dir_path>(mut self, s: P) -> Self { + self.inner = self.inner.with_temp_dir_path(s); + self + } + + /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. + pub fn icon_as_template(mut self, is_template: bool) -> Self { + self.inner = self.inner.with_icon_as_template(is_template); + self + } + + /// Whether to show the tray menu on left click or not, default is `true`. **macOS only**. + pub fn menu_on_left_click(mut self, enable: bool) -> Self { + self.inner = self.inner.with_menu_on_left_click(enable); + self + } + + /// Set a handler for menu events. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + pub fn on_menu_event, MenuEvent) + Sync + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.on_menu_event.replace(Box::new(f)); + self + } + + /// Set a handler for this tray icon events. + pub fn on_tray_event, TrayIconEvent) + Sync + Send + 'static>( + mut self, + f: F, + ) -> Self { + self.on_tray_event.replace(Box::new(f)); + self + } + + /// Access the unique id that will be assigned to the tray icon + /// this builder will create. + pub fn id(&self) -> &TrayIconId { + self.inner.id() + } + + /// Builds and adds a new [`TrayIcon`] to the system tray. + pub fn build>(self, manager: &M) -> crate::Result> { + let id = self.id().clone(); + let inner = self.inner.build()?; + let icon = TrayIcon { + id, + inner, + app_handle: manager.app_handle().clone(), + }; + + icon.register(&icon.app_handle, self.on_menu_event, self.on_tray_event); + + Ok(icon) + } +} + +/// Tray icon struct and associated methods. +/// +/// This type is reference-counted and the icon is removed when the last instance is dropped. +/// +/// See [TrayIconBuilder] to construct this type. +pub struct TrayIcon { + id: TrayIconId, + inner: tray_icon::TrayIcon, + app_handle: AppHandle, +} + +impl Clone for TrayIcon { + fn clone(&self) -> Self { + Self { + id: self.id.clone(), + inner: self.inner.clone(), + app_handle: self.app_handle.clone(), + } + } +} + +/// # Safety +/// +/// We make sure it always runs on the main thread. +unsafe impl Sync for TrayIcon {} +unsafe impl Send for TrayIcon {} + +impl TrayIcon { + fn register( + &self, + app_handle: &AppHandle, + on_menu_event: Option>>, + on_tray_event: Option>>, + ) { + if let Some(handler) = on_menu_event { + app_handle + .manager + .inner + .menu_event_listeners + .lock() + .unwrap() + .push(handler); + } + + if let Some(handler) = on_tray_event { + app_handle + .manager + .inner + .tray_event_listeners + .lock() + .unwrap() + .insert(self.id.clone(), handler); + } + + app_handle + .manager + .inner + .tray_icons + .lock() + .unwrap() + .push(self.clone()); + } + + /// The application handle associated with this type. + pub fn app_handle(&self) -> &AppHandle { + &self.app_handle + } + + /// Register a handler for menu events. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + pub fn on_menu_event, MenuEvent) + Sync + Send + 'static>(&self, f: F) { + self + .app_handle + .manager + .inner + .menu_event_listeners + .lock() + .unwrap() + .push(Box::new(f)); + } + + /// Register a handler for this tray icon events. + pub fn on_tray_event, TrayIconEvent) + Sync + Send + 'static>(&self, f: F) { + self + .app_handle + .manager + .inner + .tray_event_listeners + .lock() + .unwrap() + .insert(self.id.clone(), Box::new(f)); + } + + /// Returns the id associated with this tray icon. + pub fn id(&self) -> &TrayIconId { + &self.id + } + + /// Set new tray icon. If `None` is provided, it will remove the icon. + pub fn set_icon(&self, icon: Option) -> crate::Result<()> { + let icon = icon.and_then(|i| i.try_into().ok()); + run_main_thread!(self, |self_: Self| self_.inner.set_icon(icon))?.map_err(Into::into) + } + + /// Set new tray menu. + /// + /// ## Platform-specific: + /// + /// - **Linux**: once a menu is set it cannot be removed so `None` has no effect + pub fn set_menu(&self, menu: Option) -> crate::Result<()> { + run_main_thread!(self, |self_: Self| self_ + .inner + .set_menu(menu.map(|m| m.inner_owned()))) + } + + /// Sets the tooltip for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** Unsupported + pub fn set_tooltip>(&self, tooltip: Option) -> crate::Result<()> { + let s = tooltip.map(|s| s.as_ref().to_string()); + run_main_thread!(self, |self_: Self| self_.inner.set_tooltip(s))?.map_err(Into::into) + } + + /// Sets the tooltip for this tray icon. + /// + /// ## Platform-specific: + /// + /// - **Linux:** The title will not be shown unless there is an icon + /// as well. The title is useful for numerical and other frequently + /// updated information. In general, it shouldn't be shown unless a + /// user requests it as it can take up a significant amount of space + /// on the user's panel. This may not be shown in all visualizations. + /// - **Windows:** Unsupported + pub fn set_title>(&self, title: Option) -> crate::Result<()> { + let s = title.map(|s| s.as_ref().to_string()); + run_main_thread!(self, |self_: Self| self_.inner.set_title(s)) + } + + /// Show or hide this tray icon + pub fn set_visible(&self, visible: bool) -> crate::Result<()> { + run_main_thread!(self, |self_: Self| self_.inner.set_visible(visible))?.map_err(Into::into) + } + + /// Sets the tray icon temp dir path. **Linux only**. + /// + /// On Linux, we need to write the icon to the disk and usually it will + /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`. + pub fn set_temp_dir_path>(&self, path: Option

) -> crate::Result<()> { + #[allow(unused)] + let p = path.map(|p| p.as_ref().to_path_buf()); + #[cfg(target_os = "linux")] + run_main_thread!(self, |self_: Self| self_.inner.set_temp_dir_path(p))?; + Ok(()) + } + + /// Set the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**. + pub fn set_icon_as_template(&self, #[allow(unused)] is_template: bool) -> crate::Result<()> { + #[cfg(target_os = "macos")] + run_main_thread!(self, |self_: Self| self_ + .inner + .set_icon_as_template(is_template))?; + Ok(()) + } + + /// Disable or enable showing the tray menu on left click. **macOS only**. + pub fn set_show_menu_on_left_click(&self, #[allow(unused)] enable: bool) -> crate::Result<()> { + #[cfg(target_os = "macos")] + run_main_thread!(self, |self_: Self| self_ + .inner + .set_show_menu_on_left_click(enable))?; + Ok(()) + } +} + +impl TryFrom for tray_icon::Icon { + type Error = crate::Error; + + fn try_from(value: Icon) -> Result { + let value: crate::runtime::Icon = value.try_into()?; + tray_icon::Icon::from_rgba(value.rgba, value.width, value.height).map_err(Into::into) + } +} diff --git a/core/tauri/src/updater/core.rs b/core/tauri/src/updater/core.rs deleted file mode 100644 index 4aa42e49fd3..00000000000 --- a/core/tauri/src/updater/core.rs +++ /dev/null @@ -1,1650 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use super::error::{Error, Result}; -#[cfg(desktop)] -use crate::api::file::{ArchiveFormat, Extract, Move}; -use crate::{ - api::http::{ClientBuilder, HttpRequestBuilder}, - AppHandle, Manager, Runtime, -}; -use base64::Engine; -use http::{ - header::{HeaderName, HeaderValue}, - HeaderMap, StatusCode, -}; -use minisign_verify::{PublicKey, Signature}; -use semver::Version; -use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize}; -use tauri_utils::{platform::current_exe, Env}; -use time::OffsetDateTime; -use url::Url; - -#[cfg(desktop)] -use std::io::Seek; -use std::{ - collections::HashMap, - env, - fmt::{self}, - io::{Cursor, Read}, - path::{Path, PathBuf}, - str::{from_utf8, FromStr}, - time::Duration, -}; - -#[cfg(any(target_os = "linux", windows))] -use std::ffi::OsStr; - -#[cfg(all(desktop, not(target_os = "windows")))] -use crate::api::file::Compression; - -#[cfg(target_os = "windows")] -use std::{ - fs::read_dir, - process::{exit, Command}, -}; - -type ShouldInstall = dyn FnOnce(&Version, &RemoteRelease) -> bool + Send; - -#[derive(Debug, Deserialize, Serialize)] -#[serde(untagged)] -pub enum RemoteReleaseInner { - Dynamic(ReleaseManifestPlatform), - Static { - platforms: HashMap, - }, -} - -/// Information about a release returned by the remote update server. -/// -/// This type can have one of two shapes: Server Format (Dynamic Format) and Static Format. -#[derive(Debug)] -pub struct RemoteRelease { - /// Version to install. - version: Version, - /// Release notes. - notes: Option, - /// Release date. - pub_date: Option, - /// Release data. - data: RemoteReleaseInner, -} - -impl<'de> Deserialize<'de> for RemoteRelease { - fn deserialize(deserializer: D) -> std::result::Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - struct InnerRemoteRelease { - #[serde(alias = "name", deserialize_with = "parse_version")] - version: Version, - notes: Option, - pub_date: Option, - platforms: Option>, - // dynamic platform response - url: Option, - signature: Option, - #[cfg(target_os = "windows")] - #[serde(default)] - with_elevated_task: bool, - } - - let release = InnerRemoteRelease::deserialize(deserializer)?; - - 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}")))?, - ) - } else { - None - }; - - Ok(RemoteRelease { - version: release.version, - notes: release.notes, - pub_date, - data: if let Some(platforms) = release.platforms { - RemoteReleaseInner::Static { platforms } - } else { - RemoteReleaseInner::Dynamic(ReleaseManifestPlatform { - url: release.url.ok_or_else(|| { - DeError::custom("the `url` field was not set on the updater response") - })?, - signature: release.signature.ok_or_else(|| { - DeError::custom("the `signature` field was not set on the updater response") - })?, - #[cfg(target_os = "windows")] - with_elevated_task: release.with_elevated_task, - }) - }, - }) - } -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct ReleaseManifestPlatform { - /// Download URL for the platform - pub url: Url, - /// Signature for the platform - pub signature: String, - #[cfg(target_os = "windows")] - #[serde(default)] - /// Optional: Windows only try to use elevated task - pub with_elevated_task: bool, -} - -fn parse_version<'de, D>(deserializer: D) -> std::result::Result -where - D: serde::Deserializer<'de>, -{ - let str = String::deserialize(deserializer)?; - - Version::from_str(str.trim_start_matches('v')).map_err(serde::de::Error::custom) -} - -impl RemoteRelease { - /// The release version. - pub fn version(&self) -> &Version { - &self.version - } - - /// The release notes. - pub fn notes(&self) -> Option<&String> { - self.notes.as_ref() - } - - /// The release date. - pub fn pub_date(&self) -> Option<&OffsetDateTime> { - self.pub_date.as_ref() - } - - /// The release's download URL for the given target. - pub fn download_url(&self, target: &str) -> Result<&Url> { - match self.data { - RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.url), - RemoteReleaseInner::Static { ref platforms } => platforms - .get(target) - .map_or(Err(Error::TargetNotFound(target.to_string())), |p| { - Ok(&p.url) - }), - } - } - - /// The release's signature for the given target. - pub fn signature(&self, target: &str) -> Result<&String> { - match self.data { - RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.signature), - RemoteReleaseInner::Static { ref platforms } => platforms - .get(target) - .map_or(Err(Error::TargetNotFound(target.to_string())), |platform| { - Ok(&platform.signature) - }), - } - } - - #[cfg(target_os = "windows")] - /// Optional: Windows only try to use elevated task - pub fn with_elevated_task(&self, target: &str) -> Result { - match self.data { - RemoteReleaseInner::Dynamic(ref platform) => Ok(platform.with_elevated_task), - RemoteReleaseInner::Static { ref platforms } => platforms - .get(target) - .map_or(Err(Error::TargetNotFound(target.to_string())), |platform| { - Ok(platform.with_elevated_task) - }), - } - } -} - -pub struct UpdateBuilder { - /// Application handle. - pub app: AppHandle, - /// Current version we are running to compare with announced version - pub current_version: Version, - /// The URLs to checks updates. We suggest at least one fallback on a different domain. - pub urls: Vec, - /// The platform the updater will check and install the update. Default is from `get_updater_target` - pub target: Option, - /// The current executable path. Default is automatically extracted. - pub executable_path: Option, - should_install: Option>, - timeout: Option, - headers: HeaderMap, -} - -impl fmt::Debug for UpdateBuilder { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("UpdateBuilder") - .field("app", &self.app) - .field("current_version", &self.current_version) - .field("urls", &self.urls) - .field("target", &self.target) - .field("executable_path", &self.executable_path) - .field("timeout", &self.timeout) - .field("headers", &self.headers) - .finish() - } -} - -// Create new updater instance and return an Update -impl UpdateBuilder { - pub fn new(app: AppHandle) -> Self { - UpdateBuilder { - app, - urls: Vec::new(), - target: None, - executable_path: None, - // safe to unwrap: CARGO_PKG_VERSION is also a valid semver value - current_version: env!("CARGO_PKG_VERSION").parse().unwrap(), - should_install: None, - timeout: None, - headers: Default::default(), - } - } - - #[allow(dead_code)] - pub fn url(mut self, url: String) -> Self { - self.urls.push( - percent_encoding::percent_decode(url.as_bytes()) - .decode_utf8_lossy() - .to_string(), - ); - self - } - - /// Add multiple URLS at once inside a Vec for future reference - pub fn urls(mut self, urls: &[String]) -> Self { - let mut formatted_vec: Vec = Vec::new(); - for url in urls { - formatted_vec.push( - percent_encoding::percent_decode(url.as_bytes()) - .decode_utf8_lossy() - .to_string(), - ); - } - self.urls = formatted_vec; - self - } - - /// Set the current app version, used to compare against the latest available version. - /// The `cargo_crate_version!` macro can be used to pull the version from your `Cargo.toml` - pub fn current_version(mut self, ver: Version) -> Self { - self.current_version = ver; - self - } - - /// Set the target name. Represents the string that is looked up on the updater API or response JSON. - pub fn target(mut self, target: impl Into) -> Self { - self.target.replace(target.into()); - self - } - - /// Set the executable path - #[allow(dead_code)] - pub fn executable_path>(mut self, executable_path: A) -> Self { - self.executable_path = Some(PathBuf::from(executable_path.as_ref())); - self - } - - pub fn should_install bool + Send + 'static>( - mut self, - f: F, - ) -> Self { - self.should_install.replace(Box::new(f)); - self - } - - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout.replace(timeout); - self - } - - /// Add a `Header` to the request. - pub fn header(mut self, key: K, value: V) -> Result - where - HeaderName: TryFrom, - >::Error: Into, - HeaderValue: TryFrom, - >::Error: Into, - { - let key: std::result::Result = key.try_into().map_err(Into::into); - let value: std::result::Result = value.try_into().map_err(Into::into); - self.headers.insert(key?, value?); - Ok(self) - } - - pub async fn build(mut self) -> Result> { - let mut remote_release: Option = None; - - // make sure we have at least one url - if self.urls.is_empty() { - return Err(Error::Builder( - "Unable to check update, `url` is required.".into(), - )); - }; - - // If no executable path provided, we use current_exe from tauri_utils - let executable_path = self.executable_path.unwrap_or(current_exe()?); - - let arch = get_updater_arch().ok_or(Error::UnsupportedArch)?; - // `target` is the `{{target}}` variable we replace in the endpoint - // `json_target` is the value we search if the updater server returns a JSON with the `platforms` object - let (target, json_target) = if let Some(target) = self.target { - (target.clone(), target) - } else { - let target = get_updater_target().ok_or(Error::UnsupportedOs)?; - (target.to_string(), format!("{target}-{arch}")) - }; - - // Get the extract_path from the provided executable_path - let extract_path = extract_path_from_executable(&self.app.state::(), &executable_path); - - // Set SSL certs for linux if they aren't available. - // We do not require to recheck in the download_and_install as we use - // ENV variables, we can expect them to be set for the second call. - #[cfg(target_os = "linux")] - { - if env::var_os("SSL_CERT_FILE").is_none() { - env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt"); - } - if env::var_os("SSL_CERT_DIR").is_none() { - env::set_var("SSL_CERT_DIR", "/etc/ssl/certs"); - } - } - - // we want JSON only - let mut headers = self.headers; - headers.insert("Accept", HeaderValue::from_str("application/json").unwrap()); - - // Allow fallback if more than 1 urls is provided - let mut last_error: Option = None; - for url in &self.urls { - // replace {{current_version}}, {{target}} and {{arch}} in the provided URL - // this is useful if we need to query example - // https://releases.myapp.com/update/{{target}}/{{arch}}/{{current_version}} - // will be translated into -> - // https://releases.myapp.com/update/darwin/aarch64/1.0.0 - // The main objective is if the update URL is defined via the Cargo.toml - // the URL will be generated dynamically - let fixed_link = url - .replace("{{current_version}}", &self.current_version.to_string()) - .replace("{{target}}", &target) - .replace("{{arch}}", arch); - - let mut request = HttpRequestBuilder::new("GET", &fixed_link)?.headers(headers.clone()); - if let Some(timeout) = self.timeout { - request = request.timeout(timeout); - } - let resp = ClientBuilder::new().build()?.send(request).await; - - // If we got a success, we stop the loop - // and we set our remote_release variable - if let Ok(res) = resp { - let status = res.status(); - // got status code 2XX - if status.is_success() { - // if we got 204 - if status == StatusCode::NO_CONTENT { - // return with `UpToDate` error - // we should catch on the client - return Err(Error::UpToDate); - }; - let res = res.read().await?; - // Convert the remote result to our local struct - let built_release = serde_json::from_value(res.data).map_err(Into::into); - // make sure all went well and the remote data is compatible - // with what we need locally - match built_release { - Ok(release) => { - last_error = None; - remote_release = Some(release); - break; - } - Err(err) => last_error = Some(err), - } - } // if status code is not 2XX we keep loopin' our urls - } - } - - // Last error is cleaned on success -- shouldn't be triggered if - // we have a successful call - if let Some(error) = last_error { - return Err(error); - } - - // Extracted remote metadata - let final_release = remote_release.ok_or(Error::ReleaseNotFound)?; - - // is the announced version greater than our current one? - let should_update = if let Some(comparator) = self.should_install.take() { - comparator(&self.current_version, &final_release) - } else { - final_release.version() > &self.current_version - }; - - headers.remove("Accept"); - - // create our new updater - Ok(Update { - app: self.app, - target, - extract_path, - should_update, - version: final_release.version().to_string(), - date: final_release.pub_date().cloned(), - current_version: self.current_version, - download_url: final_release.download_url(&json_target)?.to_owned(), - body: final_release.notes().cloned(), - signature: final_release.signature(&json_target)?.to_owned(), - #[cfg(target_os = "windows")] - with_elevated_task: final_release.with_elevated_task(&json_target)?, - timeout: self.timeout, - headers, - }) - } -} - -pub fn builder(app: AppHandle) -> UpdateBuilder { - UpdateBuilder::new(app) -} - -#[derive(Debug)] -pub struct Update { - /// Application handle. - pub app: AppHandle, - /// Update description - pub body: Option, - /// Should we update or not - pub should_update: bool, - /// Version announced - pub version: String, - /// Running version - pub current_version: Version, - /// Update publish date - pub date: Option, - /// Target - #[allow(dead_code)] - target: String, - /// Extract path - extract_path: PathBuf, - /// Download URL announced - download_url: Url, - /// Signature announced - signature: String, - #[cfg(target_os = "windows")] - /// Optional: Windows only try to use elevated task - /// Default to false - with_elevated_task: bool, - /// Request timeout - timeout: Option, - /// Request headers - headers: HeaderMap, -} - -impl Clone for Update { - fn clone(&self) -> Self { - Self { - app: self.app.clone(), - body: self.body.clone(), - should_update: self.should_update, - version: self.version.clone(), - current_version: self.current_version.clone(), - date: self.date, - target: self.target.clone(), - extract_path: self.extract_path.clone(), - download_url: self.download_url.clone(), - signature: self.signature.clone(), - #[cfg(target_os = "windows")] - with_elevated_task: self.with_elevated_task, - timeout: self.timeout, - headers: self.headers.clone(), - } - } -} - -impl Update { - // Download and install our update - // @todo(lemarier): Split into download and install (two step) but need to be thread safe - pub(crate) async fn download_and_install), D: FnOnce()>( - &self, - pub_key: String, - on_chunk: C, - on_download_finish: D, - ) -> Result { - // make sure we can install the update on linux - // We fail here because later we can add more linux support - // actually if we use APPIMAGE, our extract path should already - // be set with our APPIMAGE env variable, we don't need to do - // anything with it yet - #[cfg(target_os = "linux")] - if self.app.state::().appimage.is_none() { - return Err(Error::UnsupportedLinuxPackage); - } - - // set our headers - let mut headers = self.headers.clone(); - headers.insert( - "Accept", - HeaderValue::from_str("application/octet-stream").unwrap(), - ); - headers.insert( - "User-Agent", - HeaderValue::from_str("tauri/updater").unwrap(), - ); - - let client = ClientBuilder::new().build()?; - // Create our request - let mut req = HttpRequestBuilder::new("GET", self.download_url.as_str())?.headers(headers); - if let Some(timeout) = self.timeout { - req = req.timeout(timeout); - } - - let response = client.send(req).await?; - - // make sure it's success - if !response.status().is_success() { - return Err(Error::Network(format!( - "Download request failed with status: {}", - response.status() - ))); - } - - let content_length: Option = response - .headers() - .get("Content-Length") - .and_then(|value| value.to_str().ok()) - .and_then(|value| value.parse().ok()); - - let mut buffer = Vec::new(); - { - use futures_util::StreamExt; - let mut stream = response.bytes_stream(); - while let Some(chunk) = stream.next().await { - let chunk = chunk?; - let bytes = chunk.as_ref().to_vec(); - on_chunk(bytes.len(), content_length); - buffer.extend(bytes); - } - } - - on_download_finish(); - - // create memory buffer from our archive (Seek + Read) - let mut archive_buffer = Cursor::new(buffer); - - // We need an announced signature by the server - // if there is no signature, bail out. - verify_signature(&mut archive_buffer, &self.signature, &pub_key)?; - - // TODO: implement updater in mobile - #[cfg(desktop)] - { - // we copy the files depending of the operating system - // we run the setup, appimage re-install or overwrite the - // macos .app - #[cfg(target_os = "windows")] - copy_files_and_run( - archive_buffer, - &self.extract_path, - self.with_elevated_task, - &self.app.config(), - )?; - #[cfg(not(target_os = "windows"))] - copy_files_and_run(archive_buffer, &self.extract_path)?; - } - - // We are done! - Ok(()) - } -} - -// Linux (AppImage) - -// ### Expected structure: -// ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by tauri-bundler -// │ └──[AppName]_[version]_amd64.AppImage # Application AppImage -// └── ... - -// We should have an AppImage already installed to be able to copy and install -// the extract_path is the current AppImage path -// tmp_dir is where our new AppImage is found -#[cfg(target_os = "linux")] -fn copy_files_and_run(archive_buffer: R, extract_path: &Path) -> Result { - use std::os::unix::fs::{MetadataExt, PermissionsExt}; - - let extract_path_metadata = extract_path.metadata()?; - - let tmp_dir_locations = vec![ - Box::new(|| Some(env::temp_dir())) as Box Option>, - Box::new(dirs_next::cache_dir), - Box::new(|| Some(extract_path.parent().unwrap().to_path_buf())), - ]; - - for tmp_dir_location in tmp_dir_locations { - if let Some(tmp_dir_location) = tmp_dir_location() { - let tmp_dir = tempfile::Builder::new() - .prefix("tauri_current_app") - .tempdir_in(tmp_dir_location)?; - let tmp_dir_metadata = tmp_dir.path().metadata()?; - - if extract_path_metadata.dev() == tmp_dir_metadata.dev() { - let mut perms = tmp_dir_metadata.permissions(); - perms.set_mode(0o700); - std::fs::set_permissions(tmp_dir.path(), perms)?; - - let tmp_app_image = &tmp_dir.path().join("current_app.AppImage"); - - // create a backup of our current app image - Move::from_source(extract_path).to_dest(tmp_app_image)?; - - // extract the buffer to the tmp_dir - // we extract our signed archive into our final directory without any temp file - let mut extractor = - Extract::from_cursor(archive_buffer, ArchiveFormat::Tar(Some(Compression::Gz))); - - return extractor - .with_files(|entry| { - let path = entry.path()?; - if path.extension() == Some(OsStr::new("AppImage")) { - // if something went wrong during the extraction, we should restore previous app - if let Err(err) = entry.extract(extract_path) { - Move::from_source(tmp_app_image).to_dest(extract_path)?; - return Err(crate::api::Error::Extract(err.to_string())); - } - // early finish we have everything we need here - return Ok(true); - } - Ok(false) - }) - .map_err(Into::into); - } - } - } - - Err(Error::TempDirNotOnSameMountPoint) -} - -// 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")] -#[allow(clippy::unnecessary_wraps)] -fn copy_files_and_run( - archive_buffer: R, - _extract_path: &Path, - with_elevated_task: bool, - 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) - // - // The tricky part is the MSI need to be exposed and spawned so the memory allocation - // shouldn't drop but we should be able to pass the reference so we can drop it once the installation - // is done, otherwise we have a huge memory leak. - - let tmp_dir = tempfile::Builder::new().tempdir()?.into_path(); - - // extract the buffer to the tmp_dir - // we extract our signed archive into our final directory without any temp file - let mut extractor = Extract::from_cursor(archive_buffer, ArchiveFormat::Zip); - - // extract the msi - extractor.extract_into(&tmp_dir)?; - - let paths = read_dir(&tmp_dir)?; - - for path in paths { - let found_path = path?.path(); - // we support 2 type of files exe & msi for now - // 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) - .args(config.tauri.updater.windows.install_mode.nsis_args()) - .args(&config.tauri.updater.windows.installer_args) - .spawn() - .expect("installer failed to start"); - - exit(0); - } else if found_path.extension() == Some(OsStr::new("msi")) { - if with_elevated_task { - if let Some(bin_name) = current_exe() - .ok() - .and_then(|pb| pb.file_name().map(|s| s.to_os_string())) - .and_then(|s| s.into_string().ok()) - { - let product_name = bin_name.replace(".exe", ""); - - // Check if there is a task that enables the updater to skip the UAC prompt - let update_task_name = format!("Update {product_name} - Skip UAC"); - if let Ok(output) = Command::new("schtasks") - .arg("/QUERY") - .arg("/TN") - .arg(update_task_name.clone()) - .output() - { - if output.status.success() { - // Rename the MSI to the match file name the Skip UAC task is expecting it to be - let temp_msi = tmp_dir.with_file_name(bin_name).with_extension("msi"); - Move::from_source(&found_path) - .to_dest(&temp_msi) - .expect("Unable to move update MSI"); - let exit_status = Command::new("schtasks") - .arg("/RUN") - .arg("/TN") - .arg(update_task_name) - .status() - .expect("failed to start updater task"); - - if exit_status.success() { - // Successfully launched task that skips the UAC prompt - exit(0); - } - } - // Failed to run update task. Following UAC Path - } - } - } - - // we need to wrap the current exe path in quotes for Start-Process - let mut current_exe_arg = std::ffi::OsString::new(); - current_exe_arg.push("\""); - current_exe_arg.push(current_exe()?); - current_exe_arg.push("\""); - - let mut msi_path_arg = std::ffi::OsString::new(); - msi_path_arg.push("\"\"\""); - msi_path_arg.push(&found_path); - msi_path_arg.push("\"\"\""); - - let mut msiexec_args = config - .tauri - .updater - .windows - .install_mode - .msiexec_args() - .iter() - .map(|p| p.to_string()) - .collect::>(); - 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( - |_| "powershell.exe".to_string(), - |p| format!("{p}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"), - ); - let powershell_install_res = Command::new(powershell_path) - .args(["-NoProfile", "-windowstyle", "hidden"]) - .args([ - "Start-Process", - "-Wait", - "-FilePath", - "$env:SYSTEMROOT\\System32\\msiexec.exe", - "-ArgumentList", - ]) - .arg("/i,") - .arg(msi_path_arg) - .arg(format!(", {}, /promptrestart;", msiexec_args.join(", "))) - .arg("Start-Process") - .arg(current_exe_arg) - .spawn(); - if powershell_install_res.is_err() { - // fallback to running msiexec directly - relaunch won't be available - // we use this here in case powershell fails in an older machine somehow - let msiexec_path = system_root.as_ref().map_or_else( - |_| "msiexec.exe".to_string(), - |p| format!("{p}\\System32\\msiexec.exe"), - ); - let _ = Command::new(msiexec_path) - .arg("/i") - .arg(found_path) - .args(msiexec_args) - .arg("/promptrestart") - .spawn(); - } - - exit(0); - } - } - - Ok(()) -} - -// MacOS -// ### Expected structure: -// ├── [AppName]_[version]_x64.app.tar.gz # GZ generated by tauri-bundler -// │ └──[AppName].app # Main application -// │ └── Contents # Application contents... -// │ └── ... -// └── ... -#[cfg(target_os = "macos")] -fn copy_files_and_run(archive_buffer: R, extract_path: &Path) -> Result { - let mut extracted_files: Vec = Vec::new(); - - // extract the buffer to the tmp_dir - // we extract our signed archive into our final directory without any temp file - let mut extractor = - Extract::from_cursor(archive_buffer, ArchiveFormat::Tar(Some(Compression::Gz))); - // the first file in the tar.gz will always be - // /Contents - let tmp_dir = tempfile::Builder::new() - .prefix("tauri_current_app") - .tempdir()?; - - // create backup of our current app - Move::from_source(extract_path).to_dest(tmp_dir.path())?; - - // extract all the files - extractor.with_files(|entry| { - let path = entry.path()?; - // skip the first folder (should be the app name) - let collected_path: PathBuf = path.iter().skip(1).collect(); - let extraction_path = extract_path.join(collected_path); - - // if something went wrong during the extraction, we should restore previous app - if let Err(err) = entry.extract(&extraction_path) { - for file in &extracted_files { - // delete all the files we extracted - if file.is_dir() { - std::fs::remove_dir(file)?; - } else { - std::fs::remove_file(file)?; - } - } - Move::from_source(tmp_dir.path()).to_dest(extract_path)?; - return Err(crate::api::Error::Extract(err.to_string())); - } - - extracted_files.push(extraction_path); - - Ok(false) - })?; - - let _ = std::process::Command::new("touch") - .arg(extract_path) - .status(); - - Ok(()) -} - -pub(crate) fn get_updater_target() -> Option<&'static str> { - if cfg!(target_os = "linux") { - Some("linux") - } else if cfg!(target_os = "macos") { - Some("darwin") - } else if cfg!(target_os = "windows") { - Some("windows") - } else { - None - } -} - -pub(crate) fn get_updater_arch() -> Option<&'static str> { - if cfg!(target_arch = "x86") { - Some("i686") - } else if cfg!(target_arch = "x86_64") { - Some("x86_64") - } else if cfg!(target_arch = "arm") { - Some("armv7") - } else if cfg!(target_arch = "aarch64") { - Some("aarch64") - } else { - None - } -} - -/// Get the extract_path from the provided executable_path -#[allow(unused_variables)] -pub fn extract_path_from_executable(env: &Env, executable_path: &Path) -> PathBuf { - // Return the path of the current executable by default - // Example C:\Program Files\My App\ - let extract_path = executable_path - .parent() - .map(PathBuf::from) - .expect("Can't determine extract path"); - - // MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp - // We need to get /Applications/.app - // todo(lemarier): Need a better way here - // Maybe we could search for <*.app> to get the right path - #[cfg(target_os = "macos")] - if extract_path - .display() - .to_string() - .contains("Contents/MacOS") - { - return extract_path - .parent() - .map(PathBuf::from) - .expect("Unable to find the extract path") - .parent() - .map(PathBuf::from) - .expect("Unable to find the extract path"); - } - - // We should use APPIMAGE exposed env variable - // This is where our APPIMAGE should sit and should be replaced - #[cfg(target_os = "linux")] - if let Some(app_image_path) = &env.appimage { - return PathBuf::from(app_image_path); - } - - extract_path -} - -// Convert base64 to string and prevent failing -fn base64_to_string(base64_string: &str) -> Result { - let decoded_string = &base64::engine::general_purpose::STANDARD.decode(base64_string)?; - let result = from_utf8(decoded_string) - .map_err(|_| Error::SignatureUtf8(base64_string.into()))? - .to_string(); - Ok(result) -} - -// Validate signature -// need to be public because its been used -// by our tests in the bundler -// -// NOTE: The buffer position is not reset. -pub fn verify_signature( - archive_reader: &mut R, - release_signature: &str, - pub_key: &str, -) -> Result -where - R: Read, -{ - // we need to convert the pub key - let pub_key_decoded = base64_to_string(pub_key)?; - let public_key = PublicKey::decode(&pub_key_decoded)?; - let signature_base64_decoded = base64_to_string(release_signature)?; - let signature = Signature::decode(&signature_base64_decoded)?; - - // read all bytes until EOF in the buffer - let mut data = Vec::new(); - archive_reader.read_to_end(&mut data)?; - - // Validate signature or bail out - public_key.verify(&data, &signature, true)?; - Ok(true) -} - -#[cfg(test)] -mod test { - use super::*; - #[cfg(target_os = "macos")] - use std::fs::File; - - macro_rules! block { - ($e:expr) => { - tokio_test::block_on($e) - }; - } - - fn generate_sample_raw_json() -> String { - r#"{ - "version": "v2.0.0", - "notes": "Test version !", - "pub_date": "2020-06-22T19:25:57Z", - "platforms": { - "darwin-aarch64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJZVGdpKzJmRWZ0SkRvWS9TdFpqTU9xcm1mUmJSSG5OWVlwSklrWkN1SFpWbmh4SDlBcTU3SXpjbm0xMmRjRkphbkpVeGhGcTdrdzlrWGpGVWZQSWdzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1MDU3CWZpbGU6L1VzZXJzL3J1bm5lci9ydW5uZXJzLzIuMjYzLjAvd29yay90YXVyaS90YXVyaS90YXVyaS9leGFtcGxlcy9jb21tdW5pY2F0aW9uL3NyYy10YXVyaS90YXJnZXQvZGVidWcvYnVuZGxlL29zeC9hcHAuYXBwLnRhci5negp4ZHFlUkJTVnpGUXdDdEhydTE5TGgvRlVPeVhjTnM5RHdmaGx3c0ZPWjZXWnFwVDRNWEFSbUJTZ1ZkU1IwckJGdmlwSzJPd00zZEZFN2hJOFUvL1FDZz09Cg==", - "url": "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.app.tar.gz" - }, - "darwin-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJZVGdpKzJmRWZ0SkRvWS9TdFpqTU9xcm1mUmJSSG5OWVlwSklrWkN1SFpWbmh4SDlBcTU3SXpjbm0xMmRjRkphbkpVeGhGcTdrdzlrWGpGVWZQSWdzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1MDU3CWZpbGU6L1VzZXJzL3J1bm5lci9ydW5uZXJzLzIuMjYzLjAvd29yay90YXVyaS90YXVyaS90YXVyaS9leGFtcGxlcy9jb21tdW5pY2F0aW9uL3NyYy10YXVyaS90YXJnZXQvZGVidWcvYnVuZGxlL29zeC9hcHAuYXBwLnRhci5negp4ZHFlUkJTVnpGUXdDdEhydTE5TGgvRlVPeVhjTnM5RHdmaGx3c0ZPWjZXWnFwVDRNWEFSbUJTZ1ZkU1IwckJGdmlwSzJPd00zZEZFN2hJOFUvL1FDZz09Cg==", - "url": "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.app.tar.gz" - }, - "linux-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOWZSM29hTFNmUEdXMHRoOC81WDFFVVFRaXdWOUdXUUdwT0NlMldqdXkyaWVieXpoUmdZeXBJaXRqSm1YVmczNXdRL1Brc0tHb1NOTzhrL1hadFcxdmdnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE3MzQzCWZpbGU6L2hvbWUvcnVubmVyL3dvcmsvdGF1cmkvdGF1cmkvdGF1cmkvZXhhbXBsZXMvY29tbXVuaWNhdGlvbi9zcmMtdGF1cmkvdGFyZ2V0L2RlYnVnL2J1bmRsZS9hcHBpbWFnZS9hcHAuQXBwSW1hZ2UudGFyLmd6CmRUTUM2bWxnbEtTbUhOZGtERUtaZnpUMG5qbVo5TGhtZWE1SFNWMk5OOENaVEZHcnAvVW0zc1A2ajJEbWZUbU0yalRHT0FYYjJNVTVHOHdTQlYwQkF3PT0K", - "url": "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.AppImage.tar.gz" - }, - "windows-x86_64": { - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJHMWlvTzRUSlQzTHJOMm5waWpic0p0VVI2R0hUNGxhQVMxdzBPRndlbGpXQXJJakpTN0toRURtVzBkcm15R0VaNTJuS1lZRWdzMzZsWlNKUVAzZGdJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1NTIzCWZpbGU6RDpcYVx0YXVyaVx0YXVyaVx0YXVyaVxleGFtcGxlc1xjb21tdW5pY2F0aW9uXHNyYy10YXVyaVx0YXJnZXRcZGVidWdcYXBwLng2NC5tc2kuemlwCitXa1lQc3A2MCs1KzEwZnVhOGxyZ2dGMlZqbjBaVUplWEltYUdyZ255eUF6eVF1dldWZzFObStaVEQ3QU1RS1lzcjhDVU4wWFovQ1p1QjJXbW1YZUJ3PT0K", - "url": "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.x64.msi.zip" - } - } - }"#.into() - } - - fn generate_sample_platform_json( - version: &str, - public_signature: &str, - download_url: &str, - ) -> String { - format!( - r#" - {{ - "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": "{public_signature}", - "url": "{download_url}" - }} - "# - ) - } - - fn generate_sample_with_elevated_task_platform_json( - version: &str, - public_signature: &str, - download_url: &str, - with_elevated_task: bool, - ) -> String { - format!( - r#" - {{ - "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": "{public_signature}", - "url": "{download_url}", - "with_elevated_task": {with_elevated_task} - }} - "# - ) - } - - #[test] - fn simple_http_updater() { - let _m = mockito::mock("GET", "/") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(generate_sample_raw_json()) - .create(); - - let app = crate::test::mock_app(); - let check_update = block!(builder(app.handle()) - .current_version("0.0.0".parse().unwrap()) - .url(mockito::server_url()) - .build()); - - let updater = check_update.expect("Can't check update"); - - assert!(updater.should_update); - } - - #[test] - fn simple_http_updater_raw_json() { - let _m = mockito::mock("GET", "/") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(generate_sample_raw_json()) - .create(); - - let app = crate::test::mock_app(); - let check_update = block!(builder(app.handle()) - .current_version("0.0.0".parse().unwrap()) - .url(mockito::server_url()) - .build()); - - let updater = check_update.expect("Can't check update"); - - assert!(updater.should_update); - } - - #[test] - fn simple_http_updater_raw_json_windows_x86_64() { - let _m = mockito::mock("GET", "/") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(generate_sample_raw_json()) - .create(); - - let app = crate::test::mock_app(); - let check_update = block!(builder(app.handle()) - .current_version("0.0.0".parse().unwrap()) - .target("windows-x86_64") - .url(mockito::server_url()) - .build()); - - let updater = check_update.expect("Can't check update"); - - assert!(updater.should_update); - assert_eq!(updater.version, "2.0.0"); - assert_eq!(updater.signature, "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJHMWlvTzRUSlQzTHJOMm5waWpic0p0VVI2R0hUNGxhQVMxdzBPRndlbGpXQXJJakpTN0toRURtVzBkcm15R0VaNTJuS1lZRWdzMzZsWlNKUVAzZGdJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1NTIzCWZpbGU6RDpcYVx0YXVyaVx0YXVyaVx0YXVyaVxleGFtcGxlc1xjb21tdW5pY2F0aW9uXHNyYy10YXVyaVx0YXJnZXRcZGVidWdcYXBwLng2NC5tc2kuemlwCitXa1lQc3A2MCs1KzEwZnVhOGxyZ2dGMlZqbjBaVUplWEltYUdyZ255eUF6eVF1dldWZzFObStaVEQ3QU1RS1lzcjhDVU4wWFovQ1p1QjJXbW1YZUJ3PT0K"); - assert_eq!( - updater.download_url.to_string(), - "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.x64.msi.zip" - ); - } - - #[test] - fn simple_http_updater_raw_json_uptodate() { - let _m = mockito::mock("GET", "/") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(generate_sample_raw_json()) - .create(); - - let app = crate::test::mock_app(); - let check_update = block!(builder(app.handle()) - .current_version("10.0.0".parse().unwrap()) - .url(mockito::server_url()) - .build()); - - let updater = check_update.expect("Can't check update"); - - assert!(!updater.should_update); - } - - #[test] - fn simple_http_updater_without_version() { - let _m = mockito::mock("GET", "/darwin-aarch64/1.0.0") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(generate_sample_platform_json( - "2.0.0", - "SampleTauriKey", - "https://tauri.app", - )) - .create(); - - let app = crate::test::mock_app(); - let check_update = block!(builder(app.handle()) - .current_version("1.0.0".parse().unwrap()) - .url(format!( - "{}/darwin-aarch64/{{{{current_version}}}}", - mockito::server_url() - )) - .build()); - - let updater = check_update.expect("Can't check update"); - - assert!(updater.should_update); - } - - #[test] - fn simple_http_updater_percent_decode() { - let _m = mockito::mock("GET", "/darwin-aarch64/1.0.0") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(generate_sample_platform_json( - "2.0.0", - "SampleTauriKey", - "https://tauri.app", - )) - .create(); - - let app = crate::test::mock_app(); - let check_update = block!(builder(app.handle()) - .current_version("1.0.0".parse().unwrap()) - .url( - url::Url::parse(&format!( - "{}/darwin-aarch64/{{{{current_version}}}}", - mockito::server_url() - )) - .unwrap() - .to_string() - ) - .build()); - - let updater = check_update.expect("Can't check update"); - - assert!(updater.should_update); - - let app = crate::test::mock_app(); - let check_update = block!(builder(app.handle()) - .current_version("1.0.0".parse().unwrap()) - .urls(&[url::Url::parse(&format!( - "{}/darwin-aarch64/{{{{current_version}}}}", - mockito::server_url() - )) - .unwrap() - .to_string()]) - .build()); - - let updater = check_update.expect("Can't check update"); - - assert!(updater.should_update); - } - - #[test] - fn simple_http_updater_with_elevated_task() { - let _m = mockito::mock("GET", "/windows-x86_64/1.0.0") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(generate_sample_with_elevated_task_platform_json( - "2.0.0", - "SampleTauriKey", - "https://tauri.app", - true, - )) - .create(); - - let app = crate::test::mock_app(); - let check_update = block!(builder(app.handle()) - .current_version("1.0.0".parse().unwrap()) - .url(format!( - "{}/windows-x86_64/{{{{current_version}}}}", - mockito::server_url() - )) - .build()); - - let updater = check_update.expect("Can't check update"); - - assert!(updater.should_update); - } - - #[test] - fn http_updater_uptodate() { - let _m = mockito::mock("GET", "/darwin-aarch64/10.0.0") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(generate_sample_platform_json( - "2.0.0", - "SampleTauriKey", - "https://tauri.app", - )) - .create(); - - let app = crate::test::mock_app(); - let check_update = block!(builder(app.handle()) - .current_version("10.0.0".parse().unwrap()) - .url(format!( - "{}/darwin-aarch64/{{{{current_version}}}}", - mockito::server_url() - )) - .build()); - - let updater = check_update.expect("Can't check update"); - - assert!(!updater.should_update); - } - - #[test] - fn http_updater_fallback_urls() { - let _m = mockito::mock("GET", "/") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(generate_sample_raw_json()) - .create(); - - let app = crate::test::mock_app(); - let check_update = block!(builder(app.handle()) - .url("http://badurl.www.tld/1".into()) - .url(mockito::server_url()) - .current_version("0.0.1".parse().unwrap()) - .build()); - - let updater = check_update.expect("Can't check remote update"); - - assert!(updater.should_update); - } - - #[test] - fn http_updater_fallback_urls_with_array() { - let _m = mockito::mock("GET", "/") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(generate_sample_raw_json()) - .create(); - - let app = crate::test::mock_app(); - let check_update = block!(builder(app.handle()) - .urls(&["http://badurl.www.tld/1".into(), mockito::server_url(),]) - .current_version("0.0.1".parse().unwrap()) - .build()); - - let updater = check_update.expect("Can't check remote update"); - - assert!(updater.should_update); - } - - #[test] - fn http_updater_invalid_remote_data() { - let invalid_signature = r#"{ - "version": "v0.0.3", - "notes": "Blablaa", - "pub_date": "2020-02-20T15:41:00Z", - "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz", - "signature": true - }"#; - let invalid_version = r#"{ - "version": 5, - "notes": "Blablaa", - "pub_date": "2020-02-20T15:41:00Z", - "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz", - "signature": "x" - }"#; - let invalid_name = r#"{ - "name": false, - "notes": "Blablaa", - "pub_date": "2020-02-20T15:41:00Z", - "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz", - "signature": "x" - }"#; - let invalid_date = r#"{ - "version": "1.0.0", - "notes": "Blablaa", - "pub_date": 345645646, - "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz", - "signature": "x" - }"#; - let invalid_notes = r#"{ - "version": "v0.0.3", - "notes": ["bla", "bla"], - "pub_date": "2020-02-20T15:41:00Z", - "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz", - "signature": "x" - }"#; - let invalid_url = r#"{ - "version": "v0.0.3", - "notes": "Blablaa", - "pub_date": "2020-02-20T15:41:00Z", - "url": ["https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz", "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz"], - "signature": "x" - }"#; - let invalid_platform_signature = r#"{ - "version": "v0.0.3", - "notes": "Blablaa", - "pub_date": "2020-02-20T15:41:00Z", - "platforms": { - "test-target": { - "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz", - "signature": { - "test-target": "x" - } - } - } - }"#; - let invalid_platform_url = r#"{ - "version": "v0.0.3", - "notes": "Blablaa", - "pub_date": "2020-02-20T15:41:00Z", - "platforms": { - "test-target": { - "url": { - "first": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz" - } - "signature": "x" - } - } - }"#; - - let test_cases = [ - ( - invalid_signature, - Box::new(|e| matches!(e, Error::InvalidResponseType("signature", "string", _))) - as Box bool>, - ), - ( - invalid_version, - Box::new(|e| matches!(e, Error::InvalidResponseType("version", "string", _))) - as Box bool>, - ), - ( - invalid_name, - Box::new(|e| matches!(e, Error::InvalidResponseType("name", "string", _))) - as Box bool>, - ), - ( - invalid_date, - Box::new(|e| matches!(e, Error::InvalidResponseType("pub_date", "string", _))) - as Box bool>, - ), - ( - invalid_notes, - Box::new(|e| matches!(e, Error::InvalidResponseType("notes", "string", _))) - as Box bool>, - ), - ( - invalid_url, - Box::new(|e| matches!(e, Error::InvalidResponseType("url", "string", _))) - as Box bool>, - ), - ( - invalid_platform_signature, - Box::new(|e| matches!(e, Error::InvalidResponseType("signature", "string", _))) - as Box bool>, - ), - ( - invalid_platform_url, - Box::new(|e| matches!(e, Error::InvalidResponseType("url", "string", _))) - as Box bool>, - ), - ]; - - for (response, validator) in test_cases { - let _m = mockito::mock("GET", "/") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(response) - .create(); - - let app = crate::test::mock_app(); - let check_update = block!(builder(app.handle()) - .url(mockito::server_url()) - .current_version("0.0.1".parse().unwrap()) - .target("test-target") - .build()); - if let Err(e) = check_update { - validator(e); - } else { - panic!("unexpected Ok response"); - } - } - } - - #[test] - fn http_updater_missing_remote_data() { - let missing_signature = r#"{ - "version": "v0.0.3", - "notes": "Blablaa", - "pub_date": "2020-02-20T15:41:00Z", - "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz" - }"#; - let missing_version = r#"{ - "notes": "Blablaa", - "pub_date": "2020-02-20T15:41:00Z", - "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz", - "signature": "x" - }"#; - let missing_url = r#"{ - "version": "v0.0.3", - "notes": "Blablaa", - "pub_date": "2020-02-20T15:41:00Z", - "signature": "x" - }"#; - let missing_target = r#"{ - "version": "v0.0.3", - "notes": "Blablaa", - "pub_date": "2020-02-20T15:41:00Z", - "platforms": { - "unknown-target": { - "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz", - "signature": "x" - } - } - }"#; - let missing_platform_signature = r#"{ - "version": "v0.0.3", - "notes": "Blablaa", - "pub_date": "2020-02-20T15:41:00Z", - "platforms": { - "test-target": { - "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz" - } - } - }"#; - let missing_platform_url = r#"{ - "version": "v0.0.3", - "notes": "Blablaa", - "pub_date": "2020-02-20T15:41:00Z", - "platforms": { - "test-target": { - "signature": "x" - } - } - }"#; - - fn missing_field_error(field: &str) -> String { - format!("the `{field}` field was not set on the updater response") - } - - let test_cases = [ - (missing_signature, missing_field_error("signature")), - (missing_version, "missing field `version`".to_string()), - (missing_url, missing_field_error("url")), - ( - missing_target, - Error::TargetNotFound("test-target".into()).to_string(), - ), - ( - missing_platform_signature, - "missing field `signature`".to_string(), - ), - (missing_platform_url, "missing field `url`".to_string()), - ]; - - for (response, error) in test_cases { - let _m = mockito::mock("GET", "/") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(response) - .create(); - - let app = crate::test::mock_app(); - let check_update = block!(builder(app.handle()) - .url(mockito::server_url()) - .current_version("0.0.1".parse().unwrap()) - .target("test-target") - .build()); - if let Err(e) = check_update { - println!("ERROR: {e}, expected: {error}"); - assert!(e.to_string().contains(&error)); - } else { - panic!("unexpected Ok response"); - } - } - } - - // run complete process on mac only for now as we don't have - // server (api) that we can use to test - #[test] - #[cfg(target_os = "macos")] - fn http_updater_complete_process() { - #[cfg(target_os = "macos")] - let archive_file = "archive.macos.tar.gz"; - #[cfg(target_os = "linux")] - let archive_file = "archive.linux.tar.gz"; - #[cfg(target_os = "windows")] - let archive_file = "archive.windows.zip"; - - let good_archive_url = format!("{}/{archive_file}", mockito::server_url()); - - let mut signature_file = File::open(format!( - "./test/updater/fixture/archives/{archive_file}.sig" - )) - .expect("Unable to open signature"); - let mut signature = String::new(); - signature_file - .read_to_string(&mut signature) - .expect("Unable to read signature as string"); - - let mut pubkey_file = File::open("./test/updater/fixture/good_signature/update.key.pub") - .expect("Unable to open pubkey"); - let mut pubkey = String::new(); - pubkey_file - .read_to_string(&mut pubkey) - .expect("Unable to read signature as string"); - - // add sample file - let _m = mockito::mock("GET", format!("/{archive_file}").as_str()) - .with_status(200) - .with_header("content-type", "application/octet-stream") - .with_body_from_file(format!("./test/updater/fixture/archives/{archive_file}")) - .create(); - - // sample mock for update file - let _m = mockito::mock("GET", "/") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(generate_sample_platform_json( - "2.0.1", - signature.as_ref(), - good_archive_url.as_ref(), - )) - .create(); - - // Build a tmpdir so we can test our extraction inside - // We dont want to overwrite our current executable or the directory - // Otherwise tests are failing... - let executable_path = current_exe().expect("Can't extract executable path"); - let parent_path = executable_path - .parent() - .expect("Can't find the parent path"); - - let tmp_dir = tempfile::Builder::new() - .prefix("tauri_updater_test") - .tempdir_in(parent_path); - - assert!(tmp_dir.is_ok()); - let tmp_dir_unwrap = tmp_dir.expect("Can't find tmp_dir"); - let tmp_dir_path = tmp_dir_unwrap.path(); - - #[cfg(target_os = "linux")] - let my_executable = &tmp_dir_path.join("updater-example_0.1.0_amd64.AppImage"); - #[cfg(target_os = "macos")] - let my_executable = &tmp_dir_path.join("my_app"); - #[cfg(target_os = "windows")] - let my_executable = &tmp_dir_path.join("my_app.exe"); - - // configure the updater - let app = crate::test::mock_app(); - let check_update = block!(builder(app.handle()) - .url(mockito::server_url()) - // It should represent the executable path, that's why we add my_app.exe in our - // test path -- in production you shouldn't have to provide it - .executable_path(my_executable) - // make sure we force an update - .current_version("1.0.0".parse().unwrap()) - .build()); - - #[cfg(target_os = "linux")] - { - env::set_var("APPIMAGE", my_executable); - } - - // unwrap our results - let updater = check_update.expect("Can't check remote update"); - - // make sure we need to update - assert!(updater.should_update); - // make sure we can read announced version - assert_eq!(updater.version, "2.0.1"); - - // download, install and validate signature - let install_process = block!(updater.download_and_install(pubkey, |_, _| (), || ())); - assert!(install_process.is_ok()); - - // make sure the extraction went well (it should have skipped the main app.app folder) - // as we can't extract in /Applications directly - #[cfg(target_os = "macos")] - let bin_file = tmp_dir_path.join("Contents").join("MacOS").join("app"); - #[cfg(target_os = "linux")] - // linux should extract at same place as the executable path - let bin_file = my_executable; - #[cfg(target_os = "windows")] - let bin_file = tmp_dir_path.join("with").join("long").join("path.json"); - - assert!(bin_file.exists()); - } -} diff --git a/core/tauri/src/updater/error.rs b/core/tauri/src/updater/error.rs deleted file mode 100644 index b94ad0dda42..00000000000 --- a/core/tauri/src/updater/error.rs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use thiserror::Error; - -/// All errors that can occur while running the updater. -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum Error { - /// IO Errors. - #[error("`{0}`")] - Io(#[from] std::io::Error), - /// Semver Errors. - #[error("Unable to compare version: {0}")] - Semver(#[from] semver::Error), - /// JSON (Serde) Errors. - #[error("JSON error: {0}")] - SerdeJson(#[from] serde_json::Error), - /// Minisign is used for signature validation. - #[error("Verify signature error: {0}")] - Minisign(#[from] minisign_verify::Error), - /// Error with Minisign base64 decoding. - #[error("Signature decoding error: {0}")] - Base64(#[from] base64::DecodeError), - /// UTF8 Errors in signature. - #[error("The signature {0} could not be decoded, please check if it is a valid base64 string. The signature must be the contents of the `.sig` file generated by the Tauri bundler, as a string.")] - SignatureUtf8(String), - /// Tauri utils, mainly extract and file move. - #[error("Tauri API error: {0}")] - TauriApi(#[from] crate::api::Error), - /// Network error. - #[error("Network error: {0}")] - Network(String), - /// Could not fetch a valid response from the server. - #[error("Could not fetch a valid release JSON from the remote")] - ReleaseNotFound, - /// Error building updater. - #[error("Unable to prepare the updater: {0}")] - Builder(String), - /// Error building updater. - #[error("Unable to extract the new version: {0}")] - Extract(String), - /// Updater cannot be executed on this Linux package. Currently the updater is enabled only on AppImages. - #[error("Cannot run updater on this Linux package. Currently only an AppImage can be updated.")] - UnsupportedLinuxPackage, - /// Operating system is not supported. - #[error("unsupported OS, expected one of `linux`, `darwin` or `windows`.")] - UnsupportedOs, - /// Unsupported app architecture. - #[error( - "Unsupported application architecture, expected one of `x86`, `x86_64`, `arm` or `aarch64`." - )] - UnsupportedArch, - /// The platform was not found on the updater JSON response. - #[error("the platform `{0}` was not found on the response `platforms` object")] - TargetNotFound(String), - /// Triggered when there is NO error and the two versions are equals. - /// On client side, it's important to catch this error. - #[error("No updates available")] - UpToDate, - /// The updater responded with an invalid signature type. - #[error("the updater response field `{0}` type is invalid, expected {1} but found {2}")] - InvalidResponseType(&'static str, &'static str, serde_json::Value), - /// HTTP error. - #[error(transparent)] - Http(#[from] http::Error), - /// Temp dir is not on same mount mount. This prevents our updater to rename the AppImage to a temp file. - #[cfg(target_os = "linux")] - #[error("temp directory is not on the same mount point as the AppImage")] - TempDirNotOnSameMountPoint, -} - -pub type Result = std::result::Result; diff --git a/core/tauri/src/updater/mod.rs b/core/tauri/src/updater/mod.rs deleted file mode 100644 index 549eda47828..00000000000 --- a/core/tauri/src/updater/mod.rs +++ /dev/null @@ -1,630 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! The Tauri updater. -//! -//! The updater is focused on making Tauri's application updates **as safe and transparent as updates to a website**. -//! -//! For a full guide on setting up the updater, see . -//! -//! Check [`UpdateBuilder`] to see how to manually trigger and customize the updater at runtime. -//! -//! ## Events -//! -//! To listen to the updater events, for example to check for error messages, you need to use [`RunEvent::Updater`](crate::RunEvent) in [`App::run`](crate::App#method.run). -//! -//! ```no_run -//! let app = tauri::Builder::default() -//! // on an actual app, remove the string argument -//! .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) -//! .expect("error while building tauri application"); -//! app.run(|_app_handle, event| match event { -//! tauri::RunEvent::Updater(updater_event) => { -//! match updater_event { -//! tauri::UpdaterEvent::UpdateAvailable { body, date, version } => { -//! println!("update available {} {:?} {}", body, date, version); -//! } -//! // Emitted when the download is about to be started. -//! tauri::UpdaterEvent::Pending => { -//! println!("update is pending!"); -//! } -//! tauri::UpdaterEvent::DownloadProgress { chunk_length, content_length } => { -//! println!("downloaded {} of {:?}", chunk_length, content_length); -//! } -//! // Emitted when the download has finished and the update is about to be installed. -//! tauri::UpdaterEvent::Downloaded => { -//! println!("update has been downloaded!"); -//! } -//! // Emitted when the update was installed. You can then ask to restart the app. -//! tauri::UpdaterEvent::Updated => { -//! println!("app has been updated"); -//! } -//! // Emitted when the app already has the latest version installed and an update is not needed. -//! tauri::UpdaterEvent::AlreadyUpToDate => { -//! println!("app is already up to date"); -//! } -//! // Emitted when there is an error with the updater. We suggest to listen to this event even if the default dialog is enabled. -//! tauri::UpdaterEvent::Error(error) => { -//! println!("failed to update: {}", error); -//! } -//! _ => (), -//! } -//! } -//! _ => {} -//! }); -//! ``` - -mod core; -mod error; - -use std::time::Duration; - -use http::header::{HeaderName, HeaderValue}; -use semver::Version; -use time::OffsetDateTime; - -pub use self::{core::RemoteRelease, error::Error}; -/// Alias for [`std::result::Result`] using our own [`Error`]. -pub type Result = std::result::Result; - -use crate::{runtime::EventLoopProxy, AppHandle, EventLoopMessage, Manager, Runtime, UpdaterEvent}; - -#[cfg(desktop)] -use crate::api::dialog::blocking::ask; - -#[cfg(mobile)] -fn ask( - _parent_window: Option<&crate::Window>, - _title: impl AsRef, - _message: impl AsRef, -) -> bool { - true -} - -/// Check for new updates -pub const EVENT_CHECK_UPDATE: &str = "tauri://update"; -/// New update available -pub const EVENT_UPDATE_AVAILABLE: &str = "tauri://update-available"; -/// Used to initialize an update *should run check-update first (once you received the update available event)* -pub const EVENT_INSTALL_UPDATE: &str = "tauri://update-install"; -/// Send updater status or error even if dialog is enabled, you should -/// always listen for this event. It'll send you the install progress -/// and any error triggered during update check and install -pub const EVENT_STATUS_UPDATE: &str = "tauri://update-status"; -/// The name of the event that is emitted on download progress. -pub const EVENT_DOWNLOAD_PROGRESS: &str = "tauri://update-download-progress"; -/// this is the status emitted when the download start -pub const EVENT_STATUS_PENDING: &str = "PENDING"; -/// When you got this status, something went wrong -/// you can find the error message inside the `error` field. -pub const EVENT_STATUS_ERROR: &str = "ERROR"; -/// The update has been downloaded. -pub const EVENT_STATUS_DOWNLOADED: &str = "DOWNLOADED"; -/// When you receive this status, you should ask the user to restart -pub const EVENT_STATUS_SUCCESS: &str = "DONE"; -/// When you receive this status, this is because the application is running last version -pub const EVENT_STATUS_UPTODATE: &str = "UPTODATE"; - -/// Gets the target string used on the updater. -pub fn target() -> Option { - if let (Some(target), Some(arch)) = (core::get_updater_target(), core::get_updater_arch()) { - Some(format!("{target}-{arch}")) - } else { - None - } -} - -#[derive(Clone, serde::Serialize)] -struct StatusEvent { - status: String, - error: Option, -} - -#[derive(Clone, serde::Serialize)] -#[serde(rename_all = "camelCase")] -struct DownloadProgressEvent { - chunk_length: usize, - content_length: Option, -} - -#[derive(Clone, serde::Serialize)] -struct UpdateManifest { - version: String, - date: Option, - body: String, -} - -/// An update check builder. -#[derive(Debug)] -pub struct UpdateBuilder { - inner: core::UpdateBuilder, - events: bool, -} - -impl UpdateBuilder { - /// Do not use the event system to emit information or listen to install the update. - pub fn skip_events(mut self) -> Self { - self.events = false; - self - } - - /// Sets the current platform's target name for the updater. - /// - /// The target is injected in the endpoint URL by replacing `{{target}}`. - /// Note that this does not affect the `{{arch}}` variable. - /// - /// If the updater response JSON includes the `platforms` field, - /// that object must contain a value for the target key. - /// - /// By default Tauri uses `$OS_NAME` as the replacement for `{{target}}` - /// and `$OS_NAME-$ARCH` as the key in the `platforms` object, - /// where `$OS_NAME` is the current operating system name "linux", "windows" or "darwin") - /// and `$ARCH` is one of the supported architectures ("i686", "x86_64", "armv7" or "aarch64"). - /// - /// See [`Builder::updater_target`](crate::Builder#method.updater_target) for a way to set the target globally. - /// - /// # Examples - /// - /// ## Use a macOS Universal binary target name - /// - /// In this example, we set the updater target only on macOS. - /// On other platforms, we set the default target. - /// Note that `{{target}}` will be replaced with `darwin-universal`, - /// but `{{arch}}` is still the running platform's architecture. - /// - /// ```no_run - /// tauri::Builder::default() - /// .setup(|app| { - /// let handle = app.handle(); - /// tauri::async_runtime::spawn(async move { - /// let builder = tauri::updater::builder(handle).target(if cfg!(target_os = "macos") { - /// "darwin-universal".to_string() - /// } else { - /// tauri::updater::target().unwrap() - /// }); - /// match builder.check().await { - /// Ok(update) => {} - /// Err(error) => {} - /// } - /// }); - /// Ok(()) - /// }); - /// ``` - /// - /// ## Append debug information to the target - /// - /// This allows you to provide updates for both debug and release applications. - /// - /// ```no_run - /// tauri::Builder::default() - /// .setup(|app| { - /// let handle = app.handle(); - /// tauri::async_runtime::spawn(async move { - /// let kind = if cfg!(debug_assertions) { "debug" } else { "release" }; - /// let builder = tauri::updater::builder(handle).target(format!("{}-{kind}", tauri::updater::target().unwrap())); - /// match builder.check().await { - /// Ok(update) => {} - /// Err(error) => {} - /// } - /// }); - /// Ok(()) - /// }); - /// ``` - /// - /// ## Use the platform's target triple - /// - /// ```no_run - /// tauri::Builder::default() - /// .setup(|app| { - /// let handle = app.handle(); - /// tauri::async_runtime::spawn(async move { - /// let builder = tauri::updater::builder(handle).target(tauri::utils::platform::target_triple().unwrap()); - /// match builder.check().await { - /// Ok(update) => {} - /// Err(error) => {} - /// } - /// }); - /// Ok(()) - /// }); - /// ``` - pub fn target(mut self, target: impl Into) -> Self { - self.inner = self.inner.target(target); - self - } - - /// Sets a closure that is invoked to compare the current version and the latest version returned by the updater server. - /// The first argument is the current version, and the second one is the latest version. - /// - /// The closure must return `true` if the update should be installed. - /// - /// # Examples - /// - /// - Always install the version returned by the server: - /// - /// ```no_run - /// tauri::Builder::default() - /// .setup(|app| { - /// tauri::updater::builder(app.handle()).should_install(|_current, _latest| true); - /// Ok(()) - /// }); - /// ``` - pub fn should_install bool + Send + 'static>( - mut self, - f: F, - ) -> Self { - self.inner = self.inner.should_install(f); - self - } - - /// Sets the timeout for the requests to the updater endpoints. - pub fn timeout(mut self, timeout: Duration) -> Self { - self.inner = self.inner.timeout(timeout); - self - } - - /// Add a `Header` to the request. - pub fn header(mut self, key: K, value: V) -> Result - where - HeaderName: TryFrom, - >::Error: Into, - HeaderValue: TryFrom, - >::Error: Into, - { - self.inner = self.inner.header(key, value)?; - Ok(self) - } - - /// Check if an update is available. - /// - /// # Examples - /// - /// ```no_run - /// tauri::Builder::default() - /// .setup(|app| { - /// let handle = app.handle(); - /// tauri::async_runtime::spawn(async move { - /// match tauri::updater::builder(handle).check().await { - /// Ok(update) => { - /// if update.is_update_available() { - /// update.download_and_install().await.unwrap(); - /// } - /// } - /// Err(e) => { - /// println!("failed to get update: {}", e); - /// } - /// } - /// }); - /// Ok(()) - /// }); - /// ``` - /// - /// If ther server responds with status code `204`, this method will return [`Error::UpToDate`] - pub async fn check(self) -> Result> { - let handle = self.inner.app.clone(); - // check updates - match self.inner.build().await { - Ok(update) => { - if self.events { - // send notification if we need to update - if update.should_update { - let body = update.body.clone().unwrap_or_else(|| String::from("")); - - // Emit `tauri://update-available` - let _ = handle.emit_all( - EVENT_UPDATE_AVAILABLE, - UpdateManifest { - body: body.clone(), - date: update.date.map(|d| d.to_string()), - version: update.version.clone(), - }, - ); - let _ = handle.create_proxy().send_event(EventLoopMessage::Updater( - UpdaterEvent::UpdateAvailable { - body, - date: update.date, - version: update.version.clone(), - }, - )); - - // Listen for `tauri://update-install` - let update_ = update.clone(); - handle.once_global(EVENT_INSTALL_UPDATE, move |_msg| { - crate::async_runtime::spawn(async move { - let _ = download_and_install(update_).await; - }); - }); - } else { - send_status_update(&handle, UpdaterEvent::AlreadyUpToDate); - } - } - Ok(UpdateResponse { update }) - } - Err(e) => { - if self.events { - send_status_update( - &handle, - match e { - Error::UpToDate => UpdaterEvent::AlreadyUpToDate, - _ => UpdaterEvent::Error(e.to_string()), - }, - ); - } - Err(e) - } - } - } -} - -/// The response of an updater check. -pub struct UpdateResponse { - update: core::Update, -} - -impl Clone for UpdateResponse { - fn clone(&self) -> Self { - Self { - update: self.update.clone(), - } - } -} - -impl UpdateResponse { - /// Whether the updater found a newer release or not. - pub fn is_update_available(&self) -> bool { - self.update.should_update - } - - /// The current version of the application as read by the updater. - pub fn current_version(&self) -> &Version { - &self.update.current_version - } - - /// The latest version of the application found by the updater. - pub fn latest_version(&self) -> &str { - &self.update.version - } - - /// The update date. - pub fn date(&self) -> Option<&OffsetDateTime> { - self.update.date.as_ref() - } - - /// The update description. - pub fn body(&self) -> Option<&String> { - self.update.body.as_ref() - } - - /// Downloads and installs the update. - pub async fn download_and_install(self) -> Result<()> { - download_and_install(self.update).await - } -} - -/// Check if there is any new update with builtin dialog. -pub(crate) async fn check_update_with_dialog(handle: AppHandle) { - let updater_config = handle.config().tauri.updater.clone(); - let package_info = handle.package_info().clone(); - if let Some(endpoints) = updater_config.endpoints.clone() { - let endpoints = endpoints - .iter() - .map(|e| e.to_string()) - .collect::>(); - - let mut builder = self::core::builder(handle.clone()) - .urls(&endpoints[..]) - .current_version(package_info.version); - if let Some(target) = &handle.updater_settings.target { - builder = builder.target(target); - } - - // check updates - match builder.build().await { - Ok(updater) => { - let pubkey = updater_config.pubkey.clone(); - - if !updater.should_update { - send_status_update(&handle, UpdaterEvent::AlreadyUpToDate); - } else if updater_config.dialog { - // if dialog enabled only - let body = updater.body.clone().unwrap_or_else(|| String::from("")); - let dialog = - prompt_for_install(&updater.clone(), &package_info.name, &body.clone(), pubkey).await; - - if let Err(e) = dialog { - send_status_update(&handle, UpdaterEvent::Error(e.to_string())); - } - } - } - Err(e) => { - send_status_update( - &handle, - match e { - Error::UpToDate => UpdaterEvent::AlreadyUpToDate, - _ => UpdaterEvent::Error(e.to_string()), - }, - ); - } - } - } -} - -/// Updater listener -/// This function should be run on the main thread once. -pub(crate) fn listener(handle: AppHandle) { - // Wait to receive the event `"tauri://update"` - let handle_ = handle.clone(); - handle.listen_global(EVENT_CHECK_UPDATE, move |_msg| { - let handle_ = handle_.clone(); - crate::async_runtime::spawn(async move { - let _ = builder(handle_.clone()).check().await; - }); - }); -} - -pub(crate) async fn download_and_install(update: core::Update) -> Result<()> { - // Start installation - // emit {"status": "PENDING"} - send_status_update(&update.app, UpdaterEvent::Pending); - - let handle = update.app.clone(); - let handle_ = handle.clone(); - - // Launch updater download process - // macOS we display the `Ready to restart dialog` asking to restart - // Windows is closing the current App and launch the downloaded MSI when ready (the process stop here) - // Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here) - let update_result = update - .download_and_install( - update.app.config().tauri.updater.pubkey.clone(), - move |chunk_length, content_length| { - send_download_progress_event(&handle, chunk_length, content_length); - }, - move || { - send_status_update(&handle_, UpdaterEvent::Downloaded); - }, - ) - .await; - - if let Err(err) = &update_result { - // emit {"status": "ERROR", "error": "The error message"} - send_status_update(&update.app, UpdaterEvent::Error(err.to_string())); - } else { - // emit {"status": "DONE"} - send_status_update(&update.app, UpdaterEvent::Updated); - } - update_result -} - -/// Initializes the [`UpdateBuilder`] using the app configuration. -pub fn builder(handle: AppHandle) -> UpdateBuilder { - let updater_config = &handle.config().tauri.updater; - let package_info = handle.package_info().clone(); - - // prepare our endpoints - let endpoints = updater_config - .endpoints - .as_ref() - .expect("Something wrong with endpoints") - .iter() - .map(|e| e.to_string()) - .collect::>(); - - let mut builder = self::core::builder(handle.clone()) - .urls(&endpoints[..]) - .current_version(package_info.version); - if let Some(target) = &handle.updater_settings.target { - builder = builder.target(target); - } - UpdateBuilder { - inner: builder, - events: true, - } -} - -// Send a status update via `tauri://update-download-progress` event. -fn send_download_progress_event( - handle: &AppHandle, - chunk_length: usize, - content_length: Option, -) { - let _ = handle.emit_all( - EVENT_DOWNLOAD_PROGRESS, - DownloadProgressEvent { - chunk_length, - content_length, - }, - ); - let _ = - handle - .create_proxy() - .send_event(EventLoopMessage::Updater(UpdaterEvent::DownloadProgress { - chunk_length, - content_length, - })); -} - -// Send a status update via `tauri://update-status` event. -fn send_status_update(handle: &AppHandle, message: UpdaterEvent) { - let _ = handle.emit_all( - EVENT_STATUS_UPDATE, - if let UpdaterEvent::Error(error) = &message { - StatusEvent { - error: Some(error.clone()), - status: message.clone().status_message().into(), - } - } else { - StatusEvent { - error: None, - status: message.clone().status_message().into(), - } - }, - ); - let _ = handle - .create_proxy() - .send_event(EventLoopMessage::Updater(message)); -} - -// Prompt a dialog asking if the user want to install the new version -// Maybe we should add an option to customize it in future versions. -async fn prompt_for_install( - update: &self::core::Update, - app_name: &str, - body: &str, - pubkey: String, -) -> Result<()> { - let windows = update.app.windows(); - let parent_window = windows.values().next(); - - // todo(lemarier): We should review this and make sure we have - // something more conventional. - let should_install = ask( - parent_window, - format!(r#"A new version of {app_name} is available! "#), - format!( - r#"{app_name} {} is now available -- you have {}. - -Would you like to install it now? - -Release Notes: -{body}"#, - update.version, update.current_version - ), - ); - - if should_install { - let handle = update.app.clone(); - let handle_ = handle.clone(); - - // Launch updater download process - // macOS we display the `Ready to restart dialog` asking to restart - // Windows is closing the current App and launch the downloaded MSI when ready (the process stop here) - // Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here) - update - .download_and_install( - pubkey.clone(), - move |chunk_length, content_length| { - send_download_progress_event(&handle, chunk_length, content_length); - }, - move || { - send_status_update(&handle_, UpdaterEvent::Downloaded); - }, - ) - .await?; - - // emit {"status": "DONE"} - send_status_update(&update.app, UpdaterEvent::Updated); - - // Ask user if we need to restart the application - let should_exit = ask( - parent_window, - "Ready to Restart", - "The installation was successful, do you want to restart the application now?", - ); - if should_exit { - update.app.restart(); - } - } - - Ok(()) -} diff --git a/core/tauri/src/vibrancy/macos.rs b/core/tauri/src/vibrancy/macos.rs new file mode 100644 index 00000000000..26ddc1af3d5 --- /dev/null +++ b/core/tauri/src/vibrancy/macos.rs @@ -0,0 +1,299 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg(target_os = "macos")] +#![allow(deprecated)] + +use crate::utils::config::WindowEffectsConfig; +use crate::window::{Effect, EffectState}; +use cocoa::{ + appkit::{ + NSAppKitVersionNumber, NSAppKitVersionNumber10_10, NSAppKitVersionNumber10_11, + NSAutoresizingMaskOptions, NSView, NSViewHeightSizable, NSViewWidthSizable, NSWindow, + NSWindowOrderingMode, + }, + base::{id, nil, BOOL}, + foundation::{NSAutoreleasePool, NSPoint, NSRect, NSSize}, +}; +use objc::{class, msg_send, sel, sel_impl}; + +pub fn apply_effects(window: id, effects: WindowEffectsConfig) { + let WindowEffectsConfig { + effects, + radius, + state, + .. + } = effects; + let mut appearance: NSVisualEffectMaterial = if let Some(effect) = effects.into_iter().find(|e| { + matches!( + e, + Effect::AppearanceBased + | Effect::Light + | Effect::Dark + | Effect::MediumLight + | Effect::UltraDark + | Effect::Titlebar + | Effect::Selection + | Effect::Menu + | Effect::Popover + | Effect::Sidebar + | Effect::HeaderView + | Effect::Sheet + | Effect::WindowBackground + | Effect::HudWindow + | Effect::FullScreenUI + | Effect::Tooltip + | Effect::ContentBackground + | Effect::UnderWindowBackground + | Effect::UnderPageBackground + ) + }) { + effect.into() + } else { + return; + }; + + unsafe { + if NSAppKitVersionNumber < NSAppKitVersionNumber10_10 { + return; + } + + if !msg_send![class!(NSThread), isMainThread] { + return; + } + + if appearance as u32 > 4 && NSAppKitVersionNumber < NSAppKitVersionNumber10_11 { + appearance = NSVisualEffectMaterial::AppearanceBased; + } + + if appearance as u32 > 9 && NSAppKitVersionNumber < NSAppKitVersionNumber10_14 { + appearance = NSVisualEffectMaterial::AppearanceBased; + } + + let ns_view: id = window.contentView(); + let bounds = NSView::bounds(ns_view); + + let blurred_view = NSVisualEffectView::initWithFrame_(NSVisualEffectView::alloc(nil), bounds); + blurred_view.autorelease(); + + blurred_view.setMaterial_(appearance); + blurred_view.setCornerRadius_(radius.unwrap_or(0.0)); + blurred_view.setBlendingMode_(NSVisualEffectBlendingMode::BehindWindow); + blurred_view.setState_( + state + .map(Into::into) + .unwrap_or(NSVisualEffectState::FollowsWindowActiveState), + ); + NSVisualEffectView::setAutoresizingMask_( + blurred_view, + NSViewWidthSizable | NSViewHeightSizable, + ); + + let _: () = msg_send![ns_view, addSubview: blurred_view positioned: NSWindowOrderingMode::NSWindowBelow relativeTo: 0]; + } +} + +#[allow(non_upper_case_globals)] +const NSAppKitVersionNumber10_14: f64 = 1671.0; + +// https://developer.apple.com/documentation/appkit/nsvisualeffectview/blendingmode +#[allow(dead_code)] +#[repr(u64)] +#[derive(Clone, Copy, Debug, PartialEq)] +enum NSVisualEffectBlendingMode { + BehindWindow = 0, + WithinWindow = 1, +} + +// macos 10.10+ +// https://developer.apple.com/documentation/appkit/nsvisualeffectview +#[allow(non_snake_case)] +trait NSVisualEffectView: Sized { + unsafe fn alloc(_: Self) -> id { + msg_send![class!(NSVisualEffectView), alloc] + } + + unsafe fn init(self) -> id; + unsafe fn initWithFrame_(self, frameRect: NSRect) -> id; + unsafe fn bounds(self) -> NSRect; + unsafe fn frame(self) -> NSRect; + unsafe fn setFrameSize(self, frameSize: NSSize); + unsafe fn setFrameOrigin(self, frameOrigin: NSPoint); + + unsafe fn superview(self) -> id; + unsafe fn removeFromSuperview(self); + unsafe fn setAutoresizingMask_(self, autoresizingMask: NSAutoresizingMaskOptions); + + // API_AVAILABLE(macos(10.12)); + unsafe fn isEmphasized(self) -> BOOL; + // API_AVAILABLE(macos(10.12)); + unsafe fn setEmphasized_(self, emphasized: BOOL); + + unsafe fn setMaterial_(self, material: NSVisualEffectMaterial); + unsafe fn setCornerRadius_(self, radius: f64); + unsafe fn setState_(self, state: NSVisualEffectState); + unsafe fn setBlendingMode_(self, mode: NSVisualEffectBlendingMode); +} + +#[allow(non_snake_case)] +impl NSVisualEffectView for id { + unsafe fn init(self) -> id { + msg_send![self, init] + } + + unsafe fn initWithFrame_(self, frameRect: NSRect) -> id { + msg_send![self, initWithFrame: frameRect] + } + + unsafe fn bounds(self) -> NSRect { + msg_send![self, bounds] + } + + unsafe fn frame(self) -> NSRect { + msg_send![self, frame] + } + + unsafe fn setFrameSize(self, frameSize: NSSize) { + msg_send![self, setFrameSize: frameSize] + } + + unsafe fn setFrameOrigin(self, frameOrigin: NSPoint) { + msg_send![self, setFrameOrigin: frameOrigin] + } + + unsafe fn superview(self) -> id { + msg_send![self, superview] + } + + unsafe fn removeFromSuperview(self) { + msg_send![self, removeFromSuperview] + } + + unsafe fn setAutoresizingMask_(self, autoresizingMask: NSAutoresizingMaskOptions) { + msg_send![self, setAutoresizingMask: autoresizingMask] + } + + // API_AVAILABLE(macos(10.12)); + unsafe fn isEmphasized(self) -> BOOL { + msg_send![self, isEmphasized] + } + + // API_AVAILABLE(macos(10.12)); + unsafe fn setEmphasized_(self, emphasized: BOOL) { + msg_send![self, setEmphasized: emphasized] + } + + unsafe fn setMaterial_(self, material: NSVisualEffectMaterial) { + msg_send![self, setMaterial: material] + } + + unsafe fn setCornerRadius_(self, radius: f64) { + msg_send![self, setCornerRadius: radius] + } + + unsafe fn setState_(self, state: NSVisualEffectState) { + msg_send![self, setState: state] + } + + unsafe fn setBlendingMode_(self, mode: NSVisualEffectBlendingMode) { + msg_send![self, setBlendingMode: mode] + } +} + +/// +#[repr(u64)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NSVisualEffectMaterial { + #[deprecated = "Since macOS 10.14 a default material appropriate for the view's effectiveAppearance. You should instead choose an appropriate semantic material."] + AppearanceBased = 0, + #[deprecated = "Since macOS 10.14 use a semantic material instead."] + Light = 1, + #[deprecated = "Since macOS 10.14 use a semantic material instead."] + Dark = 2, + #[deprecated = "Since macOS 10.14 use a semantic material instead."] + MediumLight = 8, + #[deprecated = "Since macOS 10.14 use a semantic material instead."] + UltraDark = 9, + + /// macOS 10.10+ + Titlebar = 3, + /// macOS 10.10+ + Selection = 4, + + /// macOS 10.11+ + Menu = 5, + /// macOS 10.11+ + Popover = 6, + /// macOS 10.11+ + Sidebar = 7, + + /// macOS 10.14+ + HeaderView = 10, + /// macOS 10.14+ + Sheet = 11, + /// macOS 10.14+ + WindowBackground = 12, + /// macOS 10.14+ + HudWindow = 13, + /// macOS 10.14+ + FullScreenUI = 15, + /// macOS 10.14+ + Tooltip = 17, + /// macOS 10.14+ + ContentBackground = 18, + /// macOS 10.14+ + UnderWindowBackground = 21, + /// macOS 10.14+ + UnderPageBackground = 22, +} + +/// +#[allow(dead_code)] +#[repr(u64)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NSVisualEffectState { + /// Make window vibrancy state follow the window's active state + FollowsWindowActiveState = 0, + /// Make window vibrancy state always active + Active = 1, + /// Make window vibrancy state always inactive + Inactive = 2, +} + +impl From for NSVisualEffectMaterial { + fn from(value: crate::window::Effect) -> Self { + match value { + Effect::AppearanceBased => NSVisualEffectMaterial::AppearanceBased, + Effect::Light => NSVisualEffectMaterial::Light, + Effect::Dark => NSVisualEffectMaterial::Dark, + Effect::MediumLight => NSVisualEffectMaterial::MediumLight, + Effect::UltraDark => NSVisualEffectMaterial::UltraDark, + Effect::Titlebar => NSVisualEffectMaterial::Titlebar, + Effect::Selection => NSVisualEffectMaterial::Selection, + Effect::Menu => NSVisualEffectMaterial::Menu, + Effect::Popover => NSVisualEffectMaterial::Popover, + Effect::Sidebar => NSVisualEffectMaterial::Sidebar, + Effect::HeaderView => NSVisualEffectMaterial::HeaderView, + Effect::Sheet => NSVisualEffectMaterial::Sheet, + Effect::WindowBackground => NSVisualEffectMaterial::WindowBackground, + Effect::HudWindow => NSVisualEffectMaterial::HudWindow, + Effect::FullScreenUI => NSVisualEffectMaterial::FullScreenUI, + Effect::Tooltip => NSVisualEffectMaterial::Tooltip, + Effect::ContentBackground => NSVisualEffectMaterial::ContentBackground, + Effect::UnderWindowBackground => NSVisualEffectMaterial::UnderWindowBackground, + Effect::UnderPageBackground => NSVisualEffectMaterial::UnderPageBackground, + _ => unreachable!(), + } + } +} + +impl From for NSVisualEffectState { + fn from(value: crate::window::EffectState) -> Self { + match value { + EffectState::FollowsWindowActiveState => NSVisualEffectState::FollowsWindowActiveState, + EffectState::Active => NSVisualEffectState::Active, + EffectState::Inactive => NSVisualEffectState::Inactive, + } + } +} diff --git a/core/tauri/src/vibrancy/mod.rs b/core/tauri/src/vibrancy/mod.rs new file mode 100644 index 00000000000..2ede80e978e --- /dev/null +++ b/core/tauri/src/vibrancy/mod.rs @@ -0,0 +1,39 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![allow(unused)] + +use tauri_utils::config::WindowEffectsConfig; + +use crate::{Runtime, Window}; + +#[cfg(target_os = "macos")] +mod macos; +#[cfg(windows)] +mod windows; + +pub fn set_window_effects( + window: &Window, + effects: Option, +) -> crate::Result<()> { + if let Some(_effects) = effects { + #[cfg(windows)] + { + let hwnd = window.hwnd()?; + windows::apply_effects(hwnd, _effects); + } + #[cfg(target_os = "macos")] + { + let ns_window = window.ns_window()?; + macos::apply_effects(ns_window as _, _effects); + } + } else { + #[cfg(windows)] + { + let hwnd = window.hwnd()?; + windows::clear_effects(hwnd); + } + } + Ok(()) +} diff --git a/core/tauri/src/vibrancy/windows.rs b/core/tauri/src/vibrancy/windows.rs new file mode 100644 index 00000000000..c28f7552dde --- /dev/null +++ b/core/tauri/src/vibrancy/windows.rs @@ -0,0 +1,266 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg(windows)] +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] +#![allow(clippy::upper_case_acronyms)] + +use std::ffi::c_void; + +use crate::utils::config::WindowEffectsConfig; +use crate::window::{Color, Effect}; +use tauri_utils::platform::{get_function_impl, is_windows_7, windows_version}; +use windows::Win32::Graphics::Dwm::{ + DwmSetWindowAttribute, DWMWA_USE_IMMERSIVE_DARK_MODE, DWMWINDOWATTRIBUTE, +}; +use windows::Win32::{ + Foundation::{BOOL, HWND}, + Graphics::{ + Dwm::{DwmEnableBlurBehindWindow, DWM_BB_ENABLE, DWM_BLURBEHIND}, + Gdi::HRGN, + }, +}; + +pub fn apply_effects(window: HWND, effects: WindowEffectsConfig) { + let WindowEffectsConfig { effects, color, .. } = effects; + let effect = if let Some(effect) = effects.iter().find(|e| { + matches!( + e, + Effect::Mica | Effect::MicaDark | Effect::MicaLight | Effect::Acrylic | Effect::Blur + ) + }) { + effect + } else { + return; + }; + + match effect { + Effect::Blur => apply_blur(window, color), + Effect::Acrylic => apply_acrylic(window, color), + Effect::Mica => apply_mica(window, None), + Effect::MicaDark => apply_mica(window, Some(true)), + Effect::MicaLight => apply_mica(window, Some(false)), + _ => unreachable!(), + } +} + +pub fn clear_effects(window: HWND) { + clear_blur(window); + clear_acrylic(window); + clear_mica(window); +} + +pub fn apply_blur(hwnd: HWND, color: Option) { + if is_windows_7() { + let bb = DWM_BLURBEHIND { + dwFlags: DWM_BB_ENABLE, + fEnable: true.into(), + hRgnBlur: HRGN::default(), + fTransitionOnMaximized: false.into(), + }; + let _ = unsafe { DwmEnableBlurBehindWindow(hwnd, &bb) }; + } else if is_swca_supported() { + unsafe { SetWindowCompositionAttribute(hwnd, ACCENT_STATE::ACCENT_ENABLE_BLURBEHIND, color) }; + } else { + return; + } +} + +fn clear_blur(hwnd: HWND) { + if is_windows_7() { + let bb = DWM_BLURBEHIND { + dwFlags: DWM_BB_ENABLE, + fEnable: false.into(), + hRgnBlur: HRGN::default(), + fTransitionOnMaximized: false.into(), + }; + let _ = unsafe { DwmEnableBlurBehindWindow(hwnd, &bb) }; + } else if is_swca_supported() { + unsafe { SetWindowCompositionAttribute(hwnd, ACCENT_STATE::ACCENT_DISABLED, None) }; + } else { + return; + } +} + +pub fn apply_acrylic(hwnd: HWND, color: Option) { + if is_backdroptype_supported() { + unsafe { + let _ = DwmSetWindowAttribute( + hwnd, + DWMWA_SYSTEMBACKDROP_TYPE, + &DWM_SYSTEMBACKDROP_TYPE::DWMSBT_TRANSIENTWINDOW as *const _ as _, + 4, + ); + } + } else if is_swca_supported() { + unsafe { + SetWindowCompositionAttribute(hwnd, ACCENT_STATE::ACCENT_ENABLE_ACRYLICBLURBEHIND, color); + } + } else { + return; + } +} + +pub fn clear_acrylic(hwnd: HWND) { + if is_backdroptype_supported() { + unsafe { + let _ = DwmSetWindowAttribute( + hwnd, + DWMWA_SYSTEMBACKDROP_TYPE, + &DWM_SYSTEMBACKDROP_TYPE::DWMSBT_DISABLE as *const _ as _, + 4, + ); + } + } else if is_swca_supported() { + unsafe { SetWindowCompositionAttribute(hwnd, ACCENT_STATE::ACCENT_DISABLED, None) }; + } else { + return; + } +} + +pub fn apply_mica(hwnd: HWND, dark: Option) { + if let Some(dark) = dark { + unsafe { + DwmSetWindowAttribute( + hwnd, + DWMWA_USE_IMMERSIVE_DARK_MODE, + &(dark as u32) as *const _ as _, + 4, + ); + } + } + + if is_backdroptype_supported() { + unsafe { + let _ = DwmSetWindowAttribute( + hwnd, + DWMWA_SYSTEMBACKDROP_TYPE, + &DWM_SYSTEMBACKDROP_TYPE::DWMSBT_MAINWINDOW as *const _ as _, + 4, + ); + } + } else if is_undocumented_mica_supported() { + let _ = unsafe { DwmSetWindowAttribute(hwnd, DWMWA_MICA_EFFECT, &1 as *const _ as _, 4) }; + } else { + return; + } +} + +pub fn clear_mica(hwnd: HWND) { + if is_backdroptype_supported() { + unsafe { + let _ = DwmSetWindowAttribute( + hwnd, + DWMWA_SYSTEMBACKDROP_TYPE, + &DWM_SYSTEMBACKDROP_TYPE::DWMSBT_DISABLE as *const _ as _, + 4, + ); + } + } else if is_undocumented_mica_supported() { + let _ = unsafe { DwmSetWindowAttribute(hwnd, DWMWA_MICA_EFFECT, &0 as *const _ as _, 4) }; + } else { + return; + } +} + +const DWMWA_MICA_EFFECT: DWMWINDOWATTRIBUTE = DWMWINDOWATTRIBUTE(1029i32); +const DWMWA_SYSTEMBACKDROP_TYPE: DWMWINDOWATTRIBUTE = DWMWINDOWATTRIBUTE(38i32); + +#[repr(C)] +struct ACCENT_POLICY { + AccentState: u32, + AccentFlags: u32, + GradientColor: u32, + AnimationId: u32, +} + +type WINDOWCOMPOSITIONATTRIB = u32; + +#[repr(C)] +struct WINDOWCOMPOSITIONATTRIBDATA { + Attrib: WINDOWCOMPOSITIONATTRIB, + pvData: *mut c_void, + cbData: usize, +} + +#[derive(PartialEq)] +#[repr(C)] +enum ACCENT_STATE { + ACCENT_DISABLED = 0, + ACCENT_ENABLE_BLURBEHIND = 3, + ACCENT_ENABLE_ACRYLICBLURBEHIND = 4, +} + +macro_rules! get_function { + ($lib:expr, $func:ident) => { + get_function_impl(concat!($lib, '\0'), concat!(stringify!($func), '\0')) + .map(|f| unsafe { std::mem::transmute::(f) }) + }; +} + +unsafe fn SetWindowCompositionAttribute( + hwnd: HWND, + accent_state: ACCENT_STATE, + color: Option, +) { + type SetWindowCompositionAttribute = + unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL; + + if let Some(set_window_composition_attribute) = + get_function!("user32.dll", SetWindowCompositionAttribute) + { + let mut color = color.unwrap_or_default(); + + let is_acrylic = accent_state == ACCENT_STATE::ACCENT_ENABLE_ACRYLICBLURBEHIND; + if is_acrylic && color.3 == 0 { + // acrylic doesn't like to have 0 alpha + color.3 = 1; + } + + let mut policy = ACCENT_POLICY { + AccentState: accent_state as _, + AccentFlags: if is_acrylic { 0 } else { 2 }, + GradientColor: (color.0 as u32) + | (color.1 as u32) << 8 + | (color.2 as u32) << 16 + | (color.3 as u32) << 24, + AnimationId: 0, + }; + + let mut data = WINDOWCOMPOSITIONATTRIBDATA { + Attrib: 0x13, + pvData: &mut policy as *mut _ as _, + cbData: std::mem::size_of_val(&policy), + }; + + set_window_composition_attribute(hwnd, &mut data as *mut _ as _); + } +} + +#[allow(unused)] +#[repr(C)] +enum DWM_SYSTEMBACKDROP_TYPE { + DWMSBT_DISABLE = 1, // None + DWMSBT_MAINWINDOW = 2, // Mica + DWMSBT_TRANSIENTWINDOW = 3, // Acrylic + DWMSBT_TABBEDWINDOW = 4, // Tabbed +} + +fn is_swca_supported() -> bool { + is_at_least_build(17763) +} + +fn is_undocumented_mica_supported() -> bool { + is_at_least_build(22000) +} + +fn is_backdroptype_supported() -> bool { + is_at_least_build(22523) +} + +fn is_at_least_build(build: u32) -> bool { + let v = windows_version().unwrap_or_default(); + v.2 >= build +} diff --git a/core/tauri/src/window.rs b/core/tauri/src/window.rs index 974a2ba4332..55a91154d4b 100644 --- a/core/tauri/src/window.rs +++ b/core/tauri/src/window.rs @@ -4,9 +4,8 @@ //! The Tauri window types and functions. -pub(crate) mod menu; - -pub use menu::{MenuEvent, MenuHandle}; +use http::HeaderMap; +pub use tauri_utils::{config::Color, WindowEffect as Effect, WindowEffectState as EffectState}; use url::Url; #[cfg(target_os = "macos")] @@ -15,24 +14,33 @@ use crate::{ app::AppHandle, command::{CommandArg, CommandItem}, event::{Event, EventHandler}, - hooks::{InvokePayload, InvokeResponder}, + ipc::{ + CallbackFn, Invoke, InvokeBody, InvokeError, InvokeMessage, InvokeResolver, InvokeResponse, + }, manager::WindowManager, runtime::{ http::{Request as HttpRequest, Response as HttpResponse}, - menu::Menu, monitor::Monitor as RuntimeMonitor, webview::{WebviewAttributes, WindowBuilder as _}, window::{ - dpi::{PhysicalPosition, PhysicalSize, Position, Size}, - DetachedWindow, JsEventListenerKey, PendingWindow, + dpi::{PhysicalPosition, PhysicalSize}, + DetachedWindow, PendingWindow, }, - Dispatch, RuntimeHandle, UserAttentionType, + Dispatch, RuntimeHandle, }, sealed::ManagerBase, sealed::RuntimeOrDispatch, - utils::config::{WindowConfig, WindowUrl}, - CursorIcon, EventLoopMessage, Icon, Invoke, InvokeError, InvokeMessage, InvokeResolver, Manager, - PageLoadPayload, Runtime, Theme, WindowEvent, + utils::config::{WindowConfig, WindowEffectsConfig, WindowUrl}, + EventLoopMessage, Manager, Runtime, Theme, WindowEvent, +}; +#[cfg(desktop)] +use crate::{ + menu::{ContextMenu, Menu, MenuId}, + runtime::{ + window::dpi::{Position, Size}, + UserAttentionType, + }, + CursorIcon, Icon, }; use serde::Serialize; @@ -42,14 +50,20 @@ use windows::Win32::Foundation::HWND; use tauri_macros::default_runtime; use std::{ + collections::{HashMap, HashSet}, fmt, hash::{Hash, Hasher}, path::PathBuf, - sync::Arc, + sync::{ + mpsc::{sync_channel, Receiver}, + Arc, Mutex, + }, }; pub(crate) type WebResourceRequestHandler = dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync; -pub(crate) type NavigationHandler = dyn Fn(Url) -> bool + Send; +pub(crate) type NavigationHandler = dyn Fn(&Url) -> bool + Send; +pub(crate) type UriSchemeProtocolHandler = + Box Result> + Send + Sync>; #[derive(Clone, Serialize)] struct WindowCreatedEvent { @@ -108,9 +122,13 @@ pub struct WindowBuilder<'a, R: Runtime> { app_handle: AppHandle, label: String, pub(crate) window_builder: >::WindowBuilder, + #[cfg(desktop)] + pub(crate) menu: Option>, pub(crate) webview_attributes: WebviewAttributes, web_resource_request_handler: Option>, navigation_handler: Option>, + #[cfg(desktop)] + on_menu_event: Option>>, } impl<'a, R: Runtime> fmt::Debug for WindowBuilder<'a, R> { @@ -151,7 +169,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { /// ``` /// tauri::Builder::default() /// .setup(|app| { - /// let handle = app.handle(); + /// let handle = app.handle().clone(); /// std::thread::spawn(move || { /// let window = tauri::WindowBuilder::new(&handle, "label", tauri::WindowUrl::App("index.html".into())) /// .build() @@ -175,16 +193,20 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583 pub fn new, L: Into>(manager: &'a M, label: L, url: WindowUrl) -> Self { let runtime = manager.runtime(); - let app_handle = manager.app_handle(); + let app_handle = manager.app_handle().clone(); Self { manager: manager.manager().clone(), runtime, app_handle, label: label.into(), window_builder: >::WindowBuilder::new(), + #[cfg(desktop)] + menu: None, webview_attributes: WebviewAttributes::new(url), web_resource_request_handler: None, navigation_handler: None, + #[cfg(desktop)] + on_menu_event: None, } } @@ -215,14 +237,18 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { let builder = Self { manager: manager.manager().clone(), runtime: manager.runtime(), - app_handle: manager.app_handle(), + app_handle: manager.app_handle().clone(), label: config.label.clone(), webview_attributes: WebviewAttributes::from(&config), window_builder: >::WindowBuilder::with_config( config, ), web_resource_request_handler: None, + #[cfg(desktop)] + menu: None, navigation_handler: None, + #[cfg(desktop)] + on_menu_event: None, }; builder @@ -295,11 +321,53 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { /// Ok(()) /// }); /// ``` - pub fn on_navigation bool + Send + 'static>(mut self, f: F) -> Self { + pub fn on_navigation bool + Send + 'static>(mut self, f: F) -> Self { self.navigation_handler.replace(Box::new(f)); self } + /// Registers a global menu event listener. + /// + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + /// + /// Also note that this handler will not be called if + /// the window used to register it was closed. + /// + /// # Examples + /// ``` + /// use tauri::menu::{Menu, Submenu, MenuItem}; + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle(); + /// let save_menu_item = MenuItem::new(handle, "Save", true, None); + /// let menu = Menu::with_items(handle, &[ + /// &Submenu::with_items(handle, "File", true, &[ + /// &save_menu_item, + /// ])?, + /// ])?; + /// let window = tauri::WindowBuilder::new(app, "editor", tauri::WindowUrl::default()) + /// .menu(menu) + /// .on_menu_event(move |window, event| { + /// if event.id == save_menu_item.id() { + /// // save menu item + /// } + /// }) + /// .build() + /// .unwrap(); + /// + /// Ok(()) + /// }); + /// ``` + #[cfg(desktop)] + pub fn on_menu_event, crate::menu::MenuEvent) + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self { + self.on_menu_event.replace(Box::new(f)); + self + } + /// Creates a new webview window. pub fn build(mut self) -> crate::Result> { let mut pending = PendingWindow::new( @@ -315,13 +383,46 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { .manager .prepare_window(self.app_handle.clone(), pending, &labels)?; + #[cfg(desktop)] + let window_menu = { + let is_app_wide = self.menu.is_none(); + self + .menu + .or_else(|| self.app_handle.menu()) + .map(|menu| WindowMenu { is_app_wide, menu }) + }; + + #[cfg(desktop)] + let handler = self + .manager + .prepare_window_menu_creation_handler(window_menu.as_ref()); + #[cfg(not(desktop))] + #[allow(clippy::type_complexity)] + let handler: Option) + Send>> = None; + + let window_effects = pending.webview_attributes.window_effects.clone(); let window = match &mut self.runtime { - RuntimeOrDispatch::Runtime(runtime) => runtime.create_window(pending), - RuntimeOrDispatch::RuntimeHandle(handle) => handle.create_window(pending), - RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_window(pending), + RuntimeOrDispatch::Runtime(runtime) => runtime.create_window(pending, handler), + RuntimeOrDispatch::RuntimeHandle(handle) => handle.create_window(pending, handler), + RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_window(pending, handler), + } + .map(|window| { + self.manager.attach_window( + self.app_handle.clone(), + window, + #[cfg(desktop)] + window_menu, + ) + })?; + + #[cfg(desktop)] + if let Some(handler) = self.on_menu_event { + window.on_menu_event(handler); } - .map(|window| self.manager.attach_window(self.app_handle.clone(), window))?; + if let Some(effects) = window_effects { + crate::vibrancy::set_window_effects(&window, Some(effects))?; + } 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())?, @@ -338,13 +439,15 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { Ok(window) } +} - // --------------------------------------------- Window builder --------------------------------------------- - +/// Desktop APIs. +#[cfg(desktop)] +impl<'a, R: Runtime> WindowBuilder<'a, R> { /// Sets the menu for the window. #[must_use] - pub fn menu(mut self, menu: Menu) -> Self { - self.window_builder = self.window_builder.menu(menu); + pub fn menu(mut self, menu: Menu) -> Self { + self.menu.replace(menu); self } @@ -512,6 +615,15 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { self } + /// Whether the window will be visible on all workspaces or virtual desktops. + #[must_use] + pub fn visible_on_all_workspaces(mut self, visible_on_all_workspaces: bool) -> Self { + self.window_builder = self + .window_builder + .visible_on_all_workspaces(visible_on_all_workspaces); + self + } + /// Prevents the window contents from being captured by other apps. #[must_use] pub fn content_protected(mut self, protected: bool) -> Self { @@ -536,6 +648,21 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { self } + /// Sets whether or not the window has shadow. + /// + /// ## Platform-specific + /// + /// - **Windows:** + /// - `false` has no effect on decorated window, shadows are always ON. + /// - `true` will make ndecorated window have a 1px white border, + /// and on Windows 11, it will have a rounded corners. + /// - **Linux:** Unsupported. + #[must_use] + pub fn shadow(mut self, enable: bool) -> Self { + self.window_builder = self.window_builder.shadow(enable); + self + } + /// Sets a parent to the window to be created. /// /// A child window has the WS_CHILD style and is confined to the client area of its parent window. @@ -600,8 +727,29 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { self } - // ------------------------------------------- Webview attributes ------------------------------------------- + /// Sets whether clicking an inactive window also clicks through to the webview. + #[must_use] + pub fn accept_first_mouse(mut self, accept: bool) -> Self { + self.webview_attributes.accept_first_mouse = accept; + self + } + + /// Sets window effects. + /// + /// Requires the window to be transparent. + /// + /// ## Platform-specific: + /// + /// - **Windows**: If using decorations or shadows, you may want to try this workaround + /// - **Linux**: Unsupported + pub fn effects(mut self, effects: WindowEffectsConfig) -> Self { + self.webview_attributes = self.webview_attributes.window_effects(effects); + self + } +} +/// Webview attributes. +impl<'a, R: Runtime> WindowBuilder<'a, R> { /// Adds the provided JavaScript to a list of scripts that should be run after the global object has been created, /// but before the HTML document has been parsed and before any other script included by the HTML document is run. /// @@ -690,30 +838,77 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { self } - /// Sets whether clicking an inactive window also clicks through to the webview. + /// Enable or disable incognito mode for the WebView.. + /// + /// ## Platform-specific: + /// + /// **Android**: Unsupported. #[must_use] - pub fn accept_first_mouse(mut self, accept: bool) -> Self { - self.webview_attributes.accept_first_mouse = accept; + pub fn incognito(mut self, incognito: bool) -> Self { + self.webview_attributes.incognito = incognito; self } } +/// Key for a JS event listener. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct JsEventListenerKey { + /// The associated window label. + pub window_label: Option, + /// The event name. + pub event: String, +} + +/// The IPC invoke request. +#[derive(Debug)] +pub struct InvokeRequest { + /// The invoke command. + pub cmd: String, + /// The success callback. + pub callback: CallbackFn, + /// The error callback. + pub error: CallbackFn, + /// The body of the request. + pub body: InvokeBody, + /// The request headers. + pub headers: HeaderMap, +} + +/// A wrapper struct to hold the window menu state +/// and whether it is global per-app or specific to this window. +#[cfg(desktop)] +pub(crate) struct WindowMenu { + pub(crate) is_app_wide: bool, + pub(crate) menu: Menu, +} + // TODO: expand these docs since this is a pretty important type /// A webview window managed by Tauri. /// /// This type also implements [`Manager`] which allows you to manage other windows attached to /// the same application. #[default_runtime(crate::Wry, wry)] -#[derive(Debug)] pub struct Window { /// The webview window created by the runtime. pub(crate) window: DetachedWindow, /// The manager to associate this webview window with. manager: WindowManager, pub(crate) app_handle: AppHandle, + js_event_listeners: Arc>>>, + // The menu set for this window + #[cfg(desktop)] + pub(crate) menu: Arc>>>, +} - #[cfg(test)] - pub(crate) current_url: url::Url, +impl std::fmt::Debug for Window { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Window") + .field("window", &self.window) + .field("manager", &self.manager) + .field("app_handle", &self.app_handle) + .field("js_event_listeners", &self.js_event_listeners) + .finish() + } } unsafe impl raw_window_handle::HasRawWindowHandle for Window { @@ -728,8 +923,9 @@ impl Clone for Window { window: self.window.clone(), manager: self.manager.clone(), app_handle: self.app_handle.clone(), - #[cfg(test)] - current_url: self.current_url.clone(), + js_event_listeners: self.js_event_listeners.clone(), + #[cfg(desktop)] + menu: self.menu.clone(), } } } @@ -776,8 +972,8 @@ impl ManagerBase for Window { RuntimeOrDispatch::Dispatch(self.dispatcher()) } - fn managed_app_handle(&self) -> AppHandle { - self.app_handle.clone() + fn managed_app_handle(&self) -> &AppHandle { + &self.app_handle } } @@ -789,11 +985,11 @@ impl<'de, R: Runtime> CommandArg<'de, R> for Window { } /// The platform webview handle. Accessed with [`Window#method.with_webview`]; -#[cfg(all(desktop, feature = "wry"))] -#[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "wry"))))] +#[cfg(feature = "wry")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "wry")))] pub struct PlatformWebview(tauri_runtime_wry::Webview); -#[cfg(all(desktop, feature = "wry"))] +#[cfg(feature = "wry")] impl PlatformWebview { /// Returns [`webkit2gtk::WebView`] handle. #[cfg(any( @@ -829,8 +1025,8 @@ impl PlatformWebview { /// Returns the [WKWebView] handle. /// /// [WKWebView]: https://developer.apple.com/documentation/webkit/wkwebview - #[cfg(target_os = "macos")] - #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))] + #[cfg(any(target_os = "macos", target_os = "ios"))] + #[cfg_attr(doc_cfg, doc(cfg(any(target_os = "macos", target_os = "ios"))))] pub fn inner(&self) -> cocoa::base::id { self.0.webview } @@ -838,8 +1034,8 @@ impl PlatformWebview { /// Returns WKWebView [controller] handle. /// /// [controller]: https://developer.apple.com/documentation/webkit/wkusercontentcontroller - #[cfg(target_os = "macos")] - #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))] + #[cfg(any(target_os = "macos", target_os = "ios"))] + #[cfg_attr(doc_cfg, doc(cfg(any(target_os = "macos", target_os = "ios"))))] pub fn controller(&self) -> cocoa::base::id { self.0.manager } @@ -852,11 +1048,80 @@ impl PlatformWebview { pub fn ns_window(&self) -> cocoa::base::id { self.0.ns_window } + + /// Returns [UIViewController] used by the WKWebView webview NSWindow. + /// + /// [UIViewController]: https://developer.apple.com/documentation/uikit/uiviewcontroller + #[cfg(target_os = "ios")] + #[cfg_attr(doc_cfg, doc(cfg(target_os = "ios")))] + pub fn view_controller(&self) -> cocoa::base::id { + self.0.view_controller + } + + /// Returns handle for JNI execution. + #[cfg(target_os = "android")] + pub fn jni_handle(&self) -> tauri_runtime_wry::wry::webview::JniHandle { + self.0 + } } -/// APIs specific to the wry runtime. -#[cfg(feature = "wry")] -impl Window { +/// Base window functions. +impl Window { + /// Create a new window that is attached to the manager. + pub(crate) fn new( + manager: WindowManager, + window: DetachedWindow, + app_handle: AppHandle, + #[cfg(desktop)] menu: Option>, + ) -> Self { + Self { + window, + manager, + app_handle, + js_event_listeners: Default::default(), + #[cfg(desktop)] + menu: Arc::new(Mutex::new(menu)), + } + } + + /// Initializes a webview window builder with the given window label and URL to load on the webview. + /// + /// Data URLs are only supported with the `window-data-url` feature flag. + pub fn builder<'a, M: Manager, L: Into>( + manager: &'a M, + label: L, + url: WindowUrl, + ) -> WindowBuilder<'a, R> { + WindowBuilder::<'a, R>::new(manager, label.into(), url) + } + + /// The current window's dispatcher. + pub(crate) fn dispatcher(&self) -> R::Dispatcher { + self.window.dispatcher.clone() + } + + /// Runs the given closure on the main thread. + pub fn run_on_main_thread(&self, f: F) -> crate::Result<()> { + self + .window + .dispatcher + .run_on_main_thread(f) + .map_err(Into::into) + } + + /// The label of this window. + pub fn label(&self) -> &str { + &self.window.label + } + + /// Registers a window event listener. + pub fn on_window_event(&self, f: F) { + self + .window + .dispatcher + .on_window_event(move |event| f(&event.clone().into())); + } + /// Executes a closure, providing it with the webview handle that is specific to the current platform. /// /// The closure is executed on the main thread. @@ -895,13 +1160,21 @@ impl Window { /// let bg_color: cocoa::base::id = msg_send![class!(NSColor), colorWithDeviceRed:0.5 green:0.2 blue:0.4 alpha:1.]; /// let () = msg_send![webview.ns_window(), setBackgroundColor: bg_color]; /// } + /// + /// #[cfg(target_os = "android")] + /// { + /// use jni::objects::JValue; + /// webview.jni_handle().exec(|env, _, webview| { + /// env.call_method(webview, "zoomBy", "(F)V", &[JValue::Float(4.)]).unwrap(); + /// }) + /// } /// }); /// Ok(()) /// }); /// } /// ``` - #[cfg(desktop)] - #[cfg_attr(doc_cfg, doc(cfg(all(feature = "wry", desktop))))] + #[cfg(feature = "wry")] + #[cfg_attr(doc_cfg, doc(feature = "wry"))] pub fn with_webview( &self, f: F, @@ -909,95 +1182,275 @@ impl Window { self .window .dispatcher - .with_webview(|w| f(PlatformWebview(w))) + .with_webview(|w| f(PlatformWebview(*w.downcast().unwrap()))) .map_err(Into::into) } } -/// Base window functions. +/// Menu APIs +#[cfg(desktop)] impl Window { - /// Create a new window that is attached to the manager. - pub(crate) fn new( - manager: WindowManager, - window: DetachedWindow, - app_handle: AppHandle, - ) -> Self { - Self { - window, - manager, - app_handle, - #[cfg(test)] - current_url: "http://tauri.app".parse().unwrap(), - } - } - - /// Initializes a webview window builder with the given window label and URL to load on the webview. + /// Registers a global menu event listener. /// - /// Data URLs are only supported with the `window-data-url` feature flag. - pub fn builder<'a, M: Manager, L: Into>( - manager: &'a M, - label: L, - url: WindowUrl, - ) -> WindowBuilder<'a, R> { - WindowBuilder::<'a, R>::new(manager, label.into(), url) + /// Note that this handler is called for any menu event, + /// whether it is coming from this window, another window or from the tray icon menu. + /// + /// Also note that this handler will not be called if + /// the window used to register it was closed. + /// + /// # Examples + /// ``` + /// use tauri::menu::{Menu, Submenu, MenuItem}; + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle(); + /// let save_menu_item = MenuItem::new(handle, "Save", true, None); + /// let menu = Menu::with_items(handle, &[ + /// &Submenu::with_items(handle, "File", true, &[ + /// &save_menu_item, + /// ])?, + /// ])?; + /// let window = tauri::WindowBuilder::new(app, "editor", tauri::WindowUrl::default()) + /// .menu(menu) + /// .build() + /// .unwrap(); + /// + /// window.on_menu_event(move |window, event| { + /// if event.id == save_menu_item.id() { + /// // save menu item + /// } + /// }); + /// + /// Ok(()) + /// }); + /// ``` + pub fn on_menu_event, crate::menu::MenuEvent) + Send + Sync + 'static>( + &self, + f: F, + ) { + self + .manager + .inner + .window_menu_event_listeners + .lock() + .unwrap() + .insert(self.label().to_string(), Box::new(f)); } - pub(crate) fn invoke_responder(&self) -> Arc> { - self.manager.invoke_responder() + pub(crate) fn menu_lock(&self) -> std::sync::MutexGuard<'_, Option>> { + self.menu.lock().expect("poisoned window") } - /// The current window's dispatcher. - pub(crate) fn dispatcher(&self) -> R::Dispatcher { - self.window.dispatcher.clone() + #[cfg_attr(target_os = "macos", allow(dead_code))] + pub(crate) fn has_app_wide_menu(&self) -> bool { + self + .menu_lock() + .as_ref() + .map(|m| m.is_app_wide) + .unwrap_or(false) } - /// Runs the given closure on the main thread. - pub fn run_on_main_thread(&self, f: F) -> crate::Result<()> { + #[cfg_attr(target_os = "macos", allow(dead_code))] + pub(crate) fn is_menu_in_use>(&self, id: &I) -> bool { self - .window - .dispatcher - .run_on_main_thread(f) - .map_err(Into::into) + .menu_lock() + .as_ref() + .map(|m| id.eq(m.menu.id())) + .unwrap_or(false) } - /// The label of this window. - pub fn label(&self) -> &str { - &self.window.label + /// Returns this window menu . + pub fn menu(&self) -> Option> { + self.menu_lock().as_ref().map(|m| m.menu.clone()) } - /// Registers a window event listener. - pub fn on_window_event(&self, f: F) { + /// Sets the window menu and returns the previous one. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one + /// window, if you need to set it, use [`AppHandle::set_menu`] instead. + #[cfg_attr(target_os = "macos", allow(unused_variables))] + pub fn set_menu(&self, menu: Menu) -> crate::Result>> { + let prev_menu = self.remove_menu()?; + + self.manager.insert_menu_into_stash(&menu); + + let window = self.clone(); + let menu_ = menu.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let _ = menu_.inner().init_for_hwnd(hwnd.0); + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let (Ok(gtk_window), Ok(gtk_box)) = (window.gtk_window(), window.default_vbox()) { + let _ = menu_ + .inner() + .init_for_gtk_window(>k_window, Some(>k_box)); + } + })?; + + self.menu_lock().replace(WindowMenu { + is_app_wide: false, + menu, + }); + + Ok(prev_menu) + } + + /// Removes the window menu and returns it. + /// + /// ## Platform-specific: + /// + /// - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one + /// window, if you need to remove it, use [`AppHandle::remove_menu`] instead. + pub fn remove_menu(&self) -> crate::Result>> { + let current_menu = self.menu_lock().as_ref().map(|m| m.menu.clone()); + + // remove from the window + #[cfg_attr(target_os = "macos", allow(unused_variables))] + if let Some(menu) = current_menu { + let window = self.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let _ = menu.inner().remove_for_hwnd(hwnd.0); + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(gtk_window) = window.gtk_window() { + let _ = menu.inner().remove_for_gtk_window(>k_window); + } + })?; + } + + let prev_menu = self.menu_lock().take().map(|m| m.menu); + self - .window - .dispatcher - .on_window_event(move |event| f(&event.clone().into())); + .manager + .remove_menu_from_stash_by_id(prev_menu.as_ref().map(|m| m.id())); + + Ok(prev_menu) + } + + /// Hides the window menu. + pub fn hide_menu(&self) -> crate::Result<()> { + // remove from the window + #[cfg_attr(target_os = "macos", allow(unused_variables))] + if let Some(window_menu) = &*self.menu_lock() { + let window = self.clone(); + let menu_ = window_menu.menu.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let _ = menu_.inner().hide_for_hwnd(hwnd.0); + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(gtk_window) = window.gtk_window() { + let _ = menu_.inner().hide_for_gtk_window(>k_window); + } + })?; + } + + Ok(()) } - /// Registers a menu event listener. - pub fn on_menu_event(&self, f: F) -> uuid::Uuid { - let menu_ids = self.window.menu_ids.clone(); - self.window.dispatcher.on_menu_event(move |event| { - let id = menu_ids - .lock() - .unwrap() - .get(&event.menu_item_id) - .unwrap() - .clone(); - f(MenuEvent { menu_item_id: id }) - }) + /// Shows the window menu. + pub fn show_menu(&self) -> crate::Result<()> { + // remove from the window + #[cfg_attr(target_os = "macos", allow(unused_variables))] + if let Some(window_menu) = &*self.menu_lock() { + let window = self.clone(); + let menu_ = window_menu.menu.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let _ = menu_.inner().show_for_hwnd(hwnd.0); + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(gtk_window) = window.gtk_window() { + let _ = menu_.inner().show_for_gtk_window(>k_window); + } + })?; + } + + Ok(()) } -} -/// Window getters. -impl Window { - /// Gets a handle to the window menu. - pub fn menu_handle(&self) -> MenuHandle { - MenuHandle { - ids: self.window.menu_ids.clone(), - dispatcher: self.dispatcher(), + /// Shows the window menu. + pub fn is_menu_visible(&self) -> crate::Result { + // remove from the window + #[cfg_attr(target_os = "macos", allow(unused_variables))] + if let Some(window_menu) = &*self.menu_lock() { + let (tx, rx) = std::sync::mpsc::channel(); + let window = self.clone(); + let menu_ = window_menu.menu.clone(); + self.run_on_main_thread(move || { + #[cfg(windows)] + if let Ok(hwnd) = window.hwnd() { + let _ = tx.send(menu_.inner().is_visible_on_hwnd(hwnd.0)); + } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + if let Ok(gtk_window) = window.gtk_window() { + let _ = tx.send(menu_.inner().is_visible_on_gtk_window(>k_window)); + } + })?; + + return Ok(rx.recv().unwrap_or(false)); } + + Ok(false) + } + + /// Shows the specified menu as a context menu at the cursor position. + pub fn popup_menu(&self, menu: &M) -> crate::Result<()> { + menu.popup(self.clone()) } + /// Shows the specified menu as a context menu at the specified position. + /// + /// The position is relative to the window's top-left corner. + pub fn popup_menu_at>( + &self, + menu: &M, + position: P, + ) -> crate::Result<()> { + menu.popup_at(self.clone(), position) + } +} + +/// Window getters. +impl Window { /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa. pub fn scale_factor(&self) -> crate::Result { self.window.dispatcher.scale_factor().map_err(Into::into) @@ -1145,6 +1598,23 @@ impl Window { }) } + /// Returns the pointer to the content view of this window. + #[cfg(target_os = "macos")] + pub fn ns_view(&self) -> crate::Result<*mut std::ffi::c_void> { + self + .window + .dispatcher + .raw_window_handle() + .map_err(Into::into) + .and_then(|handle| { + if let raw_window_handle::RawWindowHandle::AppKit(h) = handle { + Ok(h.ns_view) + } else { + Err(crate::Error::InvalidWindowHandle) + } + }) + } + /// Returns the native handle that is used by this window. #[cfg(windows)] pub fn hwnd(&self) -> crate::Result { @@ -1164,7 +1634,7 @@ impl Window { /// Returns the `ApplicationWindow` from gtk crate that is used by this window. /// - /// Note that this can only be used on the main thread. + /// Note that this type can only be used on the main thread. #[cfg(any( target_os = "linux", target_os = "dragonfly", @@ -1176,6 +1646,20 @@ impl Window { self.window.dispatcher.gtk_window().map_err(Into::into) } + /// Returns the vertical [`gtk::Box`] that is added by default as the sole child of this window. + /// + /// Note that this type can only be used on the main thread. + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + pub fn default_vbox(&self) -> crate::Result { + self.window.dispatcher.default_vbox().map_err(Into::into) + } + /// Returns the current window theme. /// /// ## Platform-specific @@ -1186,7 +1670,8 @@ impl Window { } } -/// Window setters and actions. +/// Desktop window setters and actions. +#[cfg(desktop)] impl Window { /// Centers the window. pub fn center(&self) -> crate::Result<()> { @@ -1336,6 +1821,59 @@ impl Window { .map_err(Into::into) } + /// Determines if this window should have shadow. + /// + /// ## Platform-specific + /// + /// - **Windows:** + /// - `false` has no effect on decorated window, shadow are always ON. + /// - `true` will make ndecorated window have a 1px white border, + /// and on Windows 11, it will have a rounded corners. + /// - **Linux:** Unsupported. + pub fn set_shadow(&self, enable: bool) -> crate::Result<()> { + self + .window + .dispatcher + .set_shadow(enable) + .map_err(Into::into) + } + + /// Sets window effects, pass [`None`] to clear any effects applied if possible. + /// + /// Requires the window to be transparent. + /// + /// See [`EffectsBuilder`] for a convenient builder for [`WindowEffectsConfig`]. + /// + /// + /// ```rust,no_run + /// use tauri::{Manager, window::{Color, Effect, EffectState, EffectsBuilder}}; + /// tauri::Builder::default() + /// .setup(|app| { + /// let window = app.get_window("main").unwrap(); + /// window.set_effects( + /// EffectsBuilder::new() + /// .effect(Effect::Popover) + /// .state(EffectState::Active) + /// .radius(5.) + /// .color(Color(0, 0, 0, 255)) + /// .build(), + /// )?; + /// Ok(()) + /// }); + /// ``` + /// + /// ## Platform-specific: + /// + /// - **Windows**: If using decorations or shadows, you may want to try this workaround + /// - **Linux**: Unsupported + pub fn set_effects>>(&self, effects: E) -> crate::Result<()> { + let effects = effects.into(); + let window = self.clone(); + self.run_on_main_thread(move || { + let _ = crate::vibrancy::set_window_effects(&window, effects); + }) + } + /// Determines if this window should always be on top of other windows. pub fn set_always_on_top(&self, always_on_top: bool) -> crate::Result<()> { self @@ -1345,6 +1883,18 @@ impl Window { .map_err(Into::into) } + /// Sets whether the window should be visible on all workspaces or virtual desktops. + pub fn set_visible_on_all_workspaces( + &self, + visible_on_all_workspaces: bool, + ) -> crate::Result<()> { + self + .window + .dispatcher + .set_visible_on_all_workspaces(visible_on_all_workspaces) + .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 @@ -1497,27 +2047,30 @@ impl Window { impl Window { /// Returns the current url of the webview. // TODO: in v2, change this type to Result - #[cfg(not(test))] pub fn url(&self) -> Url { self.window.dispatcher.url().unwrap() } - #[cfg(test)] - pub fn url(&self) -> Url { - self.current_url.clone() + /// Navigates the webview to the defined url. + pub fn navigate(&mut self, url: Url) { + self.window.dispatcher.navigate(url).unwrap(); } - #[cfg(test)] - pub(crate) fn navigate(&mut self, url: Url) { - self.current_url = url; + fn is_local_url(&self, current_url: &Url) -> bool { + self.manager.get_url().make_relative(current_url).is_some() + || { + let protocol_url = self.manager.protocol_url(); + current_url.scheme() == protocol_url.scheme() + && current_url.domain() == protocol_url.domain() + } + || (cfg!(dev) && current_url.domain() == Some("tauri.localhost")) } - /// Handles this window receiving an [`InvokeMessage`]. - pub fn on_message(self, payload: InvokePayload) -> crate::Result<()> { + /// Handles this window receiving an [`InvokeRequest`]. + pub fn on_message(self, request: InvokeRequest) -> Receiver { let manager = self.manager.clone(); let current_url = self.url(); - let config_url = manager.get_url(); - let is_local = config_url.make_relative(¤t_url).is_some(); + let is_local = self.is_local_url(¤t_url); let mut scope_not_found_error_message = ipc_scope_not_found_error_message(&self.window.label, current_url.as_str()); @@ -1536,57 +2089,187 @@ impl Window { } } }; - match payload.cmd.as_str() { - "__initialized" => { - let payload: PageLoadPayload = serde_json::from_value(payload.inner)?; - manager.run_on_page_load(self, payload); - } + + let (tx, rx) = sync_channel(1); + + let custom_responder = self.manager.invoke_responder(); + + let resolver = InvokeResolver::new( + self.clone(), + Arc::new( + #[allow(unused_variables)] + move |window: Window, cmd, response, callback, error| { + #[cfg(not(ipc_custom_protocol))] + { + use crate::ipc::{ + format_callback::{ + format as format_callback, format_result as format_callback_result, + }, + Channel, + }; + use serde_json::Value as JsonValue; + + // the channel data command is the only command that uses a custom protocol on Linux + if custom_responder.is_none() && cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND + { + fn responder_eval( + window: &Window, + js: crate::api::Result, + error: CallbackFn, + ) { + let eval_js = match js { + Ok(js) => js, + Err(e) => format_callback(error, &e.to_string()) + .expect("unable to serialize response error string to json"), + }; + + let _ = window.eval(&eval_js); + } + + match &response { + InvokeResponse::Ok(InvokeBody::Json(v)) => { + if matches!(v, JsonValue::Object(_) | JsonValue::Array(_)) { + let _ = Channel::from_ipc(window.clone(), callback).send(v); + } else { + responder_eval( + &window, + format_callback_result(Result::<_, ()>::Ok(v), callback, error), + error, + ) + } + } + InvokeResponse::Ok(InvokeBody::Raw(v)) => { + let _ = + Channel::from_ipc(window.clone(), callback).send(InvokeBody::Raw(v.clone())); + } + InvokeResponse::Err(e) => responder_eval( + &window, + format_callback_result(Result::<(), _>::Err(&e.0), callback, error), + error, + ), + } + } + } + + if let Some(responder) = &custom_responder { + (responder)(window, cmd, &response, callback, error); + } + + let _ = tx.send(response); + }, + ), + request.cmd.clone(), + request.callback, + request.error, + ); + + match request.cmd.as_str() { + "__initialized" => match request.body.deserialize() { + Ok(payload) => { + manager.run_on_page_load(self, payload); + resolver.resolve(()); + } + Err(e) => resolver.reject(e.to_string()), + }, _ => { + #[cfg(mobile)] + let app_handle = self.app_handle.clone(); + let message = InvokeMessage::new( - self.clone(), + self, manager.state(), - payload.cmd.to_string(), - payload.inner, + request.cmd.to_string(), + request.body, + request.headers, ); - let resolver = InvokeResolver::new(self, payload.callback, payload.error); - let invoke = Invoke { message, resolver }; + + let mut invoke = Invoke { + message, + resolver: resolver.clone(), + }; if !is_local && scope.is_none() { invoke.resolver.reject(scope_not_found_error_message); - return Ok(()); - } - - if let Some(module) = &payload.tauri_module { - if !is_local && scope.map(|s| !s.enables_tauri_api()).unwrap_or_default() { + } else if request.cmd.starts_with("plugin:") { + let command = invoke.message.command.replace("plugin:", ""); + let mut tokens = command.split('|'); + // safe to unwrap: split always has a least one item + let plugin = tokens.next().unwrap(); + invoke.message.command = tokens + .next() + .map(|c| c.to_string()) + .unwrap_or_else(String::new); + + if !(is_local + || plugin == crate::ipc::channel::CHANNEL_PLUGIN_NAME + || scope + .map(|s| s.plugins().contains(&plugin.into())) + .unwrap_or(true)) + { invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW); - return Ok(()); + return rx; } - crate::endpoints::handle( - module.to_string(), - invoke, - manager.config(), - manager.package_info(), - ); - } else if payload.cmd.starts_with("plugin:") { - if !is_local { - let command = invoke.message.command.replace("plugin:", ""); - let plugin_name = command.split('|').next().unwrap().to_string(); - if !scope - .map(|s| s.plugins().contains(&plugin_name)) - .unwrap_or(true) - { - invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW); - return Ok(()); + + let command = invoke.message.command.clone(); + + #[cfg(mobile)] + let message = invoke.message.clone(); + + #[allow(unused_mut)] + let mut handled = manager.extend_api(plugin, invoke); + + #[cfg(mobile)] + { + if !handled { + handled = true; + + fn load_channels(payload: &serde_json::Value, window: &Window) { + if let serde_json::Value::Object(map) = payload { + for v in map.values() { + if let serde_json::Value::String(s) = v { + if s.starts_with(crate::ipc::channel::IPC_PAYLOAD_PREFIX) { + crate::ipc::Channel::load_from_ipc(window.clone(), s); + } + } + } + } + } + + let payload = message.payload.into_json(); + // initialize channels + load_channels(&payload, &message.window); + + let resolver_ = resolver.clone(); + if let Err(e) = crate::plugin::mobile::run_command( + plugin, + &app_handle, + message.command, + payload, + move |response| match response { + Ok(r) => resolver_.resolve(r), + Err(e) => resolver_.reject(e), + }, + ) { + resolver.reject(e.to_string()); + return rx; + } } } - manager.extend_api(invoke); + + if !handled { + resolver.reject(format!("Command {command} not found")); + } } else { - manager.run_invoke_handler(invoke); + let command = invoke.message.command.clone(); + let handled = manager.run_invoke_handler(invoke); + if !handled { + resolver.reject(format!("Command {command} not found")); + } } } } - Ok(()) + rx } /// Evaluates JavaScript on this window. @@ -1594,9 +2277,24 @@ impl Window { self.window.dispatcher.eval_script(js).map_err(Into::into) } - pub(crate) fn register_js_listener(&self, window_label: Option, event: String, id: u32) { + /// Register a JS event listener and return its identifier. + pub(crate) fn listen_js( + &self, + window_label: Option, + event: String, + handler: CallbackFn, + ) -> crate::Result { + let event_id = rand::random(); + + self.eval(&crate::event::listen_js( + self.manager().event_listeners_object_name(), + format!("'{}'", event), + event_id, + window_label.clone(), + format!("window['_{}']", handler.0), + ))?; + self - .window .js_event_listeners .lock() .unwrap() @@ -1605,12 +2303,21 @@ impl Window { event, }) .or_insert_with(Default::default) - .insert(id); + .insert(event_id); + + Ok(event_id) } - pub(crate) fn unregister_js_listener(&self, id: u32) { + /// Unregister a JS event listener. + pub(crate) fn unlisten_js(&self, event: String, id: usize) -> crate::Result<()> { + self.eval(&crate::event::unlisten_js( + self.manager().event_listeners_object_name(), + event, + id, + ))?; + let mut empty = None; - let mut js_listeners = self.window.js_event_listeners.lock().unwrap(); + let mut js_listeners = self.js_event_listeners.lock().unwrap(); let iter = js_listeners.iter_mut(); for (key, ids) in iter { if ids.contains(&id) { @@ -1625,12 +2332,13 @@ impl Window { if let Some(key) = empty { js_listeners.remove(&key); } + + Ok(()) } /// Whether this window registered a listener to an event from the given window and event name. pub(crate) fn has_js_listener(&self, window_label: Option, event: &str) -> bool { self - .window .js_event_listeners .lock() .unwrap() @@ -1808,7 +2516,7 @@ impl Window { /// /// This listener only receives events that are triggered using the /// [`trigger`](Window#method.trigger) and [`emit_and_trigger`](Window#method.emit_and_trigger) methods or - /// the `appWindow.emit` function from the @tauri-apps/api `window` module. + /// the `emit` function from the window plugin (`@tauri-apps/plugin-window` package). /// /// # Examples /// ``` @@ -1893,6 +2601,61 @@ impl Window { } } +/// The [`WindowEffectsConfig`] object builder +#[derive(Default)] +pub struct EffectsBuilder(WindowEffectsConfig); +impl EffectsBuilder { + /// Create a new [`WindowEffectsConfig`] builder + pub fn new() -> Self { + Self(WindowEffectsConfig::default()) + } + + /// Adds effect to the [`WindowEffectsConfig`] `effects` field + pub fn effect(mut self, effect: Effect) -> Self { + self.0.effects.push(effect); + self + } + + /// Adds effects to the [`WindowEffectsConfig`] `effects` field + pub fn effects>(mut self, effects: I) -> Self { + self.0.effects.extend(effects); + self + } + + /// Clears the [`WindowEffectsConfig`] `effects` field + pub fn clear_effects(mut self) -> Self { + self.0.effects.clear(); + self + } + + /// Sets `state` field for the [`WindowEffectsConfig`] **macOS Only** + pub fn state(mut self, state: EffectState) -> Self { + self.0.state = Some(state); + self + } + /// Sets `radius` field fo the [`WindowEffectsConfig`] **macOS Only** + pub fn radius(mut self, radius: f64) -> Self { + self.0.radius = Some(radius); + self + } + /// Sets `color` field fo the [`WindowEffectsConfig`] **Windows Only** + pub fn color(mut self, color: Color) -> Self { + self.0.color = Some(color); + self + } + + /// Builds a [`WindowEffectsConfig`] + pub fn build(self) -> WindowEffectsConfig { + self.0 + } +} + +impl From for EffectsBuilder { + fn from(value: WindowEffectsConfig) -> Self { + Self(value) + } +} + pub(crate) const IPC_SCOPE_DOES_NOT_ALLOW: &str = "Not allowed by the scope"; pub(crate) fn ipc_scope_not_found_error_message(label: &str, url: &str) -> String { diff --git a/core/tauri/src/window/menu.rs b/core/tauri/src/window/menu.rs deleted file mode 100644 index 6888822843a..00000000000 --- a/core/tauri/src/window/menu.rs +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use crate::{ - runtime::{ - menu::{MenuHash, MenuId, MenuIdRef, MenuUpdate}, - Dispatch, - }, - Runtime, -}; - -use tauri_macros::default_runtime; - -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, -}; - -/// The window menu event. -#[derive(Debug, Clone)] -pub struct MenuEvent { - pub(crate) menu_item_id: MenuId, -} - -impl MenuEvent { - /// The menu item id. - pub fn menu_item_id(&self) -> MenuIdRef<'_> { - &self.menu_item_id - } -} - -/// A handle to a system tray. Allows updating the context menu items. -#[default_runtime(crate::Wry, wry)] -#[derive(Debug)] -pub struct MenuHandle { - pub(crate) ids: Arc>>, - pub(crate) dispatcher: R::Dispatcher, -} - -impl Clone for MenuHandle { - fn clone(&self) -> Self { - Self { - ids: self.ids.clone(), - dispatcher: self.dispatcher.clone(), - } - } -} - -/// A handle to a system tray menu item. -#[default_runtime(crate::Wry, wry)] -#[derive(Debug)] -pub struct MenuItemHandle { - id: u16, - dispatcher: R::Dispatcher, -} - -impl Clone for MenuItemHandle { - fn clone(&self) -> Self { - Self { - id: self.id, - dispatcher: self.dispatcher.clone(), - } - } -} - -impl MenuHandle { - /// Gets a handle to the menu item that has the specified `id`. - pub fn get_item(&self, id: MenuIdRef<'_>) -> MenuItemHandle { - let ids = self.ids.lock().unwrap(); - let iter = ids.iter(); - for (raw, item_id) in iter { - if item_id == id { - return MenuItemHandle { - id: *raw, - dispatcher: self.dispatcher.clone(), - }; - } - } - panic!("item id not found") - } - - /// Attempts to get a handle to the menu item that has the specified `id`, return an error if `id` is not found. - pub fn try_get_item(&self, id: MenuIdRef<'_>) -> Option> { - self - .ids - .lock() - .unwrap() - .iter() - .find(|i| i.1 == id) - .map(|i| MenuItemHandle { - id: *i.0, - dispatcher: self.dispatcher.clone(), - }) - } - - /// Shows the menu. - pub fn show(&self) -> crate::Result<()> { - self.dispatcher.show_menu().map_err(Into::into) - } - - /// Hides the menu. - pub fn hide(&self) -> crate::Result<()> { - self.dispatcher.hide_menu().map_err(Into::into) - } - - /// Whether the menu is visible or not. - pub fn is_visible(&self) -> crate::Result { - self.dispatcher.is_menu_visible().map_err(Into::into) - } - - /// Toggles the menu visibility. - pub fn toggle(&self) -> crate::Result<()> { - if self.is_visible()? { - self.hide() - } else { - self.show() - } - } -} - -impl MenuItemHandle { - /// Modifies the enabled state of the menu item. - pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> { - self - .dispatcher - .update_menu_item(self.id, MenuUpdate::SetEnabled(enabled)) - .map_err(Into::into) - } - - /// Modifies the title (label) of the menu item. - pub fn set_title>(&self, title: S) -> crate::Result<()> { - self - .dispatcher - .update_menu_item(self.id, MenuUpdate::SetTitle(title.into())) - .map_err(Into::into) - } - - /// Modifies the selected state of the menu item. - pub fn set_selected(&self, selected: bool) -> crate::Result<()> { - self - .dispatcher - .update_menu_item(self.id, MenuUpdate::SetSelected(selected)) - .map_err(Into::into) - } - - #[cfg(target_os = "macos")] - #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))] - pub fn set_native_image(&self, image: crate::NativeImage) -> crate::Result<()> { - self - .dispatcher - .update_menu_item(self.id, MenuUpdate::SetNativeImage(image)) - .map_err(Into::into) - } -} diff --git a/core/tauri/test/fixture/src-tauri/tauri.conf.json b/core/tauri/test/fixture/src-tauri/tauri.conf.json index 6b4473313d0..facec1a219b 100644 --- a/core/tauri/test/fixture/src-tauri/tauri.conf.json +++ b/core/tauri/test/fixture/src-tauri/tauri.conf.json @@ -9,19 +9,13 @@ "identifier": "studio.tauri.example", "active": true }, - "allowlist": { - "all": true - }, "windows": [ { "title": "Tauri App" } ], "security": { - "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'" - }, - "updater": { - "active": false + "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; connect-src ipc: https://ipc.localhost" } } -} +} \ No newline at end of file diff --git a/core/tests/app-updater/.gitignore b/core/tests/app-updater/.gitignore deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/core/tests/app-updater/Cargo.toml b/core/tests/app-updater/Cargo.toml deleted file mode 100644 index 641afabe83a..00000000000 --- a/core/tests/app-updater/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "app-updater" -version = "0.1.0" -edition = "2021" - -[build-dependencies] -tauri-build = { path = "../../tauri-build", features = [] } - -[dependencies] -serde = { version = "1", features = ["derive"] } -serde_json = "1" -tiny_http = "0.11" -tauri = { path = "../../tauri", features = ["updater"] } -time = { version = "0.3", features = ["formatting"] } - -[features] -default = ["custom-protocol"] -custom-protocol = ["tauri/custom-protocol"] diff --git a/core/tests/app-updater/src/main.rs b/core/tests/app-updater/src/main.rs deleted file mode 100644 index 94c27bcb4db..00000000000 --- a/core/tests/app-updater/src/main.rs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -fn main() { - let mut context = tauri::generate_context!(); - if std::env::var("TARGET").unwrap_or_default() == "nsis" { - // /D sets the default installation directory ($INSTDIR), - // overriding InstallDir and InstallDirRegKey. - // It must be the last parameter used in the command line and must not contain any quotes, even if the path contains spaces. - // Only absolute paths are supported. - // NOTE: we only need this because this is an integration test and we don't want to install the app in the programs folder - context.config_mut().tauri.updater.windows.installer_args = vec![format!( - "/D={}", - tauri::utils::platform::current_exe() - .unwrap() - .parent() - .unwrap() - .display() - )]; - } - tauri::Builder::default() - .setup(|app| { - let handle = app.handle(); - tauri::async_runtime::spawn(async move { - match handle.updater().check().await { - Ok(update) => { - if update.is_update_available() { - if let Err(e) = update.download_and_install().await { - println!("{e}"); - std::process::exit(1); - } - } - std::process::exit(0); - } - Err(e) => { - println!("{e}"); - std::process::exit(1); - } - } - }); - Ok(()) - }) - .run(context) - .expect("error while running tauri application"); -} diff --git a/core/tests/app-updater/tauri.conf.json b/core/tests/app-updater/tauri.conf.json deleted file mode 100644 index 8c3351c6343..00000000000 --- a/core/tests/app-updater/tauri.conf.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "../../../core/tauri-config-schema/schema.json", - "build": { - "distDir": [], - "devPath": [] - }, - "tauri": { - "bundle": { - "active": true, - "targets": "all", - "identifier": "com.tauri.updater", - "icon": [ - "../../../examples/.icons/32x32.png", - "../../../examples/.icons/128x128.png", - "../../../examples/.icons/128x128@2x.png", - "../../../examples/.icons/icon.icns", - "../../../examples/.icons/icon.ico" - ], - "category": "DeveloperTool", - "windows": { - "wix": { - "skipWebviewInstall": true - } - } - }, - "allowlist": { - "all": false - }, - "updater": { - "active": true, - "dialog": false, - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK", - "endpoints": ["http://localhost:3007"], - "windows": { - "installMode": "quiet" - } - } - } -} diff --git a/core/tests/app-updater/tests/update.rs b/core/tests/app-updater/tests/update.rs deleted file mode 100644 index fc32c3dfec1..00000000000 --- a/core/tests/app-updater/tests/update.rs +++ /dev/null @@ -1,317 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -#![allow(dead_code, unused_imports)] - -use std::{ - collections::HashMap, - fs::File, - path::{Path, PathBuf}, - process::Command, -}; - -use serde::Serialize; - -const UPDATER_PRIVATE_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5YTBGV3JiTy9lRDZVd3NkL0RoQ1htZmExNDd3RmJaNmRMT1ZGVjczWTBKZ0FBQkFBQUFBQUFBQUFBQUlBQUFBQWdMekUzVkE4K0tWQ1hjeGt1Vkx2QnRUR3pzQjVuV0ZpM2czWXNkRm9hVUxrVnB6TUN3K1NheHJMREhQbUVWVFZRK3NIL1VsMDBHNW5ET1EzQno0UStSb21nRW4vZlpTaXIwZFh5ZmRlL1lSN0dKcHdyOUVPclVvdzFhVkxDVnZrbHM2T1o4Tk1NWEU9Cg=="; - -#[derive(Serialize)] -struct PackageConfig { - version: &'static str, -} - -#[derive(Serialize)] -struct Config { - package: PackageConfig, -} - -#[derive(Serialize)] -struct PlatformUpdate { - signature: String, - url: &'static str, - with_elevated_task: bool, -} - -#[derive(Serialize)] -struct Update { - version: &'static str, - date: String, - platforms: HashMap, -} - -fn get_cli_bin_path(cli_dir: &Path, debug: bool) -> Option { - let mut cli_bin_path = cli_dir.join(format!( - "target/{}/cargo-tauri", - if debug { "debug" } else { "release" } - )); - if cfg!(windows) { - cli_bin_path.set_extension("exe"); - } - if cli_bin_path.exists() { - Some(cli_bin_path) - } else { - None - } -} - -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"]) - .arg("--config") - .arg(serde_json::to_string(config).unwrap()) - .current_dir(cwd); - - #[cfg(target_os = "linux")] - command.args(["--bundles", target.name()]); - #[cfg(target_os = "macos")] - 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 - .status() - .expect("failed to run Tauri CLI to bundle app"); - - if !status.code().map(|c| c == 0).unwrap_or(true) { - panic!("failed to bundle app {:?}", status.code()); - } -} - -#[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::AppImage; - #[cfg(windows)] - return Self::Nsis; - } -} - -#[cfg(target_os = "linux")] -fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> { - vec![( - BundleTarget::AppImage, - root_dir.join(format!( - "target/debug/bundle/appimage/app-updater_{version}_amd64.AppImage" - )), - )] -} - -#[cfg(target_os = "macos")] -fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> { - vec![( - BundleTarget::App, - root_dir.join("target/debug/bundle/macos/app-updater.app"), - )] -} - -#[cfg(target_os = "ios")] -fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> { - vec![( - BundleTarget::App, - root_dir.join("target/debug/bundle/ios/app-updater.app"), - )] -} - -#[cfg(windows)] -fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> { - vec![ - ( - BundleTarget::Nsis, - root_dir.join(format!( - "target/debug/bundle/nsis/app-updater_{version}_x64-setup.exe" - )), - ), - ( - BundleTarget::Msi, - root_dir.join(format!( - "target/debug/bundle/msi/app-updater_{version}_x64_en-US.msi" - )), - ), - ] -} - -#[test] -#[ignore] -fn update_app() { - let target = tauri::updater::target().expect("running updater test in an unsupported platform"); - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let root_dir = manifest_dir.join("../../.."); - let cli_dir = root_dir.join("tooling/cli"); - - let cli_bin_path = if let Some(p) = get_cli_bin_path(&cli_dir, false) { - p - } else if let Some(p) = get_cli_bin_path(&cli_dir, true) { - p - } else { - let status = Command::new("cargo") - .arg("build") - .current_dir(&cli_dir) - .status() - .expect("failed to run cargo"); - if !status.success() { - panic!("failed to build CLI"); - } - get_cli_bin_path(&cli_dir, true).expect("cargo did not build the Tauri CLI") - }; - - let mut config = Config { - package: PackageConfig { version: "1.0.0" }, - }; - - // bundle app update - build_app( - &cli_bin_path, - &manifest_dir, - &config, - true, - Default::default(), - ); - - let updater_zip_ext = if cfg!(windows) { "zip" } else { "tar.gz" }; - - for (bundle_target, out_bundle_path) in bundle_paths(&root_dir, "1.0.0") { - let bundle_updater_ext = out_bundle_path - .extension() - .unwrap() - .to_str() - .unwrap() - .replace("exe", "nsis"); - let signature_path = - out_bundle_path.with_extension(format!("{bundle_updater_ext}.{updater_zip_ext}.sig")); - let signature = std::fs::read_to_string(&signature_path) - .unwrap_or_else(|_| panic!("failed to read signature file {}", signature_path.display())); - let out_updater_path = - out_bundle_path.with_extension(format!("{}.{}", bundle_updater_ext, updater_zip_ext)); - let updater_path = root_dir.join(format!( - "target/debug/{}", - out_updater_path.file_name().unwrap().to_str().unwrap() - )); - std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle"); - - let target = target.clone(); - std::thread::spawn(move || { - // start the updater server - let server = - tiny_http::Server::http("localhost:3007").expect("failed to start updater server"); - - loop { - if let Ok(request) = server.recv() { - match request.url() { - "/" => { - let mut platforms = HashMap::new(); - - platforms.insert( - target.clone(), - PlatformUpdate { - signature: signature.clone(), - url: "http://localhost:3007/download", - with_elevated_task: false, - }, - ); - let body = serde_json::to_vec(&Update { - version: "1.0.0", - date: time::OffsetDateTime::now_utc() - .format(&time::format_description::well_known::Rfc3339) - .unwrap(), - platforms, - }) - .unwrap(); - let len = body.len(); - let response = tiny_http::Response::new( - tiny_http::StatusCode(200), - Vec::new(), - std::io::Cursor::new(body), - Some(len), - None, - ); - let _ = request.respond(response); - } - "/download" => { - let _ = request.respond(tiny_http::Response::from_file( - File::open(&updater_path).unwrap_or_else(|_| { - panic!("failed to open updater bundle {}", updater_path.display()) - }), - )); - // close server - return; - } - _ => (), - } - } - } - }); - - config.package.version = "0.1.0"; - - // bundle initial app version - build_app(&cli_bin_path, &manifest_dir, &config, false, 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"); - } - } -} diff --git a/core/tests/restart/src/main.rs b/core/tests/restart/src/main.rs index 32a1641bdc4..6984ab20c5d 100644 --- a/core/tests/restart/src/main.rs +++ b/core/tests/restart/src/main.rs @@ -13,8 +13,8 @@ fn main() { println!( "{}", - tauri::api::process::current_binary(&Default::default()) - .expect("tauri::api::process::current_binary could not resolve") + tauri::process::current_binary(&Default::default()) + .expect("tauri::process::current_binary could not resolve") .display() ); @@ -22,7 +22,7 @@ fn main() { Some("restart") => { let mut env = Env::default(); env.args.clear(); - tauri::api::process::restart(&env) + tauri::process::restart(&env) } Some(invalid) => panic!("only argument `restart` is allowed, {invalid} is invalid"), None => {} diff --git a/core/tests/restart/tests/restart.rs b/core/tests/restart/tests/restart.rs index 6f5d75ab562..a174f537a97 100644 --- a/core/tests/restart/tests/restart.rs +++ b/core/tests/restart/tests/restart.rs @@ -45,7 +45,7 @@ fn symlink_runner(create_symlinks: impl Fn(&Path) -> io::Result) -> Res // run the command from the symlink, so that we can test if restart resolves it correctly let mut cmd = Command::new(link); - // add the restart parameter so that the invocation will call tauri::api::process::restart + // add the restart parameter so that the invocation will call tauri::process::restart cmd.arg("restart"); let output = cmd.output()?; diff --git a/examples/api/dist/assets/index.css b/examples/api/dist/assets/index.css index 535be4489c3..b165b4364e6 100644 --- a/examples/api/dist/assets/index.css +++ b/examples/api/dist/assets/index.css @@ -1 +1 @@ -*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x:var(--un-empty,/*!*/ /*!*/);--un-pan-y:var(--un-empty,/*!*/ /*!*/);--un-pinch-zoom:var(--un-empty,/*!*/ /*!*/);--un-scroll-snap-strictness:proximity;--un-ordinal:var(--un-empty,/*!*/ /*!*/);--un-slashed-zero:var(--un-empty,/*!*/ /*!*/);--un-numeric-figure:var(--un-empty,/*!*/ /*!*/);--un-numeric-spacing:var(--un-empty,/*!*/ /*!*/);--un-numeric-fraction:var(--un-empty,/*!*/ /*!*/);--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 #0000;--un-ring-shadow:0 0 #0000;--un-shadow-inset:var(--un-empty,/*!*/ /*!*/);--un-shadow:0 0 #0000;--un-ring-inset:var(--un-empty,/*!*/ /*!*/);--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur:var(--un-empty,/*!*/ /*!*/);--un-brightness:var(--un-empty,/*!*/ /*!*/);--un-contrast:var(--un-empty,/*!*/ /*!*/);--un-drop-shadow:var(--un-empty,/*!*/ /*!*/);--un-grayscale:var(--un-empty,/*!*/ /*!*/);--un-hue-rotate:var(--un-empty,/*!*/ /*!*/);--un-invert:var(--un-empty,/*!*/ /*!*/);--un-saturate:var(--un-empty,/*!*/ /*!*/);--un-sepia:var(--un-empty,/*!*/ /*!*/);--un-backdrop-blur:var(--un-empty,/*!*/ /*!*/);--un-backdrop-brightness:var(--un-empty,/*!*/ /*!*/);--un-backdrop-contrast:var(--un-empty,/*!*/ /*!*/);--un-backdrop-grayscale:var(--un-empty,/*!*/ /*!*/);--un-backdrop-hue-rotate:var(--un-empty,/*!*/ /*!*/);--un-backdrop-invert:var(--un-empty,/*!*/ /*!*/);--un-backdrop-opacity:var(--un-empty,/*!*/ /*!*/);--un-backdrop-saturate:var(--un-empty,/*!*/ /*!*/);--un-backdrop-sepia:var(--un-empty,/*!*/ /*!*/);}@font-face { font-family: 'Fira Code'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/firacode/v21/uU9eCBsR6Z2vfE9aq3bL0fxyUs4tcw4W_D1sFVc.ttf) format('truetype');}@font-face { font-family: 'Fira Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/firamono/v14/N0bX2SlFPv1weGeLZDtQIQ.ttf) format('truetype');}@font-face { font-family: 'Fira Mono'; font-style: normal; font-weight: 700; font-display: swap; src: url(https://fonts.gstatic.com/s/firamono/v14/N0bS2SlFPv1weGeLZDtondv3mQ.ttf) format('truetype');}@font-face { font-family: 'Rubik'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/rubik/v26/iJWZBXyIfDnIV5PNhY1KTN7Z-Yh-B4i1UA.ttf) format('truetype');}.i-codicon-bell-dot{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath fill-rule='evenodd' d='M12.994 7.875A4.008 4.008 0 0 1 12 8h-.01v.217c0 .909.143 1.818.442 2.691l.371 1.113h-9.63v-.012l.37-1.113a8.633 8.633 0 0 0 .443-2.691V6.004c0-.563.12-1.113.347-1.616c.227-.514.55-.969.969-1.34c.419-.382.91-.67 1.436-.837c.538-.18 1.1-.24 1.65-.18l.12.018a4 4 0 0 1 .673-.887a5.15 5.15 0 0 0-.697-.135c-.694-.072-1.4 0-2.07.227c-.67.215-1.28.574-1.794 1.053a4.923 4.923 0 0 0-1.208 1.675a5.067 5.067 0 0 0-.431 2.022v2.2a7.61 7.61 0 0 1-.383 2.37L2 12.343l.479.658h3.505c0 .526.215 1.04.586 1.412c.37.37.885.586 1.412.586c.526 0 1.04-.215 1.411-.586s.587-.886.587-1.412h3.505l.478-.658l-.586-1.77a7.63 7.63 0 0 1-.383-2.381v-.318ZM7.982 14.02a.997.997 0 0 0 .706-.3a.939.939 0 0 0 .287-.705H6.977c0 .263.107.514.299.706a.999.999 0 0 0 .706.299Z' clip-rule='evenodd'/%3E%3Cpath d='M12 7a3 3 0 1 0 0-6a3 3 0 0 0 0 6Z'/%3E%3C/g%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-chrome-close{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' fill-rule='evenodd' d='m7.116 8l-4.558 4.558l.884.884L8 8.884l4.558 4.558l.884-.884L8.884 8l4.558-4.558l-.884-.884L8 7.116L3.442 2.558l-.884.884L7.116 8z' clip-rule='evenodd'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-chrome-maximize{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M3 3v10h10V3H3zm9 9H4V4h8v8z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-chrome-minimize{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M14 8v1H3V8h11z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-chrome-restore{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M3 5v9h9V5H3zm8 8H4V6h7v7z'/%3E%3Cpath fill-rule='evenodd' d='M5 5h1V4h7v7h-1v1h2V3H5v2z' clip-rule='evenodd'/%3E%3C/g%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-clear-all{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m10 12.6l.7.7l1.6-1.6l1.6 1.6l.8-.7L13 11l1.7-1.6l-.8-.8l-1.6 1.7l-1.6-1.7l-.7.8l1.6 1.6l-1.6 1.6zM1 4h14V3H1v1zm0 3h14V6H1v1zm8 2.5V9H1v1h8v-.5zM9 13v-1H1v1h8z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-clippy{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' fill-rule='evenodd' d='M7 13.992H4v-9h8v2h1v-2.5l-.5-.5H11v-1h-1a2 2 0 0 0-4 0H4.94v1H3.5l-.5.5v10l.5.5H7v-1zm0-11.2a1 1 0 0 1 .8-.8a1 1 0 0 1 .58.06a.94.94 0 0 1 .45.36a1 1 0 1 1-1.75.94a1 1 0 0 1-.08-.56zm7.08 9.46L13 13.342v-5.35h-1v5.34l-1.08-1.08l-.71.71l1.94 1.93h.71l1.93-1.93l-.71-.71zm-5.92-4.16h.71l1.93 1.93l-.71.71l-1.08-1.08v5.34h-1v-5.35l-1.08 1.09l-.71-.71l1.94-1.93z' clip-rule='evenodd'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-close{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' fill-rule='evenodd' d='m8 8.707l3.646 3.647l.708-.707L8.707 8l3.647-3.646l-.707-.708L8 7.293L4.354 3.646l-.707.708L7.293 8l-3.646 3.646l.707.708L8 8.707z' clip-rule='evenodd'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-cloud-download{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' fill-rule='evenodd' d='M11.957 6h.05a2.99 2.99 0 0 1 2.116.879a3.003 3.003 0 0 1 0 4.242a2.99 2.99 0 0 1-2.117.879v-1a2.002 2.002 0 0 0 0-4h-.914l-.123-.857a2.49 2.49 0 0 0-2.126-2.122A2.478 2.478 0 0 0 6.231 5.5l-.333.762l-.809-.189A2.49 2.49 0 0 0 4.523 6c-.662 0-1.297.263-1.764.732A2.503 2.503 0 0 0 4.523 11h.498v1h-.498a3.486 3.486 0 0 1-2.628-1.16a3.502 3.502 0 0 1 1.958-5.78a3.462 3.462 0 0 1 1.468.04a3.486 3.486 0 0 1 3.657-2.06A3.479 3.479 0 0 1 11.957 6zm-5.25 5.121l1.314 1.314V7h.994v5.4l1.278-1.279l.707.707l-2.146 2.147h-.708L6 11.829l.707-.708z' clip-rule='evenodd'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-files{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M17.5 0h-9L7 1.5V6H2.5L1 7.5v15.07L2.5 24h12.07L16 22.57V18h4.7l1.3-1.43V4.5L17.5 0zm0 2.12l2.38 2.38H17.5V2.12zm-3 20.38h-12v-15H7v9.07L8.5 18h6v4.5zm6-6h-12v-15H16V6h4.5v10.5z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-hubot{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' fill-rule='evenodd' d='M8.48 4h4l.5.5v2.03h.52l.5.5V8l-.5.5h-.52v3l-.5.5H9.36l-2.5 2.76L6 14.4V12H3.5l-.5-.64V8.5h-.5L2 8v-.97l.5-.5H3V4.36L3.53 4h4V2.86A1 1 0 0 1 7 2a1 1 0 0 1 2 0a1 1 0 0 1-.52.83V4zM12 8V5H4v5.86l2.5.14H7v2.19l1.8-2.04l.35-.15H12V8zm-2.12.51a2.71 2.71 0 0 1-1.37.74v-.01a2.71 2.71 0 0 1-2.42-.74l-.7.71c.34.34.745.608 1.19.79c.45.188.932.286 1.42.29a3.7 3.7 0 0 0 2.58-1.07l-.7-.71zM6.49 6.5h-1v1h1v-1zm3 0h1v1h-1v-1z' clip-rule='evenodd'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-link-external{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M1.5 1H6v1H2v12h12v-4h1v4.5l-.5.5h-13l-.5-.5v-13l.5-.5z'/%3E%3Cpath d='M15 1.5V8h-1V2.707L7.243 9.465l-.707-.708L13.293 2H8V1h6.5l.5.5z'/%3E%3C/g%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-menu{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M16 5H0V4h16v1zm0 8H0v-1h16v1zm0-4.008H0V8h16v.992z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-multiple-windows{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' fill-rule='evenodd' d='m6 1.5l.5-.5h8l.5.5v7l-.5.5H12V8h2V4H7v1H6V1.5zM7 2v1h7V2H7zM1.5 7l-.5.5v7l.5.5h8l.5-.5v-7L9.5 7h-8zM2 9V8h7v1H2zm0 1h7v4H2v-4z' clip-rule='evenodd'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-radio-tower{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' fill-rule='evenodd' d='M2.998 5.58a5.55 5.55 0 0 1 1.62-3.88l-.71-.7a6.45 6.45 0 0 0 0 9.16l.71-.7a5.55 5.55 0 0 1-1.62-3.88zm1.06 0a4.42 4.42 0 0 0 1.32 3.17l.71-.71a3.27 3.27 0 0 1-.76-1.12a3.45 3.45 0 0 1 0-2.67a3.22 3.22 0 0 1 .76-1.13l-.71-.71a4.46 4.46 0 0 0-1.32 3.17zm7.65 3.21l-.71-.71c.33-.32.59-.704.76-1.13a3.449 3.449 0 0 0 0-2.67a3.22 3.22 0 0 0-.76-1.13l.71-.7a4.468 4.468 0 0 1 0 6.34zM13.068 1l-.71.71a5.43 5.43 0 0 1 0 7.74l.71.71a6.45 6.45 0 0 0 0-9.16zM9.993 5.43a1.5 1.5 0 0 1-.245.98a2 2 0 0 1-.27.23l3.44 7.73l-.92.4l-.77-1.73h-5.54l-.77 1.73l-.92-.4l3.44-7.73a1.52 1.52 0 0 1-.33-1.63a1.55 1.55 0 0 1 .56-.68a1.5 1.5 0 0 1 2.325 1.1zm-1.595-.34a.52.52 0 0 0-.25.14a.52.52 0 0 0-.11.22a.48.48 0 0 0 0 .29c.04.09.102.17.18.23a.54.54 0 0 0 .28.08a.51.51 0 0 0 .5-.5a.54.54 0 0 0-.08-.28a.58.58 0 0 0-.23-.18a.48.48 0 0 0-.29 0zm.23 2.05h-.27l-.87 1.94h2l-.86-1.94zm2.2 4.94l-.89-2h-2.88l-.89 2h4.66z' clip-rule='evenodd'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-record-keys{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' fill-rule='evenodd' d='M14 3H3a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm0 8H3V4h11v7zm-3-6h-1v1h1V5zm-1 2H9v1h1V7zm2-2h1v1h-1V5zm1 4h-1v1h1V9zM6 9h5v1H6V9zm7-2h-2v1h2V7zM8 5h1v1H8V5zm0 2H7v1h1V7zM4 9h1v1H4V9zm0-4h1v1H4V5zm3 0H6v1h1V5zM4 7h2v1H4V7z' clip-rule='evenodd'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-terminal{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 24 24' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' fill-rule='evenodd' d='M3 1.5L1.5 3v18L3 22.5h18l1.5-1.5V3L21 1.5H3zM3 21V3h18v18H3zm5.656-4.01l1.038 1.061l5.26-5.243v-.912l-5.26-5.26l-1.035 1.06l4.59 4.702l-4.593 4.592z' clip-rule='evenodd'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-terminal-bash{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M13.655 3.56L8.918.75a1.785 1.785 0 0 0-1.82 0L2.363 3.56a1.889 1.889 0 0 0-.921 1.628v5.624a1.889 1.889 0 0 0 .913 1.627l4.736 2.812a1.785 1.785 0 0 0 1.82 0l4.736-2.812a1.888 1.888 0 0 0 .913-1.627V5.188a1.889 1.889 0 0 0-.904-1.627zm-3.669 8.781v.404a.149.149 0 0 1-.07.124l-.239.137c-.038.02-.07 0-.07-.053v-.396a.78.78 0 0 1-.545.053a.073.073 0 0 1-.027-.09l.086-.365a.153.153 0 0 1 .071-.096a.048.048 0 0 1 .038 0a.662.662 0 0 0 .497-.063a.662.662 0 0 0 .37-.567c0-.206-.112-.292-.384-.293c-.344 0-.661-.066-.67-.574A1.47 1.47 0 0 1 9.6 9.437V9.03a.147.147 0 0 1 .07-.126l.231-.147c.038-.02.07 0 .07.054v.409a.754.754 0 0 1 .453-.055a.073.073 0 0 1 .03.095l-.081.362a.156.156 0 0 1-.065.09a.055.055 0 0 1-.035 0a.6.6 0 0 0-.436.072a.549.549 0 0 0-.331.486c0 .185.098.242.425.248c.438 0 .627.199.632.639a1.568 1.568 0 0 1-.576 1.185zm2.481-.68a.094.094 0 0 1-.036.092l-1.198.727a.034.034 0 0 1-.04.003a.035.035 0 0 1-.016-.037v-.31a.086.086 0 0 1 .055-.076l1.179-.706a.035.035 0 0 1 .056.035v.273zm.827-6.914L8.812 7.515c-.559.331-.97.693-.97 1.367v5.52c0 .404.165.662.413.741a1.465 1.465 0 0 1-.248.025c-.264 0-.522-.072-.748-.207L2.522 12.15a1.558 1.558 0 0 1-.75-1.338V5.188a1.558 1.558 0 0 1 .75-1.34l4.738-2.81a1.46 1.46 0 0 1 1.489 0l4.736 2.812a1.548 1.548 0 0 1 .728 1.083c-.154-.334-.508-.427-.92-.185h.002z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-window{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M14.5 2h-13l-.5.5v11l.5.5h13l.5-.5v-11l-.5-.5zM14 13H2V6h12v7zm0-8H2V3h12v2z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-ph-broadcast{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M128 88a40 40 0 1 0 40 40a40 40 0 0 0-40-40Zm0 64a24 24 0 1 1 24-24a24.1 24.1 0 0 1-24 24Zm-59-48.9a64.5 64.5 0 0 0 0 49.8a65.4 65.4 0 0 0 13.7 20.4a7.9 7.9 0 0 1 0 11.3a8 8 0 0 1-5.6 2.3a8.3 8.3 0 0 1-5.7-2.3a80 80 0 0 1-17.1-25.5a79.9 79.9 0 0 1 0-62.2a80 80 0 0 1 17.1-25.5a8 8 0 0 1 11.3 0a7.9 7.9 0 0 1 0 11.3A65.4 65.4 0 0 0 69 103.1Zm132.7 56a80 80 0 0 1-17.1 25.5a8.3 8.3 0 0 1-5.7 2.3a8 8 0 0 1-5.6-2.3a7.9 7.9 0 0 1 0-11.3a65.4 65.4 0 0 0 13.7-20.4a64.5 64.5 0 0 0 0-49.8a65.4 65.4 0 0 0-13.7-20.4a7.9 7.9 0 0 1 0-11.3a8 8 0 0 1 11.3 0a80 80 0 0 1 17.1 25.5a79.9 79.9 0 0 1 0 62.2ZM54.5 201.5a8.1 8.1 0 0 1 0 11.4a8.3 8.3 0 0 1-5.7 2.3a8.5 8.5 0 0 1-5.7-2.3a121.8 121.8 0 0 1-25.7-38.2a120.7 120.7 0 0 1 0-93.4a121.8 121.8 0 0 1 25.7-38.2a8.1 8.1 0 0 1 11.4 11.4A103.5 103.5 0 0 0 24 128a103.5 103.5 0 0 0 30.5 73.5ZM248 128a120.2 120.2 0 0 1-9.4 46.7a121.8 121.8 0 0 1-25.7 38.2a8.5 8.5 0 0 1-5.7 2.3a8.3 8.3 0 0 1-5.7-2.3a8.1 8.1 0 0 1 0-11.4A103.5 103.5 0 0 0 232 128a103.5 103.5 0 0 0-30.5-73.5a8.1 8.1 0 1 1 11.4-11.4a121.8 121.8 0 0 1 25.7 38.2A120.2 120.2 0 0 1 248 128Z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-ph-globe-hemisphere-west{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M221.6 173.3A102.9 102.9 0 0 0 232 128a104.2 104.2 0 0 0-77.2-100.5h-.5A103.8 103.8 0 0 0 60.4 49l-1.3 1.2A103.9 103.9 0 0 0 128 232h2.4a104.3 104.3 0 0 0 90.6-57.4ZM216 128a89.3 89.3 0 0 1-5.5 30.7l-46.4-28.5a16.6 16.6 0 0 0-6.3-2.3l-22.8-3a16.1 16.1 0 0 0-15.3 6.8h-8.6l-3.8-7.9a15.9 15.9 0 0 0-11-8.7l-6.6-1.4l4.6-10.8h21.4a16.1 16.1 0 0 0 7.7-2l12.2-6.8a16.1 16.1 0 0 0 3-2.1l26.9-24.4a15.7 15.7 0 0 0 4.5-16.9a88 88 0 0 1 46 77.3Zm-68.8-85.9l7.6 13.7l-26.9 24.3l-12.2 6.8H94.3a15.9 15.9 0 0 0-14.7 9.8l-5.3 12.4l-10.9-29.2l8.1-19.3a88 88 0 0 1 75.7-18.5ZM40 128a87.1 87.1 0 0 1 9.5-39.7l10.4 27.9a16.1 16.1 0 0 0 11.6 10l5.5 1.2h.1l15.8 3.4l3.8 7.9a16.3 16.3 0 0 0 14.4 9h1.2l-7.7 17.2a15.9 15.9 0 0 0 2.8 17.4l18.8 20.4l-2.5 13.2A88.1 88.1 0 0 1 40 128Zm100.1 87.2l1.8-9.5a16 16 0 0 0-3.9-13.9l-18.8-20.3l12.7-28.7l1-2.1l22.8 3.1l47.8 29.4a88.5 88.5 0 0 1-63.4 42Z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-ph-hand-waving{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m220.2 104l-20-34.7a28.1 28.1 0 0 0-47.3-1.9l-17.3-30a28.1 28.1 0 0 0-38.3-10.3a29.4 29.4 0 0 0-9.9 9.6a27.9 27.9 0 0 0-11.5-6.2a27.2 27.2 0 0 0-21.2 2.8a27.9 27.9 0 0 0-10.3 38.2l3.4 5.8A28.5 28.5 0 0 0 36 81a28.1 28.1 0 0 0-10.2 38.2l42 72.8a88 88 0 1 0 152.4-88Zm-6.7 62.6a71.2 71.2 0 0 1-33.5 43.7A72.1 72.1 0 0 1 81.6 184l-42-72.8a12 12 0 0 1 20.8-12l22 38.1l.6.9v.2l.5.5l.2.2l.7.6h.1l.7.5h.3l.6.3h.2l.9.3h.1l.8.2h2.2l.9-.2h.3l.6-.2h.3l.9-.4a8.1 8.1 0 0 0 2.9-11l-22-38.1l-16-27.7a12 12 0 0 1-1.2-9.1a11.8 11.8 0 0 1 5.6-7.3a12 12 0 0 1 9.1-1.2a12.5 12.5 0 0 1 7.3 5.6l8 14h.1l26 45a7 7 0 0 0 1.5 1.9a8 8 0 0 0 12.3-9.9l-26-45a12 12 0 1 1 20.8-12l30 51.9l6.3 11a48.1 48.1 0 0 0-10.9 61a8 8 0 0 0 13.8-8a32 32 0 0 1 11.7-43.7l.7-.4l.5-.4h.1l.6-.6l.5-.5l.4-.5l.3-.6h.1l.2-.5v-.2a1.9 1.9 0 0 0 .2-.7h.1c0-.2.1-.4.1-.6s0-.2.1-.2v-2.1a6.4 6.4 0 0 0-.2-.7a1.9 1.9 0 0 0-.2-.7v-.2c0-.2-.1-.3-.2-.5l-.3-.7l-10-17.4a12 12 0 0 1 13.5-17.5a11.8 11.8 0 0 1 7.2 5.5l20 34.7a70.9 70.9 0 0 1 7.2 53.8Zm-125.8 78a8.2 8.2 0 0 1-6.6 3.4a8.6 8.6 0 0 1-4.6-1.4A117.9 117.9 0 0 1 41.1 208a8 8 0 1 1 13.8-8a102.6 102.6 0 0 0 30.8 33.4a8.1 8.1 0 0 1 2 11.2ZM168 31a8 8 0 0 1 8-8a60.2 60.2 0 0 1 52 30a7.9 7.9 0 0 1-3 10.9a7.1 7.1 0 0 1-4 1.1a8 8 0 0 1-6.9-4A44 44 0 0 0 176 39a8 8 0 0 1-8-8Z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-ph-moon{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M224.3 150.3a8.1 8.1 0 0 0-7.8-5.7l-2.2.4A84 84 0 0 1 111 41.6a5.7 5.7 0 0 0 .3-1.8a7.9 7.9 0 0 0-10.3-8.1a100 100 0 1 0 123.3 123.2a7.2 7.2 0 0 0 0-4.6ZM128 212A84 84 0 0 1 92.8 51.7a99.9 99.9 0 0 0 111.5 111.5A84.4 84.4 0 0 1 128 212Z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-ph-sun{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M128 60a68 68 0 1 0 68 68a68.1 68.1 0 0 0-68-68Zm0 120a52 52 0 1 1 52-52a52 52 0 0 1-52 52Zm-8-144V16a8 8 0 0 1 16 0v20a8 8 0 0 1-16 0ZM43.1 54.5a8.1 8.1 0 1 1 11.4-11.4l14.1 14.2a8 8 0 0 1 0 11.3a8.1 8.1 0 0 1-11.3 0ZM36 136H16a8 8 0 0 1 0-16h20a8 8 0 0 1 0 16Zm32.6 51.4a8 8 0 0 1 0 11.3l-14.1 14.2a8.3 8.3 0 0 1-5.7 2.3a8.5 8.5 0 0 1-5.7-2.3a8.1 8.1 0 0 1 0-11.4l14.2-14.1a8 8 0 0 1 11.3 0ZM136 220v20a8 8 0 0 1-16 0v-20a8 8 0 0 1 16 0Zm76.9-18.5a8.1 8.1 0 0 1 0 11.4a8.5 8.5 0 0 1-5.7 2.3a8.3 8.3 0 0 1-5.7-2.3l-14.1-14.2a8 8 0 0 1 11.3-11.3ZM248 128a8 8 0 0 1-8 8h-20a8 8 0 0 1 0-16h20a8 8 0 0 1 8 8Zm-60.6-59.4a8 8 0 0 1 0-11.3l14.1-14.2a8.1 8.1 0 0 1 11.4 11.4l-14.2 14.1a8.1 8.1 0 0 1-11.3 0Z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.note{position:relative;display:inline-flex;align-items:center;border-left-width:4px;border-left-style:solid;--un-border-opacity:1;border-color:rgba(53,120,229,var(--un-border-opacity));border-radius:0.25rem;background-color:rgba(53,120,229,0.1);padding:0.5rem;text-decoration:none;}.note-red{position:relative;display:inline-flex;align-items:center;border-left-width:4px;border-left-style:solid;--un-border-opacity:1;border-color:rgba(53,120,229,var(--un-border-opacity));border-radius:0.25rem;background-color:rgba(53,120,229,0.1);background-color:rgba(185,28,28,0.1);padding:0.5rem;text-decoration:none;}.nv{position:relative;display:flex;align-items:center;border-radius:0.25rem;padding:0.5rem;--un-text-opacity:1;color:rgba(194,197,202,var(--un-text-opacity));text-decoration:none;transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:125ms;}.nv_selected{position:relative;display:flex;align-items:center;border-left-width:4px;border-left-style:solid;border-radius:0.25rem;--un-bg-opacity:.05;background-color:hsla(0,0%,100%,var(--un-bg-opacity));padding:0.5rem;--un-text-opacity:1;color:rgba(194,197,202,var(--un-text-opacity));color:rgba(53,120,229,var(--un-text-opacity));text-decoration:none;transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:125ms;}.input{height:2.5rem;display:flex;align-items:center;border-radius:0.25rem;border-style:none;--un-bg-opacity:1;background-color:rgba(233,236,239,var(--un-bg-opacity));padding:0.5rem;--un-text-opacity:1;color:rgba(28,30,33,var(--un-text-opacity));--un-shadow:var(--un-shadow-inset) 0 4px 6px -1px var(--un-shadow-color, rgba(0,0,0,0.1)),var(--un-shadow-inset) 0 2px 4px -2px var(--un-shadow-color, rgba(0,0,0,0.1));box-shadow:var(--un-ring-offset-shadow, 0 0 #0000), var(--un-ring-shadow, 0 0 #0000), var(--un-shadow);outline:2px solid transparent;outline-offset:2px;}.btn{user-select:none;border-radius:0.25rem;border-style:none;--un-bg-opacity:1;background-color:rgba(53,120,229,var(--un-bg-opacity));padding:0.5rem;font-weight:400;--un-text-opacity:1;color:rgba(28,30,33,var(--un-text-opacity));color:rgba(255,255,255,var(--un-text-opacity));--un-shadow:var(--un-shadow-inset) 0 4px 6px -1px var(--un-shadow-color, rgba(0,0,0,0.1)),var(--un-shadow-inset) 0 2px 4px -2px var(--un-shadow-color, rgba(0,0,0,0.1));box-shadow:var(--un-ring-offset-shadow, 0 0 #0000), var(--un-ring-shadow, 0 0 #0000), var(--un-shadow);outline:2px solid transparent;outline-offset:2px;}.nv_selected:hover,.nv:hover{border-left-width:4px;border-left-style:solid;--un-bg-opacity:.05;background-color:hsla(0,0%,100%,var(--un-bg-opacity));--un-text-opacity:1;color:rgba(53,120,229,var(--un-text-opacity));}.dark .note{--un-border-opacity:1;border-color:rgba(103,214,237,var(--un-border-opacity));background-color:rgba(103,214,237,0.1);}.dark .note-red{--un-border-opacity:1;border-color:rgba(103,214,237,var(--un-border-opacity));background-color:rgba(103,214,237,0.1);background-color:rgba(185,28,28,0.1);}.btn:hover{--un-bg-opacity:1;background-color:rgba(45,102,195,var(--un-bg-opacity));}.dark .btn{--un-bg-opacity:1;background-color:rgba(103,214,237,var(--un-bg-opacity));font-weight:600;--un-text-opacity:1;color:rgba(28,30,33,var(--un-text-opacity));}.dark .btn:hover{--un-bg-opacity:1;background-color:rgba(57,202,232,var(--un-bg-opacity));}.dark .input{--un-bg-opacity:1;background-color:rgba(36,37,38,var(--un-bg-opacity));--un-text-opacity:1;color:rgba(227,227,227,var(--un-text-opacity));}.dark .note-red::after,.note-red::after{--un-bg-opacity:1;background-color:rgba(185,28,28,var(--un-bg-opacity));}.btn:active{--un-bg-opacity:1;background-color:rgba(37,84,160,var(--un-bg-opacity));}.dark .btn:active{--un-bg-opacity:1;background-color:rgba(25,181,213,var(--un-bg-opacity));}.dark .nv_selected,.dark .nv_selected:hover,.dark .nv:hover{--un-text-opacity:1;color:rgba(103,214,237,var(--un-text-opacity));} ::-webkit-scrollbar-thumb { background-color: #3578E5; } .dark ::-webkit-scrollbar-thumb { background-color: #67d6ed; } code { font-size: 0.75rem; font-family: "Fira Code","Fira Mono",ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; border-radius: 0.25rem; background-color: #d6d8da; } .code-block { font-family: "Fira Code","Fira Mono",ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; font-size: 0.875rem; } .dark code { background-color: #282a2e; } .visible{visibility:visible;}.absolute{position:absolute;}.left-2{left:0.5rem;}.top-2{top:0.5rem;}.z-2000{z-index:2000;}.grid{display:grid;}.grid-rows-\[2fr_auto\]{grid-template-rows:2fr auto;}.grid-rows-\[2px_2rem_1fr\]{grid-template-rows:2px 2rem 1fr;}.grid-rows-\[auto_1fr\]{grid-template-rows:auto 1fr;}.my-2{margin-top:0.5rem;margin-bottom:0.5rem;}.mb-2{margin-bottom:0.5rem;}.mr-2{margin-right:0.5rem;}.display-none,.hidden{display:none;}.children-h-10>*,.children\:h10>*{height:2.5rem;}.children\:h-100\%>*,.h-100\%{height:100%;}.children\:w-12>*{width:3rem;}.h-15rem{height:15rem;}.h-2px{height:2px;}.h-8{height:2rem;}.h-85\%{height:85%;}.h-auto{height:auto;}.h-screen{height:100vh;}.w-100\%{width:100%;}.w-8{width:2rem;}.w-screen{width:100vw;}.flex{display:flex;}.children\:inline-flex>*{display:inline-flex;}.flex-1{flex:1 1 0%;}.children-flex-none>*{flex:none;}.children\:grow>*,.grow{flex-grow:1;}.flex-row{flex-direction:row;}.flex-col{flex-direction:column;}.flex-wrap{flex-wrap:wrap;}@keyframes fade-in{from{opacity:0}to{opacity:1}}@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}.animate-fade-in{animation:fade-in 1s linear 1;}.animate-spin{animation:spin 1s linear infinite;}.animate-duration-300ms{animation-duration:300ms;}.cursor-ns-resize{cursor:ns-resize;}.cursor-pointer{cursor:pointer;}.select-none{user-select:none;}.children\:items-center>*,.items-center{align-items:center;}.self-center{align-self:center;}.children\:justify-center>*,.justify-center{justify-content:center;}.justify-between{justify-content:space-between;}.gap-1{grid-gap:0.25rem;gap:0.25rem;}.gap-2{grid-gap:0.5rem;gap:0.5rem;}.overflow-hidden{overflow:hidden;}.overflow-y-auto{overflow-y:auto;}.rd-1{border-radius:0.25rem;}.rd-8{border-radius:2rem;}.bg-accent{--un-bg-opacity:1;background-color:rgba(53,120,229,var(--un-bg-opacity));}.bg-black\/20{background-color:rgba(0,0,0,0.2);}.bg-darkPrimaryLighter{--un-bg-opacity:1;background-color:rgba(36,37,38,var(--un-bg-opacity));}.bg-primary{--un-bg-opacity:1;background-color:rgba(255,255,255,var(--un-bg-opacity));}.bg-white\/5{background-color:rgba(255,255,255,0.05);}.dark .dark\:bg-darkAccent{--un-bg-opacity:1;background-color:rgba(103,214,237,var(--un-bg-opacity));}.dark .dark\:bg-darkPrimary{--un-bg-opacity:1;background-color:rgba(27,27,29,var(--un-bg-opacity));}.dark .dark\:hover\:bg-darkHoverOverlay:hover{--un-bg-opacity:.05;background-color:hsla(0,0%,100%,var(--un-bg-opacity));}.dark .dark\:hover\:bg-red-700:hover,.hover\:bg-red-700:hover{--un-bg-opacity:1;background-color:rgba(185,28,28,var(--un-bg-opacity));}.hover\:bg-hoverOverlay:hover{--un-bg-opacity:.05;background-color:rgba(0,0,0,var(--un-bg-opacity));}.active\:bg-accentDark:active{--un-bg-opacity:1;background-color:rgba(48,108,206,var(--un-bg-opacity));}.active\:bg-hoverOverlay\/25:active{background-color:rgba(0,0,0,0.25);}.active\:bg-hoverOverlayDarker:active{--un-bg-opacity:.1;background-color:rgba(0,0,0,var(--un-bg-opacity));}.active\:bg-red-700\/90:active,.dark .dark\:active\:bg-red-700\/90:active{background-color:rgba(185,28,28,0.9);}.dark .dark\:active\:bg-darkAccentDark:active{--un-bg-opacity:1;background-color:rgba(73,206,233,var(--un-bg-opacity));}.dark .dark\:active\:bg-darkHoverOverlay\/25:active{background-color:hsla(0,0%,100%,0.25);}.dark .dark\:active\:bg-darkHoverOverlayDarker:active{--un-bg-opacity:.1;background-color:hsla(0,0%,100%,var(--un-bg-opacity));}.p-1{padding:0.25rem;}.p-7{padding:1.75rem;}.px{padding-left:1rem;padding-right:1rem;}.px-2{padding-left:0.5rem;padding-right:0.5rem;}.px-5{padding-left:1.25rem;padding-right:1.25rem;}.children-pb-2>*{padding-bottom:0.5rem;}.children-pt8>*{padding-top:2rem;}.pl-2{padding-left:0.5rem;}.all\:font-mono *{font-family:"Fira Code","Fira Mono",ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;}.all\:text-xs *{font-size:0.75rem;line-height:1rem;}.text-sm{font-size:0.875rem;line-height:1.25rem;}.font-700{font-weight:700;}.font-semibold{font-weight:600;}.dark .dark\:text-darkAccent{--un-text-opacity:1;color:rgba(103,214,237,var(--un-text-opacity));}.dark .dark\:text-darkAccentText,.text-primaryText{--un-text-opacity:1;color:rgba(28,30,33,var(--un-text-opacity));}.dark .dark\:text-darkPrimaryText,.hover\:text-darkPrimaryText:hover,.text-darkPrimaryText,.active\:text-darkPrimaryText:active{--un-text-opacity:1;color:rgba(227,227,227,var(--un-text-opacity));}.text-accent{--un-text-opacity:1;color:rgba(53,120,229,var(--un-text-opacity));}.text-accentText{--un-text-opacity:1;color:rgba(255,255,255,var(--un-text-opacity));}.filter{filter:var(--un-blur) var(--un-brightness) var(--un-contrast) var(--un-drop-shadow) var(--un-grayscale) var(--un-hue-rotate) var(--un-invert) var(--un-saturate) var(--un-sepia);}.transition-colors-250{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:250ms;}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}@media (max-width: 639.9px){.lt-sm\:absolute{position:absolute;}.lt-sm\:z-1999{z-index:1999;}.lt-sm\:h-screen{height:100vh;}.lt-sm\:flex{display:flex;}.lt-sm\:pl-10{padding-left:2.5rem;}.lt-sm\:shadow{--un-shadow:var(--un-shadow-inset) 0 1px 3px 0 var(--un-shadow-color, rgba(0,0,0,0.1)),var(--un-shadow-inset) 0 1px 2px -1px var(--un-shadow-color, rgba(0,0,0,0.1));box-shadow:var(--un-ring-offset-shadow, 0 0 #0000), var(--un-ring-shadow, 0 0 #0000), var(--un-shadow);}.lt-sm\:shadow-lg{--un-shadow:var(--un-shadow-inset) 0 10px 15px -3px var(--un-shadow-color, rgba(0,0,0,0.1)),var(--un-shadow-inset) 0 4px 6px -4px var(--un-shadow-color, rgba(0,0,0,0.1));box-shadow:var(--un-ring-offset-shadow, 0 0 #0000), var(--un-ring-shadow, 0 0 #0000), var(--un-shadow);}.lt-sm\:transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}}*:not(h1,h2,h3,h4,h5,h6){margin:0;padding:0}*{box-sizing:border-box;font-family:Rubik,sans-serif}::-webkit-scrollbar{width:.25rem;height:3px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{border-radius:.25rem}code{padding:.05rem .25rem}code.code-block{padding:.5rem}#sidebar{width:18.75rem}@media screen and (max-width: 640px){#sidebar{--translate-x: -18.75rem;transform:translate(var(--translate-x))}}ul.svelte-gbh3pt{list-style:none;margin:0;padding:0;padding-left:var(--nodePaddingLeft, 1rem);border-left:var(--nodeBorderLeft, 1px dotted #9ca3af);color:var(--nodeColor, #374151)}.hidden.svelte-gbh3pt{display:none}.bracket.svelte-gbh3pt{cursor:pointer}.bracket.svelte-gbh3pt:hover{background:var(--bracketHoverBackground, #d1d5db)}.comma.svelte-gbh3pt{color:var(--nodeColor, #374151)}.val.svelte-gbh3pt{color:var(--leafDefaultColor, #9ca3af)}.val.string.svelte-gbh3pt{color:var(--leafStringColor, #059669)}.val.number.svelte-gbh3pt{color:var(--leafNumberColor, #d97706)}.val.boolean.svelte-gbh3pt{color:var(--leafBooleanColor, #2563eb)}.spinner.svelte-4xesec{height:1.2rem;width:1.2rem;border-radius:50rem;color:currentColor;border:2px dashed currentColor} +*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x:var(--un-empty,/*!*/ /*!*/);--un-pan-y:var(--un-empty,/*!*/ /*!*/);--un-pinch-zoom:var(--un-empty,/*!*/ /*!*/);--un-scroll-snap-strictness:proximity;--un-ordinal:var(--un-empty,/*!*/ /*!*/);--un-slashed-zero:var(--un-empty,/*!*/ /*!*/);--un-numeric-figure:var(--un-empty,/*!*/ /*!*/);--un-numeric-spacing:var(--un-empty,/*!*/ /*!*/);--un-numeric-fraction:var(--un-empty,/*!*/ /*!*/);--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 #0000;--un-ring-shadow:0 0 #0000;--un-shadow-inset:var(--un-empty,/*!*/ /*!*/);--un-shadow:0 0 #0000;--un-ring-inset:var(--un-empty,/*!*/ /*!*/);--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur:var(--un-empty,/*!*/ /*!*/);--un-brightness:var(--un-empty,/*!*/ /*!*/);--un-contrast:var(--un-empty,/*!*/ /*!*/);--un-drop-shadow:var(--un-empty,/*!*/ /*!*/);--un-grayscale:var(--un-empty,/*!*/ /*!*/);--un-hue-rotate:var(--un-empty,/*!*/ /*!*/);--un-invert:var(--un-empty,/*!*/ /*!*/);--un-saturate:var(--un-empty,/*!*/ /*!*/);--un-sepia:var(--un-empty,/*!*/ /*!*/);--un-backdrop-blur:var(--un-empty,/*!*/ /*!*/);--un-backdrop-brightness:var(--un-empty,/*!*/ /*!*/);--un-backdrop-contrast:var(--un-empty,/*!*/ /*!*/);--un-backdrop-grayscale:var(--un-empty,/*!*/ /*!*/);--un-backdrop-hue-rotate:var(--un-empty,/*!*/ /*!*/);--un-backdrop-invert:var(--un-empty,/*!*/ /*!*/);--un-backdrop-opacity:var(--un-empty,/*!*/ /*!*/);--un-backdrop-saturate:var(--un-empty,/*!*/ /*!*/);--un-backdrop-sepia:var(--un-empty,/*!*/ /*!*/);}@font-face { font-family: 'Fira Code'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/firacode/v21/uU9eCBsR6Z2vfE9aq3bL0fxyUs4tcw4W_D1sFVc.ttf) format('truetype');}@font-face { font-family: 'Fira Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/firamono/v14/N0bX2SlFPv1weGeLZDtQIQ.ttf) format('truetype');}@font-face { font-family: 'Fira Mono'; font-style: normal; font-weight: 700; font-display: swap; src: url(https://fonts.gstatic.com/s/firamono/v14/N0bS2SlFPv1weGeLZDtondv3mQ.ttf) format('truetype');}@font-face { font-family: 'Rubik'; font-style: normal; font-weight: 400; font-display: swap; src: url(https://fonts.gstatic.com/s/rubik/v28/iJWZBXyIfDnIV5PNhY1KTN7Z-Yh-B4i1UA.ttf) format('truetype');}.i-codicon-clear-all{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m10 12.6l.7.7l1.6-1.6l1.6 1.6l.8-.7L13 11l1.7-1.6l-.8-.8l-1.6 1.7l-1.6-1.7l-.7.8l1.6 1.6l-1.6 1.6zM1 4h14V3H1v1zm0 3h14V6H1v1zm8 2.5V9H1v1h8v-.5zM9 13v-1H1v1h8z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-close{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' fill-rule='evenodd' d='m8 8.707l3.646 3.647l.708-.707L8.707 8l3.647-3.646l-.707-.708L8 7.293L4.354 3.646l-.707.708L7.293 8l-3.646 3.646l.707.708L8 8.707z' clip-rule='evenodd'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-link-external{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M1.5 1H6v1H2v12h12v-4h1v4.5l-.5.5h-13l-.5-.5v-13l.5-.5z'/%3E%3Cpath d='M15 1.5V8h-1V2.707L7.243 9.465l-.707-.708L13.293 2H8V1h6.5l.5.5z'/%3E%3C/g%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-menu{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M16 5H0V4h16v1zm0 8H0v-1h16v1zm0-4.008H0V8h16v.992z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-codicon-radio-tower{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 16 16' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' fill-rule='evenodd' d='M2.998 5.58a5.55 5.55 0 0 1 1.62-3.88l-.71-.7a6.45 6.45 0 0 0 0 9.16l.71-.7a5.55 5.55 0 0 1-1.62-3.88zm1.06 0a4.42 4.42 0 0 0 1.32 3.17l.71-.71a3.27 3.27 0 0 1-.76-1.12a3.45 3.45 0 0 1 0-2.67a3.22 3.22 0 0 1 .76-1.13l-.71-.71a4.46 4.46 0 0 0-1.32 3.17zm7.65 3.21l-.71-.71c.33-.32.59-.704.76-1.13a3.449 3.449 0 0 0 0-2.67a3.22 3.22 0 0 0-.76-1.13l.71-.7a4.468 4.468 0 0 1 0 6.34zM13.068 1l-.71.71a5.43 5.43 0 0 1 0 7.74l.71.71a6.45 6.45 0 0 0 0-9.16zM9.993 5.43a1.5 1.5 0 0 1-.245.98a2 2 0 0 1-.27.23l3.44 7.73l-.92.4l-.77-1.73h-5.54l-.77 1.73l-.92-.4l3.44-7.73a1.52 1.52 0 0 1-.33-1.63a1.55 1.55 0 0 1 .56-.68a1.5 1.5 0 0 1 2.325 1.1zm-1.595-.34a.52.52 0 0 0-.25.14a.52.52 0 0 0-.11.22a.48.48 0 0 0 0 .29c.04.09.102.17.18.23a.54.54 0 0 0 .28.08a.51.51 0 0 0 .5-.5a.54.54 0 0 0-.08-.28a.58.58 0 0 0-.23-.18a.48.48 0 0 0-.29 0zm.23 2.05h-.27l-.87 1.94h2l-.86-1.94zm2.2 4.94l-.89-2h-2.88l-.89 2h4.66z' clip-rule='evenodd'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-ph-broadcast{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M128 88a40 40 0 1 0 40 40a40 40 0 0 0-40-40Zm0 64a24 24 0 1 1 24-24a24.1 24.1 0 0 1-24 24Zm-59-48.9a64.5 64.5 0 0 0 0 49.8a65.4 65.4 0 0 0 13.7 20.4a7.9 7.9 0 0 1 0 11.3a8 8 0 0 1-5.6 2.3a8.3 8.3 0 0 1-5.7-2.3a80 80 0 0 1-17.1-25.5a79.9 79.9 0 0 1 0-62.2a80 80 0 0 1 17.1-25.5a8 8 0 0 1 11.3 0a7.9 7.9 0 0 1 0 11.3A65.4 65.4 0 0 0 69 103.1Zm132.7 56a80 80 0 0 1-17.1 25.5a8.3 8.3 0 0 1-5.7 2.3a8 8 0 0 1-5.6-2.3a7.9 7.9 0 0 1 0-11.3a65.4 65.4 0 0 0 13.7-20.4a64.5 64.5 0 0 0 0-49.8a65.4 65.4 0 0 0-13.7-20.4a7.9 7.9 0 0 1 0-11.3a8 8 0 0 1 11.3 0a80 80 0 0 1 17.1 25.5a79.9 79.9 0 0 1 0 62.2ZM54.5 201.5a8.1 8.1 0 0 1 0 11.4a8.3 8.3 0 0 1-5.7 2.3a8.5 8.5 0 0 1-5.7-2.3a121.8 121.8 0 0 1-25.7-38.2a120.7 120.7 0 0 1 0-93.4a121.8 121.8 0 0 1 25.7-38.2a8.1 8.1 0 0 1 11.4 11.4A103.5 103.5 0 0 0 24 128a103.5 103.5 0 0 0 30.5 73.5ZM248 128a120.2 120.2 0 0 1-9.4 46.7a121.8 121.8 0 0 1-25.7 38.2a8.5 8.5 0 0 1-5.7 2.3a8.3 8.3 0 0 1-5.7-2.3a8.1 8.1 0 0 1 0-11.4A103.5 103.5 0 0 0 232 128a103.5 103.5 0 0 0-30.5-73.5a8.1 8.1 0 1 1 11.4-11.4a121.8 121.8 0 0 1 25.7 38.2A120.2 120.2 0 0 1 248 128Z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-ph-hand-waving{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m220.2 104l-20-34.7a28.1 28.1 0 0 0-47.3-1.9l-17.3-30a28.1 28.1 0 0 0-38.3-10.3a29.4 29.4 0 0 0-9.9 9.6a27.9 27.9 0 0 0-11.5-6.2a27.2 27.2 0 0 0-21.2 2.8a27.9 27.9 0 0 0-10.3 38.2l3.4 5.8A28.5 28.5 0 0 0 36 81a28.1 28.1 0 0 0-10.2 38.2l42 72.8a88 88 0 1 0 152.4-88Zm-6.7 62.6a71.2 71.2 0 0 1-33.5 43.7A72.1 72.1 0 0 1 81.6 184l-42-72.8a12 12 0 0 1 20.8-12l22 38.1l.6.9v.2l.5.5l.2.2l.7.6h.1l.7.5h.3l.6.3h.2l.9.3h.1l.8.2h2.2l.9-.2h.3l.6-.2h.3l.9-.4a8.1 8.1 0 0 0 2.9-11l-22-38.1l-16-27.7a12 12 0 0 1-1.2-9.1a11.8 11.8 0 0 1 5.6-7.3a12 12 0 0 1 9.1-1.2a12.5 12.5 0 0 1 7.3 5.6l8 14h.1l26 45a7 7 0 0 0 1.5 1.9a8 8 0 0 0 12.3-9.9l-26-45a12 12 0 1 1 20.8-12l30 51.9l6.3 11a48.1 48.1 0 0 0-10.9 61a8 8 0 0 0 13.8-8a32 32 0 0 1 11.7-43.7l.7-.4l.5-.4h.1l.6-.6l.5-.5l.4-.5l.3-.6h.1l.2-.5v-.2a1.9 1.9 0 0 0 .2-.7h.1c0-.2.1-.4.1-.6s0-.2.1-.2v-2.1a6.4 6.4 0 0 0-.2-.7a1.9 1.9 0 0 0-.2-.7v-.2c0-.2-.1-.3-.2-.5l-.3-.7l-10-17.4a12 12 0 0 1 13.5-17.5a11.8 11.8 0 0 1 7.2 5.5l20 34.7a70.9 70.9 0 0 1 7.2 53.8Zm-125.8 78a8.2 8.2 0 0 1-6.6 3.4a8.6 8.6 0 0 1-4.6-1.4A117.9 117.9 0 0 1 41.1 208a8 8 0 1 1 13.8-8a102.6 102.6 0 0 0 30.8 33.4a8.1 8.1 0 0 1 2 11.2ZM168 31a8 8 0 0 1 8-8a60.2 60.2 0 0 1 52 30a7.9 7.9 0 0 1-3 10.9a7.1 7.1 0 0 1-4 1.1a8 8 0 0 1-6.9-4A44 44 0 0 0 176 39a8 8 0 0 1-8-8Z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-ph-moon{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M224.3 150.3a8.1 8.1 0 0 0-7.8-5.7l-2.2.4A84 84 0 0 1 111 41.6a5.7 5.7 0 0 0 .3-1.8a7.9 7.9 0 0 0-10.3-8.1a100 100 0 1 0 123.3 123.2a7.2 7.2 0 0 0 0-4.6ZM128 212A84 84 0 0 1 92.8 51.7a99.9 99.9 0 0 0 111.5 111.5A84.4 84.4 0 0 1 128 212Z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.i-ph-sun{--un-icon:url("data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 256 256' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M128 60a68 68 0 1 0 68 68a68.1 68.1 0 0 0-68-68Zm0 120a52 52 0 1 1 52-52a52 52 0 0 1-52 52Zm-8-144V16a8 8 0 0 1 16 0v20a8 8 0 0 1-16 0ZM43.1 54.5a8.1 8.1 0 1 1 11.4-11.4l14.1 14.2a8 8 0 0 1 0 11.3a8.1 8.1 0 0 1-11.3 0ZM36 136H16a8 8 0 0 1 0-16h20a8 8 0 0 1 0 16Zm32.6 51.4a8 8 0 0 1 0 11.3l-14.1 14.2a8.3 8.3 0 0 1-5.7 2.3a8.5 8.5 0 0 1-5.7-2.3a8.1 8.1 0 0 1 0-11.4l14.2-14.1a8 8 0 0 1 11.3 0ZM136 220v20a8 8 0 0 1-16 0v-20a8 8 0 0 1 16 0Zm76.9-18.5a8.1 8.1 0 0 1 0 11.4a8.5 8.5 0 0 1-5.7 2.3a8.3 8.3 0 0 1-5.7-2.3l-14.1-14.2a8 8 0 0 1 11.3-11.3ZM248 128a8 8 0 0 1-8 8h-20a8 8 0 0 1 0-16h20a8 8 0 0 1 8 8Zm-60.6-59.4a8 8 0 0 1 0-11.3l14.1-14.2a8.1 8.1 0 0 1 11.4 11.4l-14.2 14.1a8.1 8.1 0 0 1-11.3 0Z'/%3E%3C/svg%3E");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;}.note-red{position:relative;display:inline-flex;align-items:center;border-left-width:4px;border-left-style:solid;--un-border-opacity:1;border-color:rgba(53,120,229,var(--un-border-opacity));border-radius:0.25rem;background-color:rgba(53,120,229,0.1);background-color:rgba(185,28,28,0.1);padding:0.5rem;text-decoration:none;}.nv{position:relative;display:flex;align-items:center;border-radius:0.25rem;padding:0.5rem;--un-text-opacity:1;color:rgba(194,197,202,var(--un-text-opacity));text-decoration:none;transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:125ms;}.nv_selected{position:relative;display:flex;align-items:center;border-left-width:4px;border-left-style:solid;border-radius:0.25rem;--un-bg-opacity:.05;background-color:hsla(0,0%,100%,var(--un-bg-opacity));padding:0.5rem;--un-text-opacity:1;color:rgba(194,197,202,var(--un-text-opacity));color:rgba(53,120,229,var(--un-text-opacity));text-decoration:none;transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:125ms;}.input{height:2.5rem;display:flex;align-items:center;border-radius:0.25rem;border-style:none;--un-bg-opacity:1;background-color:rgba(233,236,239,var(--un-bg-opacity));padding:0.5rem;--un-text-opacity:1;color:rgba(28,30,33,var(--un-text-opacity));--un-shadow:var(--un-shadow-inset) 0 4px 6px -1px var(--un-shadow-color, rgba(0,0,0,0.1)),var(--un-shadow-inset) 0 2px 4px -2px var(--un-shadow-color, rgba(0,0,0,0.1));box-shadow:var(--un-ring-offset-shadow, 0 0 #0000), var(--un-ring-shadow, 0 0 #0000), var(--un-shadow);outline:2px solid transparent;outline-offset:2px;}.btn{user-select:none;border-radius:0.25rem;border-style:none;--un-bg-opacity:1;background-color:rgba(53,120,229,var(--un-bg-opacity));padding:0.5rem;font-weight:400;--un-text-opacity:1;color:rgba(28,30,33,var(--un-text-opacity));color:rgba(255,255,255,var(--un-text-opacity));--un-shadow:var(--un-shadow-inset) 0 4px 6px -1px var(--un-shadow-color, rgba(0,0,0,0.1)),var(--un-shadow-inset) 0 2px 4px -2px var(--un-shadow-color, rgba(0,0,0,0.1));box-shadow:var(--un-ring-offset-shadow, 0 0 #0000), var(--un-ring-shadow, 0 0 #0000), var(--un-shadow);outline:2px solid transparent;outline-offset:2px;}.nv_selected:hover,.nv:hover{border-left-width:4px;border-left-style:solid;--un-bg-opacity:.05;background-color:hsla(0,0%,100%,var(--un-bg-opacity));--un-text-opacity:1;color:rgba(53,120,229,var(--un-text-opacity));}.dark .note-red{--un-border-opacity:1;border-color:rgba(103,214,237,var(--un-border-opacity));background-color:rgba(103,214,237,0.1);background-color:rgba(185,28,28,0.1);}.btn:hover{--un-bg-opacity:1;background-color:rgba(45,102,195,var(--un-bg-opacity));}.dark .btn{--un-bg-opacity:1;background-color:rgba(103,214,237,var(--un-bg-opacity));font-weight:600;--un-text-opacity:1;color:rgba(28,30,33,var(--un-text-opacity));}.dark .btn:hover{--un-bg-opacity:1;background-color:rgba(57,202,232,var(--un-bg-opacity));}.dark .input{--un-bg-opacity:1;background-color:rgba(36,37,38,var(--un-bg-opacity));--un-text-opacity:1;color:rgba(227,227,227,var(--un-text-opacity));}.dark .note-red::after,.note-red::after{--un-bg-opacity:1;background-color:rgba(185,28,28,var(--un-bg-opacity));}.btn:active{--un-bg-opacity:1;background-color:rgba(37,84,160,var(--un-bg-opacity));}.dark .btn:active{--un-bg-opacity:1;background-color:rgba(25,181,213,var(--un-bg-opacity));}.dark .nv_selected,.dark .nv_selected:hover,.dark .nv:hover{--un-text-opacity:1;color:rgba(103,214,237,var(--un-text-opacity));} ::-webkit-scrollbar-thumb { background-color: #3578E5; } .dark ::-webkit-scrollbar-thumb { background-color: #67d6ed; } code { font-size: 0.75rem; font-family: "Fira Code","Fira Mono",ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; border-radius: 0.25rem; background-color: #d6d8da; } .code-block { font-family: "Fira Code","Fira Mono",ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; font-size: 0.875rem; } .dark code { background-color: #282a2e; } .visible{visibility:visible;}.absolute{position:absolute;}.left-2{left:0.5rem;}.top-2{top:0.5rem;}.z-2000{z-index:2000;}.grid{display:grid;}.grid-rows-\[2fr_auto\]{grid-template-rows:2fr auto;}.grid-rows-\[2px_2rem_1fr\]{grid-template-rows:2px 2rem 1fr;}.grid-rows-\[auto_1fr\]{grid-template-rows:auto 1fr;}.grid-rows-\[min-content_auto\]{grid-template-rows:min-content auto;}.mr-2{margin-right:0.5rem;}.display-none{display:none;}.children-h-10>*{height:2.5rem;}.h-15rem{height:15rem;}.h-2px{height:2px;}.h-8{height:2rem;}.h-85\%{height:85%;}.h-screen{height:100vh;}.w-8{width:2rem;}.w-screen{width:100vw;}.flex{display:flex;}.flex-1{flex:1 1 0%;}.children-flex-none>*{flex:none;}.grow{flex-grow:1;}.flex-col{flex-direction:column;}@keyframes fade-in{from{opacity:0}to{opacity:1}}.animate-fade-in{animation:fade-in 1s linear 1;}.animate-duration-300ms{animation-duration:300ms;}.cursor-ns-resize{cursor:ns-resize;}.cursor-pointer{cursor:pointer;}.select-none{user-select:none;}.items-center{align-items:center;}.self-center{align-self:center;}.justify-center{justify-content:center;}.justify-between{justify-content:space-between;}.gap-1{grid-gap:0.25rem;gap:0.25rem;}.gap-2{grid-gap:0.5rem;gap:0.5rem;}.overflow-hidden{overflow:hidden;}.overflow-y-auto{overflow-y:auto;}.b{border-width:1px;border-style:solid;}.rd-1{border-radius:0.25rem;}.rd-8{border-radius:2rem;}.bg-accent{--un-bg-opacity:1;background-color:rgba(53,120,229,var(--un-bg-opacity));}.bg-black\/20{background-color:rgba(0,0,0,0.2);}.bg-darkPrimaryLighter{--un-bg-opacity:1;background-color:rgba(36,37,38,var(--un-bg-opacity));}.bg-primary{--un-bg-opacity:1;background-color:rgba(255,255,255,var(--un-bg-opacity));}.bg-white\/5{background-color:rgba(255,255,255,0.05);}.dark .dark\:bg-darkAccent{--un-bg-opacity:1;background-color:rgba(103,214,237,var(--un-bg-opacity));}.dark .dark\:bg-darkPrimary{--un-bg-opacity:1;background-color:rgba(27,27,29,var(--un-bg-opacity));}.dark .dark\:hover\:bg-darkHoverOverlay:hover{--un-bg-opacity:.05;background-color:hsla(0,0%,100%,var(--un-bg-opacity));}.hover\:bg-hoverOverlay:hover{--un-bg-opacity:.05;background-color:rgba(0,0,0,var(--un-bg-opacity));}.active\:bg-accentDark:active{--un-bg-opacity:1;background-color:rgba(48,108,206,var(--un-bg-opacity));}.active\:bg-hoverOverlay\/25:active{background-color:rgba(0,0,0,0.25);}.dark .dark\:active\:bg-darkAccentDark:active{--un-bg-opacity:1;background-color:rgba(73,206,233,var(--un-bg-opacity));}.dark .dark\:active\:bg-darkHoverOverlay\/25:active{background-color:hsla(0,0%,100%,0.25);}.p-1{padding:0.25rem;}.p-7{padding:1.75rem;}.px{padding-left:1rem;padding-right:1rem;}.px-2{padding-left:0.5rem;padding-right:0.5rem;}.px-5{padding-left:1.25rem;padding-right:1.25rem;}.children-pb-2>*{padding-bottom:0.5rem;}.children-pt8>*{padding-top:2rem;}.all\:font-mono *{font-family:"Fira Code","Fira Mono",ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;}.all\:text-xs *{font-size:0.75rem;line-height:1rem;}.font-semibold{font-weight:600;}.dark .dark\:text-darkAccent{--un-text-opacity:1;color:rgba(103,214,237,var(--un-text-opacity));}.dark .dark\:text-darkPrimaryText{--un-text-opacity:1;color:rgba(227,227,227,var(--un-text-opacity));}.text-accent{--un-text-opacity:1;color:rgba(53,120,229,var(--un-text-opacity));}.text-primaryText{--un-text-opacity:1;color:rgba(28,30,33,var(--un-text-opacity));}.transition-colors-250{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:250ms;}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}@media (max-width: 639.9px){.lt-sm\:absolute{position:absolute;}.lt-sm\:z-1999{z-index:1999;}.lt-sm\:h-screen{height:100vh;}.lt-sm\:flex{display:flex;}.lt-sm\:shadow{--un-shadow:var(--un-shadow-inset) 0 1px 3px 0 var(--un-shadow-color, rgba(0,0,0,0.1)),var(--un-shadow-inset) 0 1px 2px -1px var(--un-shadow-color, rgba(0,0,0,0.1));box-shadow:var(--un-ring-offset-shadow, 0 0 #0000), var(--un-ring-shadow, 0 0 #0000), var(--un-shadow);}.lt-sm\:shadow-lg{--un-shadow:var(--un-shadow-inset) 0 10px 15px -3px var(--un-shadow-color, rgba(0,0,0,0.1)),var(--un-shadow-inset) 0 4px 6px -4px var(--un-shadow-color, rgba(0,0,0,0.1));box-shadow:var(--un-ring-offset-shadow, 0 0 #0000), var(--un-ring-shadow, 0 0 #0000), var(--un-shadow);}.lt-sm\:transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}}*:not(h1,h2,h3,h4,h5,h6){margin:0;padding:0}*{box-sizing:border-box;font-family:Rubik,sans-serif}::-webkit-scrollbar{width:.25rem;height:3px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{border-radius:.25rem}code{padding:.05rem .25rem}code.code-block{padding:.5rem}#sidebar{width:18.75rem}@media screen and (max-width: 640px){#sidebar{--translate-x: -18.75rem;transform:translate(var(--translate-x))}} diff --git a/examples/api/dist/assets/index.js b/examples/api/dist/assets/index.js index d1121b86296..9f653c886b9 100644 --- a/examples/api/dist/assets/index.js +++ b/examples/api/dist/assets/index.js @@ -1,50 +1,9 @@ -const vo=function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))i(l);new MutationObserver(l=>{for(const o of l)if(o.type==="childList")for(const u of o.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&i(u)}).observe(document,{childList:!0,subtree:!0});function n(l){const o={};return l.integrity&&(o.integrity=l.integrity),l.referrerpolicy&&(o.referrerPolicy=l.referrerpolicy),l.crossorigin==="use-credentials"?o.credentials="include":l.crossorigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function i(l){if(l.ep)return;l.ep=!0;const o=n(l);fetch(l.href,o)}};vo();function V(){}function Ps(e){return e()}function ns(){return Object.create(null)}function oe(e){e.forEach(Ps)}function wo(e){return typeof e=="function"}function pe(e,t){return e!=e?t==t:e!==t||e&&typeof e=="object"||typeof e=="function"}let ti;function ko(e,t){return ti||(ti=document.createElement("a")),ti.href=t,e===ti.href}function Mo(e){return Object.keys(e).length===0}function Co(e,...t){if(e==null)return V;const n=e.subscribe(...t);return n.unsubscribe?()=>n.unsubscribe():n}function Os(e,t,n){e.$$.on_destroy.push(Co(t,n))}function s(e,t){e.appendChild(t)}function h(e,t,n){e.insertBefore(t,n||null)}function p(e){e.parentNode.removeChild(e)}function yt(e,t){for(let n=0;ne.removeEventListener(t,n,i)}function ai(e){return function(t){return t.preventDefault(),e.call(this,t)}}function r(e,t,n){n==null?e.removeAttribute(t):e.getAttribute(t)!==n&&e.setAttribute(t,n)}function se(e){return e===""?null:+e}function Ao(e){return Array.from(e.childNodes)}function Z(e,t){t=""+t,e.wholeText!==t&&(e.data=t)}function q(e,t){e.value=t==null?"":t}function Ht(e,t){for(let n=0;n{si.delete(e),i&&(n&&e.d(1),i())}),e.o(t)}else i&&i()}function ui(e){e&&e.c()}function xt(e,t,n,i){const{fragment:l,on_mount:o,on_destroy:u,after_update:d}=e.$$;l&&l.m(t,n),i||Ft(()=>{const c=o.map(Ps).filter(wo);u?u.push(...c):oe(c),e.$$.on_mount=[]}),d.forEach(Ft)}function $t(e,t){const n=e.$$;n.fragment!==null&&(oe(n.on_destroy),n.fragment&&n.fragment.d(t),n.on_destroy=n.fragment=null,n.ctx=[])}function Wo(e,t){e.$$.dirty[0]===-1&&(Yt.push(e),zo(),e.$$.dirty.fill(0)),e.$$.dirty[t/31|0]|=1<{const y=v.length?v[0]:_;return f.ctx&&l(f.ctx[k],f.ctx[k]=y)&&(!f.skip_bound&&f.bound[k]&&f.bound[k](y),g&&Wo(e,k)),_}):[],f.update(),g=!0,oe(f.before_update),f.fragment=i?i(f.ctx):!1,t.target){if(t.hydrate){const k=Ao(t.target);f.fragment&&f.fragment.l(k),k.forEach(p)}else f.fragment&&f.fragment.c();t.intro&&Te(e.$$.fragment),xt(e,t.target,t.anchor,t.customElement),Is()}Qt(c)}class ye{$destroy(){$t(this,1),this.$destroy=V}$on(t,n){const i=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return i.push(n),()=>{const l=i.indexOf(n);l!==-1&&i.splice(l,1)}}$set(t){this.$$set&&!Mo(t)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}}const It=[];function Hs(e,t=V){let n;const i=new Set;function l(d){if(pe(e,d)&&(e=d,n)){const c=!It.length;for(const f of i)f[1](),It.push(f,e);if(c){for(let f=0;f{i.delete(f),i.size===0&&(n(),n=null)}}return{set:l,update:o,subscribe:u}}var Do=Object.defineProperty,Me=(e,t)=>{for(var n in t)Do(e,n,{get:t[n],enumerable:!0})},Po={};Me(Po,{convertFileSrc:()=>Fs,invoke:()=>ci,transformCallback:()=>vt});function Oo(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function vt(e,t=!1){let n=Oo(),i=`_${n}`;return Object.defineProperty(window,i,{value:l=>(t&&Reflect.deleteProperty(window,i),e==null?void 0:e(l)),writable:!1,configurable:!0}),n}async function ci(e,t={}){return new Promise((n,i)=>{let l=vt(u=>{n(u),Reflect.deleteProperty(window,`_${o}`)},!0),o=vt(u=>{i(u),Reflect.deleteProperty(window,`_${l}`)},!0);window.__TAURI_IPC__({cmd:e,callback:l,error:o,...t})})}function Fs(e,t="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${t}.localhost/${n}`:`${t}://localhost/${n}`}async function L(e){return ci("tauri",e)}var Ro={};Me(Ro,{Child:()=>Ns,Command:()=>Yi,EventEmitter:()=>oi,open:()=>Ki});async function Io(e,t,n=[],i){return typeof n=="object"&&Object.freeze(n),L({__tauriModule:"Shell",message:{cmd:"execute",program:t,args:n,options:i,onEventFn:vt(e)}})}var oi=class{constructor(){this.eventListeners=Object.create(null)}addListener(e,t){return this.on(e,t)}removeListener(e,t){return this.off(e,t)}on(e,t){return e in this.eventListeners?this.eventListeners[e].push(t):this.eventListeners[e]=[t],this}once(e,t){let n=(...i)=>{this.removeListener(e,n),t(...i)};return this.addListener(e,n)}off(e,t){return e in this.eventListeners&&(this.eventListeners[e]=this.eventListeners[e].filter(n=>n!==t)),this}removeAllListeners(e){return e?delete this.eventListeners[e]:this.eventListeners=Object.create(null),this}emit(e,...t){if(e in this.eventListeners){let n=this.eventListeners[e];for(let i of n)i(...t);return!0}return!1}listenerCount(e){return e in this.eventListeners?this.eventListeners[e].length:0}prependListener(e,t){return e in this.eventListeners?this.eventListeners[e].unshift(t):this.eventListeners[e]=[t],this}prependOnceListener(e,t){let n=(...i)=>{this.removeListener(e,n),t(...i)};return this.prependListener(e,n)}},Ns=class{constructor(e){this.pid=e}async write(e){return L({__tauriModule:"Shell",message:{cmd:"stdinWrite",pid:this.pid,buffer:typeof e=="string"?e:Array.from(e)}})}async kill(){return L({__tauriModule:"Shell",message:{cmd:"killChild",pid:this.pid}})}},Yi=class extends oi{constructor(e,t=[],n){super(),this.stdout=new oi,this.stderr=new oi,this.program=e,this.args=typeof t=="string"?[t]:t,this.options=n!=null?n:{}}static sidecar(e,t=[],n){let i=new Yi(e,t,n);return i.options.sidecar=!0,i}async spawn(){return Io(e=>{switch(e.event){case"Error":this.emit("error",e.payload);break;case"Terminated":this.emit("close",e.payload);break;case"Stdout":this.stdout.emit("data",e.payload);break;case"Stderr":this.stderr.emit("data",e.payload);break}},this.program,this.args,this.options).then(e=>new Ns(e))}async execute(){return new Promise((e,t)=>{this.on("error",t);let n=[],i=[];this.stdout.on("data",l=>{n.push(l)}),this.stderr.on("data",l=>{i.push(l)}),this.on("close",l=>{e({code:l.code,signal:l.signal,stdout:n.join(` -`),stderr:i.join(` -`)})}),this.spawn().catch(t)})}};async function Ki(e,t){return L({__tauriModule:"Shell",message:{cmd:"open",path:e,with:t}})}var Ho={};Me(Ho,{TauriEvent:()=>Bs,emit:()=>_i,listen:()=>tn,once:()=>Vs});async function js(e,t){return L({__tauriModule:"Event",message:{cmd:"unlisten",event:e,eventId:t}})}async function Us(e,t,n){await L({__tauriModule:"Event",message:{cmd:"emit",event:e,windowLabel:t,payload:n}})}async function Qi(e,t,n){return L({__tauriModule:"Event",message:{cmd:"listen",event:e,windowLabel:t,handler:vt(n)}}).then(i=>async()=>js(e,i))}async function qs(e,t,n){return Qi(e,t,i=>{n(i),js(e,i.id).catch(()=>{})})}var Bs=(e=>(e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_CREATED="tauri://window-created",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_FILE_DROP="tauri://file-drop",e.WINDOW_FILE_DROP_HOVER="tauri://file-drop-hover",e.WINDOW_FILE_DROP_CANCELLED="tauri://file-drop-cancelled",e.MENU="tauri://menu",e.CHECK_UPDATE="tauri://update",e.UPDATE_AVAILABLE="tauri://update-available",e.INSTALL_UPDATE="tauri://update-install",e.STATUS_UPDATE="tauri://update-status",e.DOWNLOAD_PROGRESS="tauri://update-download-progress",e))(Bs||{});async function tn(e,t){return Qi(e,null,t)}async function Vs(e,t){return qs(e,null,t)}async function _i(e,t){return Us(e,void 0,t)}var Fo={};Me(Fo,{CloseRequestedEvent:()=>Ks,LogicalPosition:()=>Gs,LogicalSize:()=>di,PhysicalPosition:()=>ot,PhysicalSize:()=>gt,UserAttentionType:()=>Zi,WebviewWindow:()=>wt,WebviewWindowHandle:()=>Xs,WindowManager:()=>Ys,appWindow:()=>Ge,availableMonitors:()=>Uo,currentMonitor:()=>No,getAll:()=>Js,getCurrent:()=>Kt,primaryMonitor:()=>jo});var di=class{constructor(e,t){this.type="Logical",this.width=e,this.height=t}},gt=class{constructor(e,t){this.type="Physical",this.width=e,this.height=t}toLogical(e){return new di(this.width/e,this.height/e)}},Gs=class{constructor(e,t){this.type="Logical",this.x=e,this.y=t}},ot=class{constructor(e,t){this.type="Physical",this.x=e,this.y=t}toLogical(e){return new Gs(this.x/e,this.y/e)}},Zi=(e=>(e[e.Critical=1]="Critical",e[e.Informational=2]="Informational",e))(Zi||{});function Kt(){return new wt(window.__TAURI_METADATA__.__currentWindow.label,{skip:!0})}function Js(){return window.__TAURI_METADATA__.__windows.map(e=>new wt(e.label,{skip:!0}))}var ls=["tauri://created","tauri://error"],Xs=class{constructor(e){this.label=e,this.listeners=Object.create(null)}async listen(e,t){return this._handleTauriEvent(e,t)?Promise.resolve(()=>{let n=this.listeners[e];n.splice(n.indexOf(t),1)}):Qi(e,this.label,t)}async once(e,t){return this._handleTauriEvent(e,t)?Promise.resolve(()=>{let n=this.listeners[e];n.splice(n.indexOf(t),1)}):qs(e,this.label,t)}async emit(e,t){if(ls.includes(e)){for(let n of this.listeners[e]||[])n({event:e,id:-1,windowLabel:this.label,payload:t});return Promise.resolve()}return Us(e,this.label,t)}_handleTauriEvent(e,t){return ls.includes(e)?(e in this.listeners?this.listeners[e].push(t):this.listeners[e]=[t],!0):!1}},Ys=class extends Xs{async scaleFactor(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"scaleFactor"}}}})}async innerPosition(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"innerPosition"}}}}).then(({x:e,y:t})=>new ot(e,t))}async outerPosition(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"outerPosition"}}}}).then(({x:e,y:t})=>new ot(e,t))}async innerSize(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"innerSize"}}}}).then(({width:e,height:t})=>new gt(e,t))}async outerSize(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"outerSize"}}}}).then(({width:e,height:t})=>new gt(e,t))}async isFullscreen(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isFullscreen"}}}})}async isMinimized(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isMinimized"}}}})}async isMaximized(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isMaximized"}}}})}async isDecorated(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isDecorated"}}}})}async isResizable(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isResizable"}}}})}async isMaximizable(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isMaximizable"}}}})}async isMinimizable(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isMinimizable"}}}})}async isClosable(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isClosable"}}}})}async isVisible(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"isVisible"}}}})}async title(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"title"}}}})}async theme(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"theme"}}}})}async center(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"center"}}}})}async requestUserAttention(e){let t=null;return e&&(e===1?t={type:"Critical"}:t={type:"Informational"}),L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"requestUserAttention",payload:t}}}})}async setResizable(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setResizable",payload:e}}}})}async setMaximizable(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setMaximizable",payload:e}}}})}async setMinimizable(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setMinimizable",payload:e}}}})}async setClosable(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setClosable",payload:e}}}})}async setTitle(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setTitle",payload:e}}}})}async maximize(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"maximize"}}}})}async unmaximize(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"unmaximize"}}}})}async toggleMaximize(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"toggleMaximize"}}}})}async minimize(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"minimize"}}}})}async unminimize(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"unminimize"}}}})}async show(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"show"}}}})}async hide(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"hide"}}}})}async close(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"close"}}}})}async setDecorations(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setDecorations",payload:e}}}})}async setAlwaysOnTop(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setAlwaysOnTop",payload:e}}}})}async setContentProtected(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setContentProtected",payload:e}}}})}async setSize(e){if(!e||e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `size` argument must be either a LogicalSize or a PhysicalSize instance");return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setSize",payload:{type:e.type,data:{width:e.width,height:e.height}}}}}})}async setMinSize(e){if(e&&e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `size` argument must be either a LogicalSize or a PhysicalSize instance");return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setMinSize",payload:e?{type:e.type,data:{width:e.width,height:e.height}}:null}}}})}async setMaxSize(e){if(e&&e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `size` argument must be either a LogicalSize or a PhysicalSize instance");return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setMaxSize",payload:e?{type:e.type,data:{width:e.width,height:e.height}}:null}}}})}async setPosition(e){if(!e||e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `position` argument must be either a LogicalPosition or a PhysicalPosition instance");return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setPosition",payload:{type:e.type,data:{x:e.x,y:e.y}}}}}})}async setFullscreen(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setFullscreen",payload:e}}}})}async setFocus(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setFocus"}}}})}async setIcon(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setIcon",payload:{icon:typeof e=="string"?e:Array.from(e)}}}}})}async setSkipTaskbar(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setSkipTaskbar",payload:e}}}})}async setCursorGrab(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setCursorGrab",payload:e}}}})}async setCursorVisible(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setCursorVisible",payload:e}}}})}async setCursorIcon(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setCursorIcon",payload:e}}}})}async setCursorPosition(e){if(!e||e.type!=="Logical"&&e.type!=="Physical")throw new Error("the `position` argument must be either a LogicalPosition or a PhysicalPosition instance");return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setCursorPosition",payload:{type:e.type,data:{x:e.x,y:e.y}}}}}})}async setIgnoreCursorEvents(e){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"setIgnoreCursorEvents",payload:e}}}})}async startDragging(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{label:this.label,cmd:{type:"startDragging"}}}})}async onResized(e){return this.listen("tauri://resize",t=>{t.payload=Zs(t.payload),e(t)})}async onMoved(e){return this.listen("tauri://move",t=>{t.payload=Qs(t.payload),e(t)})}async onCloseRequested(e){return this.listen("tauri://close-requested",t=>{let n=new Ks(t);Promise.resolve(e(n)).then(()=>{if(!n.isPreventDefault())return this.close()})})}async onFocusChanged(e){let t=await this.listen("tauri://focus",i=>{e({...i,payload:!0})}),n=await this.listen("tauri://blur",i=>{e({...i,payload:!1})});return()=>{t(),n()}}async onScaleChanged(e){return this.listen("tauri://scale-change",e)}async onMenuClicked(e){return this.listen("tauri://menu",e)}async onFileDropEvent(e){let t=await this.listen("tauri://file-drop",l=>{e({...l,payload:{type:"drop",paths:l.payload}})}),n=await this.listen("tauri://file-drop-hover",l=>{e({...l,payload:{type:"hover",paths:l.payload}})}),i=await this.listen("tauri://file-drop-cancelled",l=>{e({...l,payload:{type:"cancel"}})});return()=>{t(),n(),i()}}async onThemeChanged(e){return this.listen("tauri://theme-changed",e)}},Ks=class{constructor(e){this._preventDefault=!1,this.event=e.event,this.windowLabel=e.windowLabel,this.id=e.id}preventDefault(){this._preventDefault=!0}isPreventDefault(){return this._preventDefault}},wt=class extends Ys{constructor(e,t={}){super(e),t!=null&&t.skip||L({__tauriModule:"Window",message:{cmd:"createWebview",data:{options:{label:e,...t}}}}).then(async()=>this.emit("tauri://created")).catch(async n=>this.emit("tauri://error",n))}static getByLabel(e){return Js().some(t=>t.label===e)?new wt(e,{skip:!0}):null}},Ge;"__TAURI_METADATA__"in window?Ge=new wt(window.__TAURI_METADATA__.__currentWindow.label,{skip:!0}):(console.warn(`Could not find "window.__TAURI_METADATA__". The "appWindow" value will reference the "main" window label. -Note that this is not an issue if running this frontend on a browser instead of a Tauri window.`),Ge=new wt("main",{skip:!0}));function xi(e){return e===null?null:{name:e.name,scaleFactor:e.scaleFactor,position:Qs(e.position),size:Zs(e.size)}}function Qs(e){return new ot(e.x,e.y)}function Zs(e){return new gt(e.width,e.height)}async function No(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{cmd:{type:"currentMonitor"}}}}).then(xi)}async function jo(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{cmd:{type:"primaryMonitor"}}}}).then(xi)}async function Uo(){return L({__tauriModule:"Window",message:{cmd:"manage",data:{cmd:{type:"availableMonitors"}}}}).then(e=>e.map(xi))}function qo(){return navigator.appVersion.includes("Win")}var Bo={};Me(Bo,{EOL:()=>Vo,arch:()=>Xo,locale:()=>Ko,platform:()=>xs,tempdir:()=>Yo,type:()=>Jo,version:()=>Go});var Vo=qo()?`\r -`:` -`;async function xs(){return L({__tauriModule:"Os",message:{cmd:"platform"}})}async function Go(){return L({__tauriModule:"Os",message:{cmd:"version"}})}async function Jo(){return L({__tauriModule:"Os",message:{cmd:"osType"}})}async function Xo(){return L({__tauriModule:"Os",message:{cmd:"arch"}})}async function Yo(){return L({__tauriModule:"Os",message:{cmd:"tempdir"}})}async function Ko(){return L({__tauriModule:"Os",message:{cmd:"locale"}})}var Qo={};Me(Qo,{getName:()=>eo,getTauriVersion:()=>to,getVersion:()=>$s,hide:()=>io,show:()=>no});async function $s(){return L({__tauriModule:"App",message:{cmd:"getAppVersion"}})}async function eo(){return L({__tauriModule:"App",message:{cmd:"getAppName"}})}async function to(){return L({__tauriModule:"App",message:{cmd:"getTauriVersion"}})}async function no(){return L({__tauriModule:"App",message:{cmd:"show"}})}async function io(){return L({__tauriModule:"App",message:{cmd:"hide"}})}var Zo={};Me(Zo,{exit:()=>lo,relaunch:()=>$i});async function lo(e=0){return L({__tauriModule:"Process",message:{cmd:"exit",exitCode:e}})}async function $i(){return L({__tauriModule:"Process",message:{cmd:"relaunch"}})}function xo(e){let t,n,i,l,o,u,d,c,f,g,k,_,v,y,b,A,P,I,O,j,W,C,T,E,M,N;return{c(){t=a("p"),t.innerHTML=`This is a demo of Tauri's API capabilities using the @tauri-apps/api package. It's used as the main validation app, serving as the test bed of our - development process. In the future, this app will be used on Tauri's integration - tests.`,n=m(),i=a("br"),l=m(),o=a("br"),u=m(),d=a("pre"),c=z("App name: "),f=a("code"),g=z(e[2]),k=z(` -App version: `),_=a("code"),v=z(e[0]),y=z(` -Tauri version: `),b=a("code"),A=z(e[1]),P=z(` -`),I=m(),O=a("br"),j=m(),W=a("div"),C=a("button"),C.textContent="Close application",T=m(),E=a("button"),E.textContent="Relaunch application",r(C,"class","btn"),r(E,"class","btn"),r(W,"class","flex flex-wrap gap-1 shadow-")},m(U,J){h(U,t,J),h(U,n,J),h(U,i,J),h(U,l,J),h(U,o,J),h(U,u,J),h(U,d,J),s(d,c),s(d,f),s(f,g),s(d,k),s(d,_),s(_,v),s(d,y),s(d,b),s(b,A),s(d,P),h(U,I,J),h(U,O,J),h(U,j,J),h(U,W,J),s(W,C),s(W,T),s(W,E),M||(N=[S(C,"click",e[3]),S(E,"click",e[4])],M=!0)},p(U,[J]){J&4&&Z(g,U[2]),J&1&&Z(v,U[0]),J&2&&Z(A,U[1])},i:V,o:V,d(U){U&&p(t),U&&p(n),U&&p(i),U&&p(l),U&&p(o),U&&p(u),U&&p(d),U&&p(I),U&&p(O),U&&p(j),U&&p(W),M=!1,oe(N)}}}function $o(e,t,n){let i="0.0.0",l="0.0.0",o="Unknown";eo().then(c=>{n(2,o=c)}),$s().then(c=>{n(0,i=c)}),to().then(c=>{n(1,l=c)});async function u(){await lo()}async function d(){await $i()}return[i,l,o,u,d]}class ea extends ye{constructor(t){super(),ge(this,t,$o,xo,pe,{})}}var ta={};Me(ta,{getMatches:()=>so});async function so(){return L({__tauriModule:"Cli",message:{cmd:"cliMatches"}})}function na(e){let t,n,i,l,o,u,d,c,f,g,k,_,v;return{c(){t=a("p"),t.innerHTML=`This binary can be run from the terminal and takes the following arguments: -

  --config <PATH>
-  --theme <light|dark|system>
-  --verbose
- Additionally, it has a update --background subcommand.`,n=m(),i=a("br"),l=m(),o=a("div"),o.textContent="Note that the arguments are only parsed, not implemented.",u=m(),d=a("br"),c=m(),f=a("br"),g=m(),k=a("button"),k.textContent="Get matches",r(o,"class","note"),r(k,"class","btn"),r(k,"id","cli-matches")},m(y,b){h(y,t,b),h(y,n,b),h(y,i,b),h(y,l,b),h(y,o,b),h(y,u,b),h(y,d,b),h(y,c,b),h(y,f,b),h(y,g,b),h(y,k,b),_||(v=S(k,"click",e[0]),_=!0)},p:V,i:V,o:V,d(y){y&&p(t),y&&p(n),y&&p(i),y&&p(l),y&&p(o),y&&p(u),y&&p(d),y&&p(c),y&&p(f),y&&p(g),y&&p(k),_=!1,v()}}}function ia(e,t,n){let{onMessage:i}=t;function l(){so().then(i).catch(i)}return e.$$set=o=>{"onMessage"in o&&n(1,i=o.onMessage)},[l,i]}class la extends ye{constructor(t){super(),ge(this,t,ia,na,pe,{onMessage:1})}}function sa(e){let t,n,i,l,o,u,d,c;return{c(){t=a("div"),n=a("button"),n.textContent="Call Log API",i=m(),l=a("button"),l.textContent="Call Request (async) API",o=m(),u=a("button"),u.textContent="Send event to Rust",r(n,"class","btn"),r(n,"id","log"),r(l,"class","btn"),r(l,"id","request"),r(u,"class","btn"),r(u,"id","event")},m(f,g){h(f,t,g),s(t,n),s(t,i),s(t,l),s(t,o),s(t,u),d||(c=[S(n,"click",e[0]),S(l,"click",e[1]),S(u,"click",e[2])],d=!0)},p:V,i:V,o:V,d(f){f&&p(t),d=!1,oe(c)}}}function oa(e,t,n){let{onMessage:i}=t,l;_t(async()=>{l=await tn("rust-event",i)}),Xi(()=>{l&&l()});function o(){ci("log_operation",{event:"tauri-click",payload:"this payload is optional because we used Option in Rust"})}function u(){ci("perform_request",{endpoint:"dummy endpoint arg",body:{id:5,name:"test"}}).then(i).catch(i)}function d(){_i("js-event","this is the payload string")}return e.$$set=c=>{"onMessage"in c&&n(3,i=c.onMessage)},[o,u,d,i]}class aa extends ye{constructor(t){super(),ge(this,t,oa,sa,pe,{onMessage:3})}}var ra={};Me(ra,{ask:()=>ao,confirm:()=>ca,message:()=>ua,open:()=>el,save:()=>oo});async function el(e={}){return typeof e=="object"&&Object.freeze(e),L({__tauriModule:"Dialog",message:{cmd:"openDialog",options:e}})}async function oo(e={}){return typeof e=="object"&&Object.freeze(e),L({__tauriModule:"Dialog",message:{cmd:"saveDialog",options:e}})}async function ua(e,t){var i,l;let n=typeof t=="string"?{title:t}:t;return L({__tauriModule:"Dialog",message:{cmd:"messageDialog",message:e.toString(),title:(i=n==null?void 0:n.title)==null?void 0:i.toString(),type:n==null?void 0:n.type,buttonLabel:(l=n==null?void 0:n.okLabel)==null?void 0:l.toString()}})}async function ao(e,t){var i,l,o,u,d;let n=typeof t=="string"?{title:t}:t;return L({__tauriModule:"Dialog",message:{cmd:"askDialog",message:e.toString(),title:(i=n==null?void 0:n.title)==null?void 0:i.toString(),type:n==null?void 0:n.type,buttonLabels:[(o=(l=n==null?void 0:n.okLabel)==null?void 0:l.toString())!=null?o:"Yes",(d=(u=n==null?void 0:n.cancelLabel)==null?void 0:u.toString())!=null?d:"No"]}})}async function ca(e,t){var i,l,o,u,d;let n=typeof t=="string"?{title:t}:t;return L({__tauriModule:"Dialog",message:{cmd:"confirmDialog",message:e.toString(),title:(i=n==null?void 0:n.title)==null?void 0:i.toString(),type:n==null?void 0:n.type,buttonLabels:[(o=(l=n==null?void 0:n.okLabel)==null?void 0:l.toString())!=null?o:"Ok",(d=(u=n==null?void 0:n.cancelLabel)==null?void 0:u.toString())!=null?d:"Cancel"]}})}var da={};Me(da,{BaseDirectory:()=>en,Dir:()=>en,copyFile:()=>_a,createDir:()=>ma,exists:()=>ya,readBinaryFile:()=>tl,readDir:()=>ro,readTextFile:()=>fa,removeDir:()=>ha,removeFile:()=>ba,renameFile:()=>ga,writeBinaryFile:()=>pa,writeFile:()=>Ji,writeTextFile:()=>Ji});var en=(e=>(e[e.Audio=1]="Audio",e[e.Cache=2]="Cache",e[e.Config=3]="Config",e[e.Data=4]="Data",e[e.LocalData=5]="LocalData",e[e.Desktop=6]="Desktop",e[e.Document=7]="Document",e[e.Download=8]="Download",e[e.Executable=9]="Executable",e[e.Font=10]="Font",e[e.Home=11]="Home",e[e.Picture=12]="Picture",e[e.Public=13]="Public",e[e.Runtime=14]="Runtime",e[e.Template=15]="Template",e[e.Video=16]="Video",e[e.Resource=17]="Resource",e[e.App=18]="App",e[e.Log=19]="Log",e[e.Temp=20]="Temp",e[e.AppConfig=21]="AppConfig",e[e.AppData=22]="AppData",e[e.AppLocalData=23]="AppLocalData",e[e.AppCache=24]="AppCache",e[e.AppLog=25]="AppLog",e))(en||{});async function fa(e,t={}){return L({__tauriModule:"Fs",message:{cmd:"readTextFile",path:e,options:t}})}async function tl(e,t={}){let n=await L({__tauriModule:"Fs",message:{cmd:"readFile",path:e,options:t}});return Uint8Array.from(n)}async function Ji(e,t,n){typeof n=="object"&&Object.freeze(n),typeof e=="object"&&Object.freeze(e);let i={path:"",contents:""},l=n;return typeof e=="string"?i.path=e:(i.path=e.path,i.contents=e.contents),typeof t=="string"?i.contents=t!=null?t:"":l=t,L({__tauriModule:"Fs",message:{cmd:"writeFile",path:i.path,contents:Array.from(new TextEncoder().encode(i.contents)),options:l}})}async function pa(e,t,n){typeof n=="object"&&Object.freeze(n),typeof e=="object"&&Object.freeze(e);let i={path:"",contents:[]},l=n;return typeof e=="string"?i.path=e:(i.path=e.path,i.contents=e.contents),t&&"dir"in t?l=t:typeof e=="string"&&(i.contents=t!=null?t:[]),L({__tauriModule:"Fs",message:{cmd:"writeFile",path:i.path,contents:Array.from(i.contents instanceof ArrayBuffer?new Uint8Array(i.contents):i.contents),options:l}})}async function ro(e,t={}){return L({__tauriModule:"Fs",message:{cmd:"readDir",path:e,options:t}})}async function ma(e,t={}){return L({__tauriModule:"Fs",message:{cmd:"createDir",path:e,options:t}})}async function ha(e,t={}){return L({__tauriModule:"Fs",message:{cmd:"removeDir",path:e,options:t}})}async function _a(e,t,n={}){return L({__tauriModule:"Fs",message:{cmd:"copyFile",source:e,destination:t,options:n}})}async function ba(e,t={}){return L({__tauriModule:"Fs",message:{cmd:"removeFile",path:e,options:t}})}async function ga(e,t,n={}){return L({__tauriModule:"Fs",message:{cmd:"renameFile",oldPath:e,newPath:t,options:n}})}async function ya(e,t={}){return L({__tauriModule:"Fs",message:{cmd:"exists",path:e,options:t}})}function va(e){let t,n,i,l,o,u,d,c,f,g,k,_,v,y,b,A,P,I,O,j,W,C,T,E;return{c(){t=a("div"),n=a("input"),i=m(),l=a("input"),o=m(),u=a("br"),d=m(),c=a("div"),f=a("input"),g=m(),k=a("label"),k.textContent="Multiple",_=m(),v=a("div"),y=a("input"),b=m(),A=a("label"),A.textContent="Directory",P=m(),I=a("br"),O=m(),j=a("button"),j.textContent="Open dialog",W=m(),C=a("button"),C.textContent="Open save dialog",r(n,"class","input"),r(n,"id","dialog-default-path"),r(n,"placeholder","Default path"),r(l,"class","input"),r(l,"id","dialog-filter"),r(l,"placeholder","Extensions filter, comma-separated"),r(t,"class","flex gap-2 children:grow"),r(f,"type","checkbox"),r(f,"id","dialog-multiple"),r(k,"for","dialog-multiple"),r(y,"type","checkbox"),r(y,"id","dialog-directory"),r(A,"for","dialog-directory"),r(j,"class","btn"),r(j,"id","open-dialog"),r(C,"class","btn"),r(C,"id","save-dialog")},m(M,N){h(M,t,N),s(t,n),q(n,e[0]),s(t,i),s(t,l),q(l,e[1]),h(M,o,N),h(M,u,N),h(M,d,N),h(M,c,N),s(c,f),f.checked=e[2],s(c,g),s(c,k),h(M,_,N),h(M,v,N),s(v,y),y.checked=e[3],s(v,b),s(v,A),h(M,P,N),h(M,I,N),h(M,O,N),h(M,j,N),h(M,W,N),h(M,C,N),T||(E=[S(n,"input",e[8]),S(l,"input",e[9]),S(f,"change",e[10]),S(y,"change",e[11]),S(j,"click",e[4]),S(C,"click",e[5])],T=!0)},p(M,[N]){N&1&&n.value!==M[0]&&q(n,M[0]),N&2&&l.value!==M[1]&&q(l,M[1]),N&4&&(f.checked=M[2]),N&8&&(y.checked=M[3])},i:V,o:V,d(M){M&&p(t),M&&p(o),M&&p(u),M&&p(d),M&&p(c),M&&p(_),M&&p(v),M&&p(P),M&&p(I),M&&p(O),M&&p(j),M&&p(W),M&&p(C),T=!1,oe(E)}}}function wa(e,t){var n=new Blob([e],{type:"application/octet-binary"}),i=new FileReader;i.onload=function(l){var o=l.target.result;t(o.substr(o.indexOf(",")+1))},i.readAsDataURL(n)}function ka(e,t,n){let{onMessage:i}=t,{insecureRenderHtml:l}=t,o=null,u=null,d=!1,c=!1;function f(){el({title:"My wonderful open dialog",defaultPath:o,filters:u?[{name:"Tauri Example",extensions:u.split(",").map(b=>b.trim())}]:[],multiple:d,directory:c}).then(function(b){if(Array.isArray(b))i(b);else{var A=b,P=A.match(/\S+\.\S+$/g);tl(A).then(function(I){P&&(A.includes(".png")||A.includes(".jpg"))?wa(new Uint8Array(I),function(O){var j="data:image/png;base64,"+O;l('')}):i(b)}).catch(i(b))}}).catch(i)}function g(){oo({title:"My wonderful save dialog",defaultPath:o,filters:u?[{name:"Tauri Example",extensions:u.split(",").map(b=>b.trim())}]:[]}).then(i).catch(i)}function k(){o=this.value,n(0,o)}function _(){u=this.value,n(1,u)}function v(){d=this.checked,n(2,d)}function y(){c=this.checked,n(3,c)}return e.$$set=b=>{"onMessage"in b&&n(6,i=b.onMessage),"insecureRenderHtml"in b&&n(7,l=b.insecureRenderHtml)},[o,u,d,c,f,g,i,l,k,_,v,y]}class Ma extends ye{constructor(t){super(),ge(this,t,ka,va,pe,{onMessage:6,insecureRenderHtml:7})}}function ss(e,t,n){const i=e.slice();return i[9]=t[n],i}function os(e){let t,n=e[9][0]+"",i,l;return{c(){t=a("option"),i=z(n),t.__value=l=e[9][1],t.value=t.__value},m(o,u){h(o,t,u),s(t,i)},p:V,d(o){o&&p(t)}}}function Ca(e){let t,n,i,l,o,u,d,c,f,g,k,_,v,y,b,A,P,I,O,j=e[2],W=[];for(let C=0;CisNaN(parseInt(_))).map(_=>[_,en[_]]);function c(){const _=o.match(/\S+\.\S+$/g),v={dir:as()};(_?tl(o,v):ro(o,v)).then(function(b){if(_)if(o.includes(".png")||o.includes(".jpg"))Ta(new Uint8Array(b),function(A){const P="data:image/png;base64,"+A;l('')});else{const A=String.fromCharCode.apply(null,b);l(''),setTimeout(()=>{const P=document.getElementById("file-response");P.value=A,document.getElementById("file-save").addEventListener("click",function(){Ji(o,P.value,{dir:as()}).catch(i)})})}else i(b)}).catch(i)}function f(){n(1,u.src=Fs(o),u)}function g(){o=this.value,n(0,o)}function k(_){ri[_?"unshift":"push"](()=>{u=_,n(1,u)})}return e.$$set=_=>{"onMessage"in _&&n(5,i=_.onMessage),"insecureRenderHtml"in _&&n(6,l=_.insecureRenderHtml)},[o,u,d,c,f,i,l,g,k]}class La extends ye{constructor(t){super(),ge(this,t,Aa,Ca,pe,{onMessage:5,insecureRenderHtml:6})}}var Sa={};Me(Sa,{Body:()=>at,Client:()=>co,Response:()=>uo,ResponseType:()=>nl,fetch:()=>za,getClient:()=>fi});var nl=(e=>(e[e.JSON=1]="JSON",e[e.Text=2]="Text",e[e.Binary=3]="Binary",e))(nl||{}),at=class{constructor(e,t){this.type=e,this.payload=t}static form(e){let t={},n=(i,l)=>{if(l!==null){let o;typeof l=="string"?o=l:l instanceof Uint8Array||Array.isArray(l)?o=Array.from(l):l instanceof File?o={file:l.name,mime:l.type,fileName:l.name}:typeof l.file=="string"?o={file:l.file,mime:l.mime,fileName:l.fileName}:o={file:Array.from(l.file),mime:l.mime,fileName:l.fileName},t[String(i)]=o}};if(e instanceof FormData)for(let[i,l]of e)n(i,l);else for(let[i,l]of Object.entries(e))n(i,l);return new at("Form",t)}static json(e){return new at("Json",e)}static text(e){return new at("Text",e)}static bytes(e){return new at("Bytes",Array.from(e instanceof ArrayBuffer?new Uint8Array(e):e))}},uo=class{constructor(e){this.url=e.url,this.status=e.status,this.ok=this.status>=200&&this.status<300,this.headers=e.headers,this.rawHeaders=e.rawHeaders,this.data=e.data}},co=class{constructor(e){this.id=e}async drop(){return L({__tauriModule:"Http",message:{cmd:"dropClient",client:this.id}})}async request(e){let t=!e.responseType||e.responseType===1;return t&&(e.responseType=2),L({__tauriModule:"Http",message:{cmd:"httpRequest",client:this.id,options:e}}).then(n=>{let i=new uo(n);if(t){try{i.data=JSON.parse(i.data)}catch(l){if(i.ok&&i.data==="")i.data={};else if(i.ok)throw Error(`Failed to parse response \`${i.data}\` as JSON: ${l}; - try setting the \`responseType\` option to \`ResponseType.Text\` or \`ResponseType.Binary\` if the API does not return a JSON response.`)}return i}return i})}async get(e,t){return this.request({method:"GET",url:e,...t})}async post(e,t,n){return this.request({method:"POST",url:e,body:t,...n})}async put(e,t,n){return this.request({method:"PUT",url:e,body:t,...n})}async patch(e,t){return this.request({method:"PATCH",url:e,...t})}async delete(e,t){return this.request({method:"DELETE",url:e,...t})}};async function fi(e){return L({__tauriModule:"Http",message:{cmd:"createClient",options:e}}).then(t=>new co(t))}var Bi=null;async function za(e,t){var n;return Bi===null&&(Bi=await fi()),Bi.request({url:e,method:(n=t==null?void 0:t.method)!=null?n:"GET",...t})}function rs(e,t,n){const i=e.slice();return i[12]=t[n],i[14]=n,i}function us(e){let t,n,i,l,o,u,d,c,f,g,k,_,v,y,b,A,P,I=e[5],O=[];for(let T=0;TNe(O[T],1,1,()=>{O[T]=null});let W=!e[3]&&ps(),C=!e[3]&&e[8]&&ms();return{c(){t=a("span"),n=a("span"),i=z(e[6]),l=m(),o=a("ul");for(let T=0;T{g[y]=null}),hi(),o=g[l],o?o.p(_,v):(o=g[l]=f[l](_),o.c()),Te(o,1),o.m(t,u))},i(_){d||(Te(o),d=!0)},o(_){Ne(o),d=!1},d(_){_&&p(t),c&&c.d(),g[l].d()}}}function ps(e){let t;return{c(){t=a("span"),t.textContent=",",r(t,"class","comma svelte-gbh3pt")},m(n,i){h(n,t,i)},d(n){n&&p(t)}}}function ms(e){let t;return{c(){t=a("span"),t.textContent=",",r(t,"class","comma svelte-gbh3pt")},m(n,i){h(n,t,i)},d(n){n&&p(t)}}}function Da(e){let t,n,i=e[5].length&&us(e);return{c(){i&&i.c(),t=pi()},m(l,o){i&&i.m(l,o),h(l,t,o),n=!0},p(l,[o]){l[5].length?i?(i.p(l,o),o&32&&Te(i,1)):(i=us(l),i.c(),Te(i,1),i.m(t.parentNode,t)):i&&(mi(),Ne(i,1,1,()=>{i=null}),hi())},i(l){n||(Te(i),n=!0)},o(l){Ne(i),n=!1},d(l){i&&i.d(l),l&&p(t)}}}const Pa="...";function Oa(e,t,n){let{json:i}=t,{depth:l=1/0}=t,{_lvl:o=0}=t,{_last:u=!0}=t;const d=b=>b===null?"null":typeof b;let c,f,g,k,_;const v=b=>{switch(d(b)){case"string":return`"${b}"`;case"function":return"f () {...}";case"symbol":return b.toString();default:return b}},y=()=>{n(8,_=!_)};return e.$$set=b=>{"json"in b&&n(0,i=b.json),"depth"in b&&n(1,l=b.depth),"_lvl"in b&&n(2,o=b._lvl),"_last"in b&&n(3,u=b._last)},e.$$.update=()=>{e.$$.dirty&17&&(n(5,c=d(i)==="object"?Object.keys(i):[]),n(4,f=Array.isArray(i)),n(6,g=f?"[":"{"),n(7,k=f?"]":"}")),e.$$.dirty&6&&n(8,_=le[9].call(n)),r(k,"class","input h-auto w-100%"),r(k,"id","request-body"),r(k,"placeholder","Request body"),r(k,"rows","5"),r(b,"class","btn"),r(b,"id","make-request"),r(C,"class","input"),r(E,"class","input"),r(W,"class","flex gap-2 children:grow"),r($,"type","checkbox"),r(X,"class","btn"),r(X,"type","button")},m(D,G){h(D,t,G),s(t,n),s(n,i),s(n,l),s(n,o),s(n,u),s(n,d),Ht(n,e[0]),s(t,c),s(t,f),s(t,g),s(t,k),q(k,e[1]),s(t,_),s(t,v),s(t,y),s(t,b),h(D,A,G),h(D,P,G),h(D,I,G),h(D,O,G),h(D,j,G),h(D,W,G),s(W,C),q(C,e[2]),s(W,T),s(W,E),q(E,e[3]),h(D,M,G),h(D,N,G),h(D,U,G),h(D,J,G),s(J,$),$.checked=e[5],s(J,me),h(D,te,G),h(D,Y,G),h(D,de,G),h(D,x,G),h(D,H,G),h(D,X,G),h(D,K,G),h(D,ae,G),h(D,ne,G),h(D,he,G),h(D,_e,G),xt(fe,D,G),re=!0,Ae||(Le=[S(n,"change",e[9]),S(k,"input",e[10]),S(t,"submit",ai(e[6])),S(C,"input",e[11]),S(E,"input",e[12]),S($,"change",e[13]),S(X,"click",e[7])],Ae=!0)},p(D,[G]){G&1&&Ht(n,D[0]),G&2&&q(k,D[1]),G&4&&C.value!==D[2]&&q(C,D[2]),G&8&&E.value!==D[3]&&q(E,D[3]),G&32&&($.checked=D[5]);const Se={};G&16&&(Se.json=D[4]),fe.$set(Se)},i(D){re||(Te(fe.$$.fragment,D),re=!0)},o(D){Ne(fe.$$.fragment,D),re=!1},d(D){D&&p(t),D&&p(A),D&&p(P),D&&p(I),D&&p(O),D&&p(j),D&&p(W),D&&p(M),D&&p(N),D&&p(U),D&&p(J),D&&p(te),D&&p(Y),D&&p(de),D&&p(x),D&&p(H),D&&p(X),D&&p(K),D&&p(ae),D&&p(ne),D&&p(he),D&&p(_e),$t(fe,D),Ae=!1,oe(Le)}}}function Ia(e,t,n){let i="GET",l="",{onMessage:o}=t;async function u(){const P=await fi().catch(j=>{throw o(j),j}),O={url:"http://localhost:3003",method:i||"GET"||"GET"};l.startsWith("{")&&l.endsWith("}")||l.startsWith("[")&&l.endsWith("]")?O.body=at.json(JSON.parse(l)):l!==""&&(O.body=at.text(l)),P.request(O).then(o).catch(o)}let d="baz",c="qux",f=null,g=!0;async function k(){const P=await fi().catch(I=>{throw o(I),I});n(4,f=await P.request({url:"http://localhost:3003",method:"POST",body:at.form({foo:d,bar:c}),headers:g?{"Content-Type":"multipart/form-data"}:void 0,responseType:nl.Text}))}function _(){i=Vi(this),n(0,i)}function v(){l=this.value,n(1,l)}function y(){d=this.value,n(2,d)}function b(){c=this.value,n(3,c)}function A(){g=this.checked,n(5,g)}return e.$$set=P=>{"onMessage"in P&&n(8,o=P.onMessage)},[i,l,d,c,f,g,u,k,o,_,v,y,b,A]}class Ha extends ye{constructor(t){super(),ge(this,t,Ia,Ra,pe,{onMessage:8})}}function Fa(e){let t,n,i;return{c(){t=a("button"),t.textContent="Send test notification",r(t,"class","btn"),r(t,"id","notification")},m(l,o){h(l,t,o),n||(i=S(t,"click",Na),n=!0)},p:V,i:V,o:V,d(l){l&&p(t),n=!1,i()}}}function Na(){new Notification("Notification title",{body:"This is the notification body"})}function ja(e,t,n){let{onMessage:i}=t;return e.$$set=l=>{"onMessage"in l&&n(0,i=l.onMessage)},[i]}class Ua extends ye{constructor(t){super(),ge(this,t,ja,Fa,pe,{onMessage:0})}}function hs(e,t,n){const i=e.slice();return i[75]=t[n],i}function _s(e,t,n){const i=e.slice();return i[78]=t[n],i}function bs(e){let t,n,i,l,o,u,d=Object.keys(e[1]),c=[];for(let f=0;fe[43].call(i))},m(f,g){h(f,t,g),h(f,n,g),h(f,i,g),s(i,l);for(let k=0;ke[65].call(Ve)),r(nt,"class","input"),r(nt,"type","number"),r(it,"class","input"),r(it,"type","number"),r(Be,"class","flex gap-2"),r(lt,"class","input grow"),r(lt,"id","title"),r(Jt,"class","btn"),r(Jt,"type","submit"),r(mt,"class","flex gap-1"),r(st,"class","input grow"),r(st,"id","url"),r(Xt,"class","btn"),r(Xt,"id","open-url"),r(ht,"class","flex gap-1"),r(pt,"class","flex flex-col gap-1")},m(w,R){h(w,t,R),h(w,n,R),h(w,i,R),s(i,l),s(i,o),s(i,u),s(i,d),s(i,c),s(i,f),s(i,g),s(i,k),s(i,_),h(w,v,R),h(w,y,R),h(w,b,R),h(w,A,R),s(A,P),s(P,I),s(P,O),O.checked=e[6],s(A,j),s(A,W),s(W,C),s(W,T),T.checked=e[2],s(A,E),s(A,M),s(M,N),s(M,U),U.checked=e[3],s(A,J),s(A,$),s($,me),s($,te),te.checked=e[4],s(A,Y),s(A,de),s(de,x),s(de,H),H.checked=e[5],s(A,X),s(A,K),s(K,ae),s(K,ne),ne.checked=e[7],s(A,he),s(A,_e),s(_e,fe),s(_e,re),re.checked=e[8],s(A,Ae),s(A,Le),s(Le,D),s(Le,G),G.checked=e[9],s(A,Se),s(A,ve),s(ve,ue),s(ve,we),we.checked=e[10],h(w,ie,R),h(w,ze,R),h(w,Je,R),h(w,le,R),s(le,ee),s(ee,F),s(F,ce),s(F,B),q(B,e[17]),s(ee,He),s(ee,kt),s(kt,nn),s(kt,De),q(De,e[18]),s(le,ln),s(le,Xe),s(Xe,Mt),s(Mt,sn),s(Mt,Pe),q(Pe,e[11]),s(Xe,on),s(Xe,Ct),s(Ct,an),s(Ct,Oe),q(Oe,e[12]),s(le,rn),s(le,Ye),s(Ye,Tt),s(Tt,un),s(Tt,Fe),q(Fe,e[13]),s(Ye,Q),s(Ye,rt),s(rt,Nt),s(rt,Re),q(Re,e[14]),s(le,jt),s(le,je),s(je,ut),s(ut,Ut),s(ut,Ee),q(Ee,e[15]),s(je,qt),s(je,ct),s(ct,Bt),s(ct,We),q(We,e[16]),h(w,At,R),h(w,Lt,R),h(w,St,R),h(w,Ce,R),s(Ce,Ue),s(Ue,Ie),s(Ie,dt),s(Ie,Vt),s(Ie,ft),s(ft,cn),s(ft,bi),s(Ie,ll),s(Ie,fn),s(fn,sl),s(fn,gi),s(Ue,ol),s(Ue,Ke),s(Ke,mn),s(Ke,al),s(Ke,hn),s(hn,rl),s(hn,yi),s(Ke,ul),s(Ke,bn),s(bn,cl),s(bn,vi),s(Ce,dl),s(Ce,zt),s(zt,Qe),s(Qe,yn),s(Qe,fl),s(Qe,vn),s(vn,pl),s(vn,wi),s(Qe,ml),s(Qe,kn),s(kn,hl),s(kn,ki),s(zt,_l),s(zt,Ze),s(Ze,Cn),s(Ze,bl),s(Ze,Tn),s(Tn,gl),s(Tn,Mi),s(Ze,yl),s(Ze,Ln),s(Ln,vl),s(Ln,Ci),s(Ce,wl),s(Ce,Et),s(Et,xe),s(xe,zn),s(xe,kl),s(xe,En),s(En,Ml),s(En,Ti),s(xe,Cl),s(xe,Dn),s(Dn,Tl),s(Dn,Ai),s(Et,Al),s(Et,$e),s($e,On),s($e,Ll),s($e,Rn),s(Rn,Sl),s(Rn,Li),s($e,zl),s($e,Hn),s(Hn,El),s(Hn,Si),s(Ce,Wl),s(Ce,Wt),s(Wt,et),s(et,Nn),s(et,Dl),s(et,jn),s(jn,Pl),s(jn,zi),s(et,Ol),s(et,qn),s(qn,Rl),s(qn,Ei),s(Wt,Il),s(Wt,tt),s(tt,Vn),s(tt,Hl),s(tt,Gn),s(Gn,Fl),s(Gn,Wi),s(tt,Nl),s(tt,Xn),s(Xn,jl),s(Xn,Di),h(w,Pi,R),h(w,Oi,R),h(w,Ri,R),h(w,Gt,R),h(w,Ii,R),h(w,qe,R),s(qe,Kn),s(Kn,Dt),Dt.checked=e[19],s(Kn,Ul),s(qe,ql),s(qe,Qn),s(Qn,Pt),Pt.checked=e[20],s(Qn,Bl),s(qe,Vl),s(qe,Zn),s(Zn,Ot),Ot.checked=e[24],s(Zn,Gl),h(w,Hi,R),h(w,Be,R),s(Be,xn),s(xn,Jl),s(xn,Ve);for(let be=0;be=1,g,k,_,v=f&&bs(e),y=e[1][e[0]]&&ys(e);return{c(){t=a("div"),n=a("div"),i=a("input"),l=m(),o=a("button"),o.textContent="New window",u=m(),d=a("br"),c=m(),v&&v.c(),g=m(),y&&y.c(),r(i,"class","input grow"),r(i,"type","text"),r(i,"placeholder","New Window label.."),r(o,"class","btn"),r(n,"class","flex gap-1"),r(t,"class","flex flex-col children:grow gap-2")},m(b,A){h(b,t,A),s(t,n),s(n,i),q(i,e[25]),s(n,l),s(n,o),s(t,u),s(t,d),s(t,c),v&&v.m(t,null),s(t,g),y&&y.m(t,null),k||(_=[S(i,"input",e[42]),S(o,"click",e[39])],k=!0)},p(b,A){A[0]&33554432&&i.value!==b[25]&&q(i,b[25]),A[0]&2&&(f=Object.keys(b[1]).length>=1),f?v?v.p(b,A):(v=bs(b),v.c(),v.m(t,g)):v&&(v.d(1),v=null),b[1][b[0]]?y?y.p(b,A):(y=ys(b),y.c(),y.m(t,null)):y&&(y.d(1),y=null)},i:V,o:V,d(b){b&&p(t),v&&v.d(),y&&y.d(),k=!1,oe(_)}}}function Ba(e,t,n){let i=Ge.label;const l={[Ge.label]:Ge},o=["default","crosshair","hand","arrow","move","text","wait","help","progress","notAllowed","contextMenu","cell","verticalText","alias","copy","noDrop","grab","grabbing","allScroll","zoomIn","zoomOut","eResize","nResize","neResize","nwResize","sResize","seResize","swResize","wResize","ewResize","nsResize","neswResize","nwseResize","colResize","rowResize"];let{onMessage:u}=t,d,c="https://tauri.app",f=!0,g=!0,k=!0,_=!0,v=!1,y=!0,b=!1,A=!0,P=!1,I=null,O=null,j=null,W=null,C=null,T=null,E=null,M=null,N=1,U=new ot(E,M),J=new ot(E,M),$=new gt(I,O),me=new gt(I,O),te,Y,de=!1,x=!0,H=null,X=null,K="default",ae=!1,ne="Awesome Tauri Example!";function he(){Ki(c)}function _e(){l[i].setTitle(ne)}function fe(){l[i].hide(),setTimeout(l[i].show,2e3)}function re(){l[i].minimize(),setTimeout(l[i].unminimize,2e3)}function Ae(){el({multiple:!1}).then(Q=>{typeof Q=="string"&&l[i].setIcon(Q)})}function Le(){if(!d)return;const Q=new wt(d);n(1,l[d]=Q,l),Q.once("tauri://error",function(){u("Error creating new webview")})}function D(){l[i].innerSize().then(Q=>{n(30,$=Q),n(11,I=$.width),n(12,O=$.height)}),l[i].outerSize().then(Q=>{n(31,me=Q)})}function G(){l[i].innerPosition().then(Q=>{n(28,U=Q)}),l[i].outerPosition().then(Q=>{n(29,J=Q),n(17,E=J.x),n(18,M=J.y)})}async function Se(Q){!Q||(te&&te(),Y&&Y(),Y=await Q.listen("tauri://move",G),te=await Q.listen("tauri://resize",D))}async function ve(){await l[i].minimize(),await l[i].requestUserAttention(Zi.Critical),await new Promise(Q=>setTimeout(Q,3e3)),await l[i].requestUserAttention(null)}function ue(){d=this.value,n(25,d)}function we(){i=Vi(this),n(0,i),n(1,l)}const ie=()=>l[i].center();function ze(){v=this.checked,n(6,v)}function Je(){f=this.checked,n(2,f)}function le(){g=this.checked,n(3,g)}function ee(){k=this.checked,n(4,k)}function F(){_=this.checked,n(5,_)}function ce(){y=this.checked,n(7,y)}function B(){b=this.checked,n(8,b)}function He(){A=this.checked,n(9,A)}function kt(){P=this.checked,n(10,P)}function nn(){E=se(this.value),n(17,E)}function De(){M=se(this.value),n(18,M)}function ln(){I=se(this.value),n(11,I)}function Xe(){O=se(this.value),n(12,O)}function Mt(){j=se(this.value),n(13,j)}function sn(){W=se(this.value),n(14,W)}function Pe(){C=se(this.value),n(15,C)}function on(){T=se(this.value),n(16,T)}function Ct(){de=this.checked,n(19,de)}function an(){x=this.checked,n(20,x)}function Oe(){ae=this.checked,n(24,ae)}function rn(){K=Vi(this),n(23,K),n(33,o)}function Ye(){H=se(this.value),n(21,H)}function Tt(){X=se(this.value),n(22,X)}function un(){ne=this.value,n(32,ne)}function Fe(){c=this.value,n(26,c)}return e.$$set=Q=>{"onMessage"in Q&&n(41,u=Q.onMessage)},e.$$.update=()=>{var Q,rt,Nt,Re,jt,je,ut,Ut,Ee,qt,ct,Bt,We,At,Lt,St,Ce,Ue,Ie,dt,Vt,ft;e.$$.dirty[0]&3&&(l[i],G(),D()),e.$$.dirty[0]&7&&((Q=l[i])==null||Q.setResizable(f)),e.$$.dirty[0]&11&&((rt=l[i])==null||rt.setMaximizable(g)),e.$$.dirty[0]&19&&((Nt=l[i])==null||Nt.setMinimizable(k)),e.$$.dirty[0]&35&&((Re=l[i])==null||Re.setClosable(_)),e.$$.dirty[0]&67&&(v?(jt=l[i])==null||jt.maximize():(je=l[i])==null||je.unmaximize()),e.$$.dirty[0]&131&&((ut=l[i])==null||ut.setDecorations(y)),e.$$.dirty[0]&259&&((Ut=l[i])==null||Ut.setAlwaysOnTop(b)),e.$$.dirty[0]&515&&((Ee=l[i])==null||Ee.setContentProtected(A)),e.$$.dirty[0]&1027&&((qt=l[i])==null||qt.setFullscreen(P)),e.$$.dirty[0]&6147&&I&&O&&((ct=l[i])==null||ct.setSize(new gt(I,O))),e.$$.dirty[0]&24579&&(j&&W?(Bt=l[i])==null||Bt.setMinSize(new di(j,W)):(We=l[i])==null||We.setMinSize(null)),e.$$.dirty[0]&98307&&(C>800&&T>400?(At=l[i])==null||At.setMaxSize(new di(C,T)):(Lt=l[i])==null||Lt.setMaxSize(null)),e.$$.dirty[0]&393219&&E!==null&&M!==null&&((St=l[i])==null||St.setPosition(new ot(E,M))),e.$$.dirty[0]&3&&((Ce=l[i])==null||Ce.scaleFactor().then(cn=>n(27,N=cn))),e.$$.dirty[0]&3&&Se(l[i]),e.$$.dirty[0]&524291&&((Ue=l[i])==null||Ue.setCursorGrab(de)),e.$$.dirty[0]&1048579&&((Ie=l[i])==null||Ie.setCursorVisible(x)),e.$$.dirty[0]&8388611&&((dt=l[i])==null||dt.setCursorIcon(K)),e.$$.dirty[0]&6291459&&H!==null&&X!==null&&((Vt=l[i])==null||Vt.setCursorPosition(new ot(H,X))),e.$$.dirty[0]&16777219&&((ft=l[i])==null||ft.setIgnoreCursorEvents(ae))},[i,l,f,g,k,_,v,y,b,A,P,I,O,j,W,C,T,E,M,de,x,H,X,K,ae,d,c,N,U,J,$,me,ne,o,he,_e,fe,re,Ae,Le,ve,u,ue,we,ie,ze,Je,le,ee,F,ce,B,He,kt,nn,De,ln,Xe,Mt,sn,Pe,on,Ct,an,Oe,rn,Ye,Tt,un,Fe]}class Va extends ye{constructor(t){super(),ge(this,t,Ba,qa,pe,{onMessage:41},null,[-1,-1,-1])}}var Ga={};Me(Ga,{isRegistered:()=>Xa,register:()=>po,registerAll:()=>Ja,unregister:()=>mo,unregisterAll:()=>ho});async function po(e,t){return L({__tauriModule:"GlobalShortcut",message:{cmd:"register",shortcut:e,handler:vt(t)}})}async function Ja(e,t){return L({__tauriModule:"GlobalShortcut",message:{cmd:"registerAll",shortcuts:e,handler:vt(t)}})}async function Xa(e){return L({__tauriModule:"GlobalShortcut",message:{cmd:"isRegistered",shortcut:e}})}async function mo(e){return L({__tauriModule:"GlobalShortcut",message:{cmd:"unregister",shortcut:e}})}async function ho(){return L({__tauriModule:"GlobalShortcut",message:{cmd:"unregisterAll"}})}function ws(e,t,n){const i=e.slice();return i[9]=t[n],i}function ks(e){let t,n=e[9]+"",i,l,o,u,d;function c(){return e[8](e[9])}return{c(){t=a("div"),i=z(n),l=m(),o=a("button"),o.textContent="Unregister",r(o,"class","btn"),r(o,"type","button"),r(t,"class","flex justify-between")},m(f,g){h(f,t,g),s(t,i),s(t,l),s(t,o),u||(d=S(o,"click",c),u=!0)},p(f,g){e=f,g&2&&n!==(n=e[9]+"")&&Z(i,n)},d(f){f&&p(t),u=!1,d()}}}function Ms(e){let t,n,i,l,o;return{c(){t=a("br"),n=m(),i=a("button"),i.textContent="Unregister all",r(i,"class","btn"),r(i,"type","button")},m(u,d){h(u,t,d),h(u,n,d),h(u,i,d),l||(o=S(i,"click",e[5]),l=!0)},p:V,d(u){u&&p(t),u&&p(n),u&&p(i),l=!1,o()}}}function Ya(e){let t,n,i,l,o,u,d,c,f,g,k,_=e[1],v=[];for(let b=0;b<_.length;b+=1)v[b]=ks(ws(e,_,b));let y=e[1].length>1&&Ms(e);return{c(){t=a("div"),n=a("input"),i=m(),l=a("button"),l.textContent="Register",o=m(),u=a("br"),d=m(),c=a("div");for(let b=0;b1?y?y.p(b,A):(y=Ms(b),y.c(),y.m(c,null)):y&&(y.d(1),y=null)},i:V,o:V,d(b){b&&p(t),b&&p(o),b&&p(u),b&&p(d),b&&p(c),yt(v,b),y&&y.d(),g=!1,oe(k)}}}function Ka(e,t,n){let i,{onMessage:l}=t;const o=Hs([]);Os(e,o,_=>n(1,i=_));let u="CmdOrControl+X";function d(){const _=u;po(_,()=>{l(`Shortcut ${_} triggered`)}).then(()=>{o.update(v=>[...v,_]),l(`Shortcut ${_} registered successfully`)}).catch(l)}function c(_){const v=_;mo(v).then(()=>{o.update(y=>y.filter(b=>b!==v)),l(`Shortcut ${v} unregistered`)}).catch(l)}function f(){ho().then(()=>{o.update(()=>[]),l("Unregistered all shortcuts")}).catch(l)}function g(){u=this.value,n(0,u)}const k=_=>c(_);return e.$$set=_=>{"onMessage"in _&&n(6,l=_.onMessage)},[u,i,o,d,c,f,l,g,k]}class Qa extends ye{constructor(t){super(),ge(this,t,Ka,Ya,pe,{onMessage:6})}}function Cs(e){let t,n,i,l,o,u,d;return{c(){t=a("br"),n=m(),i=a("input"),l=m(),o=a("button"),o.textContent="Write",r(i,"class","input"),r(i,"placeholder","write to stdin"),r(o,"class","btn")},m(c,f){h(c,t,f),h(c,n,f),h(c,i,f),q(i,e[4]),h(c,l,f),h(c,o,f),u||(d=[S(i,"input",e[14]),S(o,"click",e[8])],u=!0)},p(c,f){f&16&&i.value!==c[4]&&q(i,c[4])},d(c){c&&p(t),c&&p(n),c&&p(i),c&&p(l),c&&p(o),u=!1,oe(d)}}}function Za(e){let t,n,i,l,o,u,d,c,f,g,k,_,v,y,b,A,P,I,O,j,W,C,T,E,M=e[5]&&Cs(e);return{c(){t=a("div"),n=a("div"),i=z(`Script: - `),l=a("input"),o=m(),u=a("div"),d=z(`Encoding: - `),c=a("input"),f=m(),g=a("div"),k=z(`Working directory: - `),_=a("input"),v=m(),y=a("div"),b=z(`Arguments: - `),A=a("input"),P=m(),I=a("div"),O=a("button"),O.textContent="Run",j=m(),W=a("button"),W.textContent="Kill",C=m(),M&&M.c(),r(l,"class","grow input"),r(n,"class","flex items-center gap-1"),r(c,"class","grow input"),r(u,"class","flex items-center gap-1"),r(_,"class","grow input"),r(_,"placeholder","Working directory"),r(g,"class","flex items-center gap-1"),r(A,"class","grow input"),r(A,"placeholder","Environment variables"),r(y,"class","flex items-center gap-1"),r(O,"class","btn"),r(W,"class","btn"),r(I,"class","flex children:grow gap-1"),r(t,"class","flex flex-col childre:grow gap-1")},m(N,U){h(N,t,U),s(t,n),s(n,i),s(n,l),q(l,e[0]),s(t,o),s(t,u),s(u,d),s(u,c),q(c,e[3]),s(t,f),s(t,g),s(g,k),s(g,_),q(_,e[1]),s(t,v),s(t,y),s(y,b),s(y,A),q(A,e[2]),s(t,P),s(t,I),s(I,O),s(I,j),s(I,W),s(t,C),M&&M.m(t,null),T||(E=[S(l,"input",e[10]),S(c,"input",e[11]),S(_,"input",e[12]),S(A,"input",e[13]),S(O,"click",e[6]),S(W,"click",e[7])],T=!0)},p(N,[U]){U&1&&l.value!==N[0]&&q(l,N[0]),U&8&&c.value!==N[3]&&q(c,N[3]),U&2&&_.value!==N[1]&&q(_,N[1]),U&4&&A.value!==N[2]&&q(A,N[2]),N[5]?M?M.p(N,U):(M=Cs(N),M.c(),M.m(t,null)):M&&(M.d(1),M=null)},i:V,o:V,d(N){N&&p(t),M&&M.d(),T=!1,oe(E)}}}function xa(e,t,n){const i=navigator.userAgent.includes("Windows");let l=i?"cmd":"sh",o=i?["/C"]:["-c"],{onMessage:u}=t,d='echo "hello world"',c=null,f="SOMETHING=value ANOTHER=2",g="",k="",_;function v(){return f.split(" ").reduce((C,T)=>{let[E,M]=T.split("=");return{...C,[E]:M}},{})}function y(){n(5,_=null);const C=new Yi(l,[...o,d],{cwd:c||null,env:v(),encoding:g});C.on("close",T=>{u(`command finished with code ${T.code} and signal ${T.signal}`),n(5,_=null)}),C.on("error",T=>u(`command error: "${T}"`)),C.stdout.on("data",T=>u(`command stdout: "${T}"`)),C.stderr.on("data",T=>u(`command stderr: "${T}"`)),C.spawn().then(T=>{n(5,_=T)}).catch(u)}function b(){_.kill().then(()=>u("killed child process")).catch(u)}function A(){_.write(k).catch(u)}function P(){d=this.value,n(0,d)}function I(){g=this.value,n(3,g)}function O(){c=this.value,n(1,c)}function j(){f=this.value,n(2,f)}function W(){k=this.value,n(4,k)}return e.$$set=C=>{"onMessage"in C&&n(9,u=C.onMessage)},[d,c,f,g,k,_,y,b,A,u,P,I,O,j,W]}class $a extends ye{constructor(t){super(),ge(this,t,xa,Za,pe,{onMessage:9})}}var er={};Me(er,{checkUpdate:()=>bo,installUpdate:()=>_o,onUpdaterEvent:()=>il});async function il(e){return tn("tauri://update-status",t=>{e(t==null?void 0:t.payload)})}async function _o(){let e;function t(){e&&e(),e=void 0}return new Promise((n,i)=>{function l(o){if(o.error){t(),i(o.error);return}o.status==="DONE"&&(t(),n())}il(l).then(o=>{e=o}).catch(o=>{throw t(),o}),_i("tauri://update-install").catch(o=>{throw t(),o})})}async function bo(){let e;function t(){e&&e(),e=void 0}return new Promise((n,i)=>{function l(u){t(),n({manifest:u,shouldUpdate:!0})}function o(u){if(u.error){t(),i(u.error);return}u.status==="UPTODATE"&&(t(),n({shouldUpdate:!1}))}Vs("tauri://update-available",u=>{l(u==null?void 0:u.payload)}).catch(u=>{throw t(),u}),il(o).then(u=>{e=u}).catch(u=>{throw t(),u}),_i("tauri://update").catch(u=>{throw t(),u})})}function tr(e){let t;return{c(){t=a("button"),t.innerHTML='
',r(t,"class","btn text-accentText dark:text-darkAccentText flex items-center justify-center")},m(n,i){h(n,t,i)},p:V,d(n){n&&p(t)}}}function nr(e){let t,n,i;return{c(){t=a("button"),t.textContent="Install update",r(t,"class","btn")},m(l,o){h(l,t,o),n||(i=S(t,"click",e[4]),n=!0)},p:V,d(l){l&&p(t),n=!1,i()}}}function ir(e){let t,n,i;return{c(){t=a("button"),t.textContent="Check update",r(t,"class","btn")},m(l,o){h(l,t,o),n||(i=S(t,"click",e[3]),n=!0)},p:V,d(l){l&&p(t),n=!1,i()}}}function lr(e){let t;function n(o,u){return!o[0]&&!o[2]?ir:!o[1]&&o[2]?nr:tr}let i=n(e),l=i(e);return{c(){t=a("div"),l.c(),r(t,"class","flex children:grow children:h10")},m(o,u){h(o,t,u),l.m(t,null)},p(o,[u]){i===(i=n(o))&&l?l.p(o,u):(l.d(1),l=i(o),l&&(l.c(),l.m(t,null)))},i:V,o:V,d(o){o&&p(t),l.d()}}}function sr(e,t,n){let{onMessage:i}=t,l;_t(async()=>{l=await tn("tauri://update-status",i)}),Xi(()=>{l&&l()});let o,u,d;async function c(){n(0,o=!0);try{const{shouldUpdate:g,manifest:k}=await bo();i(`Should update: ${g}`),i(k),n(2,d=g)}catch(g){i(g)}finally{n(0,o=!1)}}async function f(){n(1,u=!0);try{await _o(),i("Installation complete, restart required."),await $i()}catch(g){i(g)}finally{n(1,u=!1)}}return e.$$set=g=>{"onMessage"in g&&n(5,i=g.onMessage)},[o,u,d,c,f,i]}class or extends ye{constructor(t){super(),ge(this,t,sr,lr,pe,{onMessage:5})}}var ar={};Me(ar,{readText:()=>yo,writeText:()=>go});async function go(e){return L({__tauriModule:"Clipboard",message:{cmd:"writeText",data:e}})}async function yo(){return L({__tauriModule:"Clipboard",message:{cmd:"readText",data:null}})}function rr(e){let t,n,i,l,o,u,d,c;return{c(){t=a("div"),n=a("input"),i=m(),l=a("button"),l.textContent="Write",o=m(),u=a("button"),u.textContent="Read",r(n,"class","grow input"),r(n,"placeholder","Text to write to the clipboard"),r(l,"class","btn"),r(l,"type","button"),r(u,"class","btn"),r(u,"type","button"),r(t,"class","flex gap-1")},m(f,g){h(f,t,g),s(t,n),q(n,e[0]),s(t,i),s(t,l),s(t,o),s(t,u),d||(c=[S(n,"input",e[4]),S(l,"click",e[1]),S(u,"click",e[2])],d=!0)},p(f,[g]){g&1&&n.value!==f[0]&&q(n,f[0])},i:V,o:V,d(f){f&&p(t),d=!1,oe(c)}}}function ur(e,t,n){let{onMessage:i}=t,l="clipboard message";function o(){go(l).then(()=>{i("Wrote to the clipboard")}).catch(i)}function u(){yo().then(c=>{i(`Clipboard contents: ${c}`)}).catch(i)}function d(){l=this.value,n(0,l)}return e.$$set=c=>{"onMessage"in c&&n(3,i=c.onMessage)},[l,o,u,i,d]}class cr extends ye{constructor(t){super(),ge(this,t,ur,rr,pe,{onMessage:3})}}function dr(e){let t;return{c(){t=a("div"),t.innerHTML=`
Not available for Linux
- `,r(t,"class","flex flex-col gap-2")},m(n,i){h(n,t,i)},p:V,i:V,o:V,d(n){n&&p(t)}}}function fr(e,t,n){let{onMessage:i}=t;const l=window.constraints={audio:!0,video:!0};function o(d){const c=document.querySelector("video"),f=d.getVideoTracks();i("Got stream with constraints:",l),i(`Using video device: ${f[0].label}`),window.stream=d,c.srcObject=d}function u(d){if(d.name==="ConstraintNotSatisfiedError"){const c=l.video;i(`The resolution ${c.width.exact}x${c.height.exact} px is not supported by your device.`)}else d.name==="PermissionDeniedError"&&i("Permissions have not been granted to use your camera and microphone, you need to allow the page access to your devices in order for the demo to work.");i(`getUserMedia error: ${d.name}`,d)}return _t(async()=>{try{const d=await navigator.mediaDevices.getUserMedia(l);o(d)}catch(d){u(d)}}),Xi(()=>{window.stream.getTracks().forEach(function(d){d.stop()})}),e.$$set=d=>{"onMessage"in d&&n(0,i=d.onMessage)},[i]}class pr extends ye{constructor(t){super(),ge(this,t,fr,dr,pe,{onMessage:0})}}function mr(e){let t,n,i,l,o,u;return{c(){t=a("div"),n=a("button"),n.textContent="Show",i=m(),l=a("button"),l.textContent="Hide",r(n,"class","btn"),r(n,"id","show"),r(n,"title","Hides and shows the app after 2 seconds"),r(l,"class","btn"),r(l,"id","hide")},m(d,c){h(d,t,c),s(t,n),s(t,i),s(t,l),o||(u=[S(n,"click",e[0]),S(l,"click",e[1])],o=!0)},p:V,i:V,o:V,d(d){d&&p(t),o=!1,oe(u)}}}function hr(e,t,n){let{onMessage:i}=t;function l(){o().then(()=>{setTimeout(()=>{no().then(()=>i("Shown app")).catch(i)},2e3)}).catch(i)}function o(){return io().then(()=>i("Hide app")).catch(i)}return e.$$set=u=>{"onMessage"in u&&n(2,i=u.onMessage)},[l,o,i]}class _r extends ye{constructor(t){super(),ge(this,t,hr,mr,pe,{onMessage:2})}}function Ts(e,t,n){const i=e.slice();return i[32]=t[n],i}function As(e,t,n){const i=e.slice();return i[35]=t[n],i}function Ls(e){let t,n,i,l,o,u,d,c,f,g,k,_,v,y,b;function A(C,T){return C[3]?gr:br}let P=A(e),I=P(e);function O(C,T){return C[2]?vr:yr}let j=O(e),W=j(e);return{c(){t=a("div"),n=a("span"),n.textContent="Tauri API Validation",i=m(),l=a("span"),o=a("span"),I.c(),d=m(),c=a("span"),c.innerHTML='
',f=m(),g=a("span"),W.c(),_=m(),v=a("span"),v.innerHTML='
',r(n,"class","lt-sm:pl-10 text-darkPrimaryText"),r(o,"title",u=e[3]?"Switch to Light mode":"Switch to Dark mode"),r(o,"class","hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"),r(c,"title","Minimize"),r(c,"class","hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"),r(g,"title",k=e[2]?"Restore":"Maximize"),r(g,"class","hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"),r(v,"title","Close"),r(v,"class","hover:bg-red-700 dark:hover:bg-red-700 hover:text-darkPrimaryText active:bg-red-700/90 dark:active:bg-red-700/90 active:text-darkPrimaryText "),r(l,"class","h-100% children:h-100% children:w-12 children:inline-flex children:items-center children:justify-center"),r(t,"class","w-screen select-none h-8 pl-2 flex justify-between items-center absolute text-primaryText dark:text-darkPrimaryText"),r(t,"data-tauri-drag-region","")},m(C,T){h(C,t,T),s(t,n),s(t,i),s(t,l),s(l,o),I.m(o,null),s(l,d),s(l,c),s(l,f),s(l,g),W.m(g,null),s(l,_),s(l,v),y||(b=[S(o,"click",e[12]),S(c,"click",e[9]),S(g,"click",e[10]),S(v,"click",e[11])],y=!0)},p(C,T){P!==(P=A(C))&&(I.d(1),I=P(C),I&&(I.c(),I.m(o,null))),T[0]&8&&u!==(u=C[3]?"Switch to Light mode":"Switch to Dark mode")&&r(o,"title",u),j!==(j=O(C))&&(W.d(1),W=j(C),W&&(W.c(),W.m(g,null))),T[0]&4&&k!==(k=C[2]?"Restore":"Maximize")&&r(g,"title",k)},d(C){C&&p(t),I.d(),W.d(),y=!1,oe(b)}}}function br(e){let t;return{c(){t=a("div"),r(t,"class","i-ph-moon")},m(n,i){h(n,t,i)},d(n){n&&p(t)}}}function gr(e){let t;return{c(){t=a("div"),r(t,"class","i-ph-sun")},m(n,i){h(n,t,i)},d(n){n&&p(t)}}}function yr(e){let t;return{c(){t=a("div"),r(t,"class","i-codicon-chrome-maximize")},m(n,i){h(n,t,i)},d(n){n&&p(t)}}}function vr(e){let t;return{c(){t=a("div"),r(t,"class","i-codicon-chrome-restore")},m(n,i){h(n,t,i)},d(n){n&&p(t)}}}function wr(e){let t;return{c(){t=a("span"),r(t,"class","i-codicon-menu animate-duration-300ms animate-fade-in")},m(n,i){h(n,t,i)},d(n){n&&p(t)}}}function kr(e){let t;return{c(){t=a("span"),r(t,"class","i-codicon-close animate-duration-300ms animate-fade-in")},m(n,i){h(n,t,i)},d(n){n&&p(t)}}}function Ss(e){let t,n,i,l,o,u,d,c,f;function g(v,y){return v[3]?Cr:Mr}let k=g(e),_=k(e);return{c(){t=a("a"),_.c(),n=m(),i=a("br"),l=m(),o=a("div"),u=m(),d=a("br"),r(t,"href","##"),r(t,"class","nv justify-between h-8"),r(o,"class","bg-white/5 h-2px")},m(v,y){h(v,t,y),_.m(t,null),h(v,n,y),h(v,i,y),h(v,l,y),h(v,o,y),h(v,u,y),h(v,d,y),c||(f=S(t,"click",e[12]),c=!0)},p(v,y){k!==(k=g(v))&&(_.d(1),_=k(v),_&&(_.c(),_.m(t,null)))},d(v){v&&p(t),_.d(),v&&p(n),v&&p(i),v&&p(l),v&&p(o),v&&p(u),v&&p(d),c=!1,f()}}}function Mr(e){let t,n;return{c(){t=z(`Switch to Dark mode - `),n=a("div"),r(n,"class","i-ph-moon")},m(i,l){h(i,t,l),h(i,n,l)},d(i){i&&p(t),i&&p(n)}}}function Cr(e){let t,n;return{c(){t=z(`Switch to Light mode - `),n=a("div"),r(n,"class","i-ph-sun")},m(i,l){h(i,t,l),h(i,n,l)},d(i){i&&p(t),i&&p(n)}}}function Tr(e){let t,n,i,l,o,u=e[35].label+"",d,c,f,g;function k(){return e[20](e[35])}return{c(){t=a("a"),n=a("div"),l=m(),o=a("p"),d=z(u),r(n,"class",i=e[35].icon+" mr-2"),r(t,"href","##"),r(t,"class",c="nv "+(e[1]===e[35]?"nv_selected":""))},m(_,v){h(_,t,v),s(t,n),s(t,l),s(t,o),s(o,d),f||(g=S(t,"click",k),f=!0)},p(_,v){e=_,v[0]&2&&c!==(c="nv "+(e[1]===e[35]?"nv_selected":""))&&r(t,"class",c)},d(_){_&&p(t),f=!1,g()}}}function zs(e){let t,n=e[35]&&Tr(e);return{c(){n&&n.c(),t=pi()},m(i,l){n&&n.m(i,l),h(i,t,l)},p(i,l){i[35]&&n.p(i,l)},d(i){n&&n.d(i),i&&p(t)}}}function Es(e){let t,n=e[32].html+"",i;return{c(){t=new Lo(!1),i=pi(),t.a=i},m(l,o){t.m(n,l,o),h(l,i,o)},p(l,o){o[0]&64&&n!==(n=l[32].html+"")&&t.p(n)},d(l){l&&p(i),l&&t.d()}}}function Ar(e){let t,n,i,l,o,u,d,c,f,g,k,_,v,y,b,A,P,I,O,j,W,C,T,E,M,N,U=e[1].label+"",J,$,me,te,Y,de,x,H,X,K,ae,ne,he,_e,fe,re,Ae,Le,D=e[5]&&Ls(e);function G(F,ce){return F[0]?kr:wr}let Se=G(e),ve=Se(e),ue=!e[5]&&Ss(e),we=e[7],ie=[];for(let F=0;F`,k=m(),_=a("a"),_.innerHTML=`GitHub - `,v=m(),y=a("a"),y.innerHTML=`Source - `,b=m(),A=a("br"),P=m(),I=a("div"),O=m(),j=a("br"),W=m(),C=a("div");for(let F=0;F',_e=m(),fe=a("div");for(let F=0;F{$t(B,1)}),hi()}ze?(Y=new ze(Je(F)),ui(Y.$$.fragment),Te(Y.$$.fragment,1),xt(Y,te,null)):Y=null}if(ce[0]&64){le=F[6];let B;for(B=0;B{await confirm("Are you sure?")||H.preventDefault()}),Ge.onFileDropEvent(H=>{P(`File drop: ${JSON.stringify(H.payload)}`)});const l=navigator.userAgent.toLowerCase(),o=l.includes("android")||l.includes("iphone"),u=[{label:"Welcome",component:ea,icon:"i-ph-hand-waving"},{label:"Communication",component:aa,icon:"i-codicon-radio-tower"},!o&&{label:"CLI",component:la,icon:"i-codicon-terminal"},!o&&{label:"Dialog",component:Ma,icon:"i-codicon-multiple-windows"},{label:"File system",component:La,icon:"i-codicon-files"},{label:"HTTP",component:Ha,icon:"i-ph-globe-hemisphere-west"},!o&&{label:"Notifications",component:Ua,icon:"i-codicon-bell-dot"},!o&&{label:"App",component:_r,icon:"i-codicon-hubot"},!o&&{label:"Window",component:Va,icon:"i-codicon-window"},!o&&{label:"Shortcuts",component:Qa,icon:"i-codicon-record-keys"},{label:"Shell",component:$a,icon:"i-codicon-terminal-bash"},!o&&{label:"Updater",component:or,icon:"i-codicon-cloud-download"},!o&&{label:"Clipboard",component:cr,icon:"i-codicon-clippy"},{label:"WebRTC",component:pr,icon:"i-ph-broadcast"}];let d=u[0];function c(H){n(1,d=H)}let f;_t(async()=>{const H=Kt();n(2,f=await H.isMaximized()),tn("tauri://resize",async()=>{n(2,f=await H.isMaximized())})});function g(){Kt().minimize()}async function k(){const H=Kt();await H.isMaximized()?H.unmaximize():H.maximize()}let _=!1;async function v(){_||(_=await ao("Are you sure that you want to close this window?",{title:"Tauri API"}),_&&Kt().close())}let y;_t(()=>{n(3,y=localStorage&&localStorage.getItem("theme")=="dark"),Ds(y)});function b(){n(3,y=!y),Ds(y)}let A=Hs([]);Os(e,A,H=>n(6,i=H));function P(H){A.update(X=>[{html:`
[${new Date().toLocaleTimeString()}]: `+(typeof H=="string"?H:JSON.stringify(H,null,1))+"
"},...X])}function I(H){A.update(X=>[{html:`
[${new Date().toLocaleTimeString()}]: `+H+"
"},...X])}function O(){A.update(()=>[])}let j,W,C;function T(H){C=H.clientY;const X=window.getComputedStyle(j);W=parseInt(X.height,10);const K=ne=>{const he=ne.clientY-C,_e=W-he;n(4,j.style.height=`${_e{document.removeEventListener("mouseup",ae),document.removeEventListener("mousemove",K)};document.addEventListener("mouseup",ae),document.addEventListener("mousemove",K)}let E;_t(async()=>{n(5,E=await xs()==="win32")});let M=!1,N,U,J=!1,$=0,me=0;const te=(H,X,K)=>Math.min(Math.max(X,H),K);_t(()=>{n(18,N=document.querySelector("#sidebar")),U=document.querySelector("#sidebarToggle"),document.addEventListener("click",H=>{U.contains(H.target)?n(0,M=!M):M&&!N.contains(H.target)&&n(0,M=!1)}),document.addEventListener("touchstart",H=>{if(U.contains(H.target))return;const X=H.touches[0].clientX;(0{if(J){const X=H.touches[0].clientX;me=X;const K=(X-$)/10;N.style.setProperty("--translate-x",`-${te(0,M?0-K:18.75-K,18.75)}rem`)}}),document.addEventListener("touchend",()=>{if(J){const H=(me-$)/10;n(0,M=M?H>-(18.75/2):H>18.75/2)}J=!1})});const Y=()=>Ki("https://tauri.app/"),de=H=>{c(H),n(0,M=!1)};function x(H){ri[H?"unshift":"push"](()=>{j=H,n(4,j)})}return e.$$.update=()=>{if(e.$$.dirty[0]&1){const H=document.querySelector("#sidebar");H&&Lr(H,M)}},[M,d,f,y,j,E,i,u,c,g,k,v,b,A,P,I,O,T,N,Y,de,x]}class zr extends ye{constructor(t){super(),ge(this,t,Sr,Ar,pe,{},null,[-1,-1])}}new zr({target:document.querySelector("#app")}); +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))i(r);new MutationObserver(r=>{for(const a of r)if(a.type==="childList")for(const m of a.addedNodes)m.tagName==="LINK"&&m.rel==="modulepreload"&&i(m)}).observe(document,{childList:!0,subtree:!0});function n(r){const a={};return r.integrity&&(a.integrity=r.integrity),r.referrerpolicy&&(a.referrerPolicy=r.referrerpolicy),r.crossorigin==="use-credentials"?a.credentials="include":r.crossorigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function i(r){if(r.ep)return;r.ep=!0;const a=n(r);fetch(r.href,a)}})();function E(){}function st(e){return e()}function Xe(){return Object.create(null)}function F(e){e.forEach(st)}function vt(e){return typeof e=="function"}function he(e,t){return e!=e?t==t:e!==t||e&&typeof e=="object"||typeof e=="function"}let ke;function bt(e,t){return ke||(ke=document.createElement("a")),ke.href=t,e===ke.href}function yt(e){return Object.keys(e).length===0}function wt(e,...t){if(e==null)return E;const n=e.subscribe(...t);return n.unsubscribe?()=>n.unsubscribe():n}function kt(e,t,n){e.$$.on_destroy.push(wt(t,n))}function s(e,t){e.appendChild(t)}function w(e,t,n){e.insertBefore(t,n||null)}function y(e){e.parentNode.removeChild(e)}function Ye(e,t){for(let n=0;ne.removeEventListener(t,n,i)}function c(e,t,n){n==null?e.removeAttribute(t):e.getAttribute(t)!==n&&e.setAttribute(t,n)}function $t(e){return Array.from(e.childNodes)}function Lt(e,t){t=""+t,e.wholeText!==t&&(e.data=t)}class xt{constructor(t=!1){this.is_svg=!1,this.is_svg=t,this.e=this.n=null}c(t){this.h(t)}m(t,n,i=null){this.e||(this.is_svg?this.e=Et(n.nodeName):this.e=d(n.nodeName),this.t=n,this.c(t)),this.i(i)}h(t){this.e.innerHTML=t,this.n=Array.from(this.e.childNodes)}i(t){for(let n=0;n{Le.delete(e),i&&(n&&e.d(1),i())}),e.o(t)}else i&&i()}function Qe(e){e&&e.c()}function Re(e,t,n,i){const{fragment:r,on_mount:a,on_destroy:m,after_update:o}=e.$$;r&&r.m(t,n),i||We(()=>{const f=a.map(st).filter(vt);m?m.push(...f):F(f),e.$$.on_mount=[]}),o.forEach(We)}function Ae(e,t){const n=e.$$;n.fragment!==null&&(F(n.on_destroy),n.fragment&&n.fragment.d(t),n.on_destroy=n.fragment=null,n.ctx=[])}function Nt(e,t){e.$$.dirty[0]===-1&&(ce.push(e),Ot(),e.$$.dirty.fill(0)),e.$$.dirty[t/31|0]|=1<{const M=O.length?O[0]:S;return u.ctx&&r(u.ctx[_],u.ctx[_]=M)&&(!u.skip_bound&&u.bound[_]&&u.bound[_](M),T&&Nt(e,_)),S}):[],u.update(),T=!0,F(u.before_update),u.fragment=i?i(u.ctx):!1,t.target){if(t.hydrate){const _=$t(t.target);u.fragment&&u.fragment.l(_),_.forEach(y)}else u.fragment&&u.fragment.c();t.intro&&Me(e.$$.fragment),Re(e,t.target,t.anchor,t.customElement),ut()}ue(f)}class Oe{$destroy(){Ae(this,1),this.$destroy=E}$on(t,n){const i=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return i.push(n),()=>{const r=i.indexOf(n);r!==-1&&i.splice(r,1)}}$set(t){this.$$set&&!yt(t)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}}const J=[];function It(e,t=E){let n;const i=new Set;function r(o){if(he(e,o)&&(e=o,n)){const f=!J.length;for(const u of i)u[1](),J.push(u,e);if(f){for(let u=0;u{i.delete(u),i.size===0&&(n(),n=null)}}return{set:r,update:a,subscribe:m}}var Wt=Object.defineProperty,dt=(e,t)=>{for(var n in t)Wt(e,n,{get:t[n],enumerable:!0})},ft=(e,t,n)=>{if(!t.has(e))throw TypeError("Cannot "+n)},Ze=(e,t,n)=>(ft(e,t,"read from private field"),n?n.call(e):t.get(e)),Mt=(e,t,n)=>{if(t.has(e))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(e):t.set(e,n)},Rt=(e,t,n,i)=>(ft(e,t,"write to private field"),i?i.call(e,n):t.set(e,n),n),At={};dt(At,{Channel:()=>ht,PluginListener:()=>mt,addPluginListener:()=>Ht,convertFileSrc:()=>jt,invoke:()=>W,transformCallback:()=>fe});function Pt(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function fe(e,t=!1){let n=Pt(),i=`_${n}`;return Object.defineProperty(window,i,{value:r=>(t&&Reflect.deleteProperty(window,i),e==null?void 0:e(r)),writable:!1,configurable:!0}),n}var ae,ht=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,Mt(this,ae,()=>{}),this.id=fe(e=>{Ze(this,ae).call(this,e)})}set onmessage(e){Rt(this,ae,e)}get onmessage(){return Ze(this,ae)}toJSON(){return`__CHANNEL__:${this.id}`}};ae=new WeakMap;var mt=class{constructor(e,t,n){this.plugin=e,this.event=t,this.channelId=n}async unregister(){return W(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function Ht(e,t,n){let i=new ht;return i.onmessage=n,W(`plugin:${e}|register_listener`,{event:t,handler:i}).then(()=>new mt(e,t,i.id))}async function W(e,t={},n){return new Promise((i,r)=>{let a=fe(o=>{i(o),Reflect.deleteProperty(window,`_${m}`)},!0),m=fe(o=>{r(o),Reflect.deleteProperty(window,`_${a}`)},!0);window.__TAURI_IPC__({cmd:e,callback:a,error:m,payload:t,options:n})})}function jt(e,t="asset"){return window.__TAURI__.convertFileSrc(e,t)}function qt(e){let t,n,i,r,a,m;return{c(){t=d("div"),n=d("p"),n.innerHTML=`This is a demo of Tauri's API capabilities using the @tauri-apps/api package. It's used as the main validation app, serving as the test bed of our + development process. In the future, this app will be used on Tauri's integration + tests.`,i=g(),r=d("button"),r.textContent="Context menu",c(r,"class","btn")},m(o,f){w(o,t,f),s(t,n),s(t,i),s(t,r),a||(m=q(r,"click",e[0]),a=!0)},p:E,i:E,o:E,d(o){o&&y(t),a=!1,m()}}}function Ut(e){function t(){W("popup_context_menu")}return[t]}class Ft extends Oe{constructor(t){super(),Se(this,t,Ut,qt,he,{})}}var zt={};dt(zt,{TauriEvent:()=>pt,emit:()=>_t,listen:()=>Pe,once:()=>Vt});var pt=(e=>(e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_CREATED="tauri://window-created",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_FILE_DROP="tauri://file-drop",e.WINDOW_FILE_DROP_HOVER="tauri://file-drop-hover",e.WINDOW_FILE_DROP_CANCELLED="tauri://file-drop-cancelled",e.MENU="tauri://menu",e))(pt||{});async function gt(e,t){await W("plugin:event|unlisten",{event:e,eventId:t})}async function Pe(e,t,n){return W("plugin:event|listen",{event:e,windowLabel:n==null?void 0:n.target,handler:fe(t)}).then(i=>async()=>gt(e,i))}async function Vt(e,t,n){return Pe(e,i=>{t(i),gt(e,i.id).catch(()=>{})},n)}async function _t(e,t,n){await W("plugin:event|emit",{event:e,windowLabel:n==null?void 0:n.target,payload:t})}function Bt(e){let t,n,i,r,a,m,o,f;return{c(){t=d("div"),n=d("button"),n.textContent="Call Log API",i=g(),r=d("button"),r.textContent="Call Request (async) API",a=g(),m=d("button"),m.textContent="Send event to Rust",c(n,"class","btn"),c(n,"id","log"),c(r,"class","btn"),c(r,"id","request"),c(m,"class","btn"),c(m,"id","event")},m(u,T){w(u,t,T),s(t,n),s(t,i),s(t,r),s(t,a),s(t,m),o||(f=[q(n,"click",e[0]),q(r,"click",e[1]),q(m,"click",e[2])],o=!0)},p:E,i:E,o:E,d(u){u&&y(t),o=!1,F(f)}}}function Gt(e,t,n){let{onMessage:i}=t,r;xe(async()=>{r=await Pe("rust-event",i)}),at(()=>{r&&r()});function a(){W("log_operation",{event:"tauri-click",payload:"this payload is optional because we used Option in Rust"})}function m(){W("perform_request",{endpoint:"dummy endpoint arg",body:{id:5,name:"test"}}).then(i).catch(i)}function o(){_t("js-event","this is the payload string")}return e.$$set=f=>{"onMessage"in f&&n(3,i=f.onMessage)},[a,m,o,i]}class Xt extends Oe{constructor(t){super(),Se(this,t,Gt,Bt,he,{onMessage:3})}}function Yt(e){let t;return{c(){t=d("div"),t.innerHTML=`
Not available for Linux
+ `,c(t,"class","flex flex-col gap-2")},m(n,i){w(n,t,i)},p:E,i:E,o:E,d(n){n&&y(t)}}}function Kt(e,t,n){let{onMessage:i}=t;const r=window.constraints={audio:!0,video:!0};function a(o){const f=document.querySelector("video"),u=o.getVideoTracks();i("Got stream with constraints:",r),i(`Using video device: ${u[0].label}`),window.stream=o,f.srcObject=o}function m(o){if(o.name==="ConstraintNotSatisfiedError"){const f=r.video;i(`The resolution ${f.width.exact}x${f.height.exact} px is not supported by your device.`)}else o.name==="PermissionDeniedError"&&i("Permissions have not been granted to use your camera and microphone, you need to allow the page access to your devices in order for the demo to work.");i(`getUserMedia error: ${o.name}`,o)}return xe(async()=>{try{const o=await navigator.mediaDevices.getUserMedia(r);a(o)}catch(o){m(o)}}),at(()=>{window.stream.getTracks().forEach(function(o){o.stop()})}),e.$$set=o=>{"onMessage"in o&&n(0,i=o.onMessage)},[i]}class Jt extends Oe{constructor(t){super(),Se(this,t,Kt,Yt,he,{onMessage:0})}}function et(e,t,n){const i=e.slice();return i[23]=t[n],i}function tt(e,t,n){const i=e.slice();return i[26]=t[n],i}function Qt(e){let t;return{c(){t=d("span"),c(t,"class","i-codicon-menu animate-duration-300ms animate-fade-in")},m(n,i){w(n,t,i)},d(n){n&&y(t)}}}function Zt(e){let t;return{c(){t=d("span"),c(t,"class","i-codicon-close animate-duration-300ms animate-fade-in")},m(n,i){w(n,t,i)},d(n){n&&y(t)}}}function en(e){let t,n;return{c(){t=Q(`Switch to Dark mode + `),n=d("div"),c(n,"class","i-ph-moon")},m(i,r){w(i,t,r),w(i,n,r)},d(i){i&&y(t),i&&y(n)}}}function tn(e){let t,n;return{c(){t=Q(`Switch to Light mode + `),n=d("div"),c(n,"class","i-ph-sun")},m(i,r){w(i,t,r),w(i,n,r)},d(i){i&&y(t),i&&y(n)}}}function nn(e){let t,n,i,r,a=e[26].label+"",m,o,f,u;function T(){return e[14](e[26])}return{c(){t=d("a"),n=d("div"),i=g(),r=d("p"),m=Q(a),c(n,"class",e[26].icon+" mr-2"),c(t,"href","##"),c(t,"class",o="nv "+(e[1]===e[26]?"nv_selected":""))},m(_,S){w(_,t,S),s(t,n),s(t,i),s(t,r),s(r,m),f||(u=q(t,"click",T),f=!0)},p(_,S){e=_,S&2&&o!==(o="nv "+(e[1]===e[26]?"nv_selected":""))&&c(t,"class",o)},d(_){_&&y(t),f=!1,u()}}}function nt(e){let t,n=e[26]&&nn(e);return{c(){n&&n.c(),t=lt()},m(i,r){n&&n.m(i,r),w(i,t,r)},p(i,r){i[26]&&n.p(i,r)},d(i){n&&n.d(i),i&&y(t)}}}function it(e){let t,n=e[23].html+"",i;return{c(){t=new xt(!1),i=lt(),t.a=i},m(r,a){t.m(n,r,a),w(r,i,a)},p(r,a){a&16&&n!==(n=r[23].html+"")&&t.p(n)},d(r){r&&y(i),r&&t.d()}}}function rn(e){let t,n,i,r,a,m,o,f,u,T,_,S,O,M,Z,R,k,C,z,D,V,ee,me,te,pe,h,b,x,ne,A,P,B,ge=e[1].label+"",Te,He,_e,ie,v,je,I,ve,qe,G,be,Ue,re,Fe,oe,se,Ce,ze;function Ve(l,N){return l[0]?Zt:Qt}let ye=Ve(e),H=ye(e);function Be(l,N){return l[2]?tn:en}let we=Be(e),j=we(e),X=e[5],$=[];for(let l=0;l`,k=g(),C=d("a"),C.innerHTML=`GitHub + `,z=g(),D=d("a"),D.innerHTML=`Source + `,V=g(),ee=d("br"),me=g(),te=d("div"),pe=g(),h=d("br"),b=g(),x=d("div");for(let l=0;l<$.length;l+=1)$[l].c();ne=g(),A=d("main"),P=d("div"),B=d("h1"),Te=Q(ge),He=g(),_e=d("div"),ie=d("div"),v&&Qe(v.$$.fragment),je=g(),I=d("div"),ve=d("div"),qe=g(),G=d("div"),be=d("p"),be.textContent="Console",Ue=g(),re=d("div"),re.innerHTML='
',Fe=g(),oe=d("div");for(let l=0;l{Ae(p,1)}),Dt()}Y?(v=new Y(Ge(l)),Qe(v.$$.fragment),Me(v.$$.fragment,1),Re(v,ie,null)):v=null}if(N&16){K=l[4];let p;for(p=0;p{h.ctrlKey&&h.key==="b"&&W("toggle_menu")});const r=[{label:"Welcome",component:Ft,icon:"i-ph-hand-waving"},{label:"Communication",component:Xt,icon:"i-codicon-radio-tower"},{label:"WebRTC",component:Jt,icon:"i-ph-broadcast"}];let a=r[0];function m(h){n(1,a=h)}let o;xe(()=>{n(2,o=localStorage&&localStorage.getItem("theme")=="dark"),ot(o)});function f(){n(2,o=!o),ot(o)}let u=It([]);kt(e,u,h=>n(4,i=h));function T(h){u.update(b=>[{html:`
[${new Date().toLocaleTimeString()}]: `+(typeof h=="string"?h:JSON.stringify(h,null,1))+"
"},...b])}function _(h){u.update(b=>[{html:`
[${new Date().toLocaleTimeString()}]: `+h+"
"},...b])}function S(){u.update(()=>[])}let O,M,Z;function R(h){Z=h.clientY;const b=window.getComputedStyle(O);M=parseInt(b.height,10);const x=A=>{const P=A.clientY-Z,B=M-P;n(3,O.style.height=`${B{document.removeEventListener("mouseup",ne),document.removeEventListener("mousemove",x)};document.addEventListener("mouseup",ne),document.addEventListener("mousemove",x)}let k=!1,C,z,D=!1,V=0,ee=0;const me=(h,b,x)=>Math.min(Math.max(b,h),x);xe(()=>{n(13,C=document.querySelector("#sidebar")),z=document.querySelector("#sidebarToggle"),document.addEventListener("click",h=>{z.contains(h.target)?n(0,k=!k):k&&!C.contains(h.target)&&n(0,k=!1)}),document.addEventListener("touchstart",h=>{if(z.contains(h.target))return;const b=h.touches[0].clientX;(0{if(D){const b=h.touches[0].clientX;ee=b;const x=(b-V)/10;C.style.setProperty("--translate-x",`-${me(0,k?0-x:18.75-x,18.75)}rem`)}}),document.addEventListener("touchend",()=>{if(D){const h=(ee-V)/10;n(0,k=k?h>-(18.75/2):h>18.75/2)}D=!1})});const te=h=>{m(h),n(0,k=!1)};function pe(h){Ne[h?"unshift":"push"](()=>{O=h,n(3,O)})}return e.$$.update=()=>{if(e.$$.dirty&1){const h=document.querySelector("#sidebar");h&&on(h,k)}},[k,a,o,O,i,r,m,f,u,T,_,S,R,C,te,pe]}class ln extends Oe{constructor(t){super(),Se(this,t,sn,rn,he,{})}}new ln({target:document.querySelector("#app")}); diff --git a/examples/api/isolation-dist/index.js b/examples/api/isolation-dist/index.js index 7e2df30de85..84fc82e5608 100644 --- a/examples/api/isolation-dist/index.js +++ b/examples/api/isolation-dist/index.js @@ -2,6 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -window.__TAURI_ISOLATION_HOOK__ = (payload) => { +window.__TAURI_ISOLATION_HOOK__ = (payload, options) => { return payload } diff --git a/examples/api/package.json b/examples/api/package.json index f39ee176d4b..4a8cf964131 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "type": "module", "scripts": { - "dev": "vite --clearScreen false --port 5173", + "dev": "vite --clearScreen false", "build": "vite build", "serve": "vite preview", "tauri": "node ../../tooling/cli/node/tauri.js" @@ -15,9 +15,10 @@ "devDependencies": { "@iconify-json/codicon": "^1.1.10", "@iconify-json/ph": "^1.1.1", - "@sveltejs/vite-plugin-svelte": "^1.0.0-next.49", + "@sveltejs/vite-plugin-svelte": "^1.0.1", + "internal-ip": "^7.0.0", "svelte": "^3.49.0", "unocss": "^0.39.3", - "vite": "^2.9.16" + "vite": "^3.2.7" } } diff --git a/examples/api/src-tauri/.gitignore b/examples/api/src-tauri/.gitignore index aba21e242c9..e5f47c4d3d3 100644 --- a/examples/api/src-tauri/.gitignore +++ b/examples/api/src-tauri/.gitignore @@ -1,3 +1,7 @@ # Generated by Cargo # will have compiled files and executables /target/ + +# cargo-mobile +.cargo/ +/gen diff --git a/examples/api/src-tauri/.taurignore b/examples/api/src-tauri/.taurignore new file mode 100644 index 00000000000..cbff5293847 --- /dev/null +++ b/examples/api/src-tauri/.taurignore @@ -0,0 +1 @@ +tauri-plugin-sample/ \ No newline at end of file diff --git a/examples/api/src-tauri/Cargo.lock b/examples/api/src-tauri/Cargo.lock index 080a409e25d..01f36e50fd9 100644 --- a/examples/api/src-tauri/Cargo.lock +++ b/examples/api/src-tauri/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -10,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aead" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c192eb8f11fc081b0fe4259ba5af04217d4e0faddd02417310a927911abd7c8" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", "generic-array", @@ -20,9 +29,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher", @@ -31,9 +40,9 @@ dependencies = [ [[package]] name = "aes-gcm" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e1366e0c69c9f927b1fa5ce2c7bf9eafc8f9268c0b9800729e8b267612447c" +checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" dependencies = [ "aead", "aes", @@ -45,9 +54,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "86b8f9420f797f2d9e935edf629310eb938a0d839f984e25327f3c7eed22300c" dependencies = [ "memchr", ] @@ -67,6 +76,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_log-sys" version = "0.2.0" @@ -75,14 +90,14 @@ checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e" [[package]] name = "android_logger" -version = "0.9.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ec2333c185d826313162cee39d3fcc6a84ba08114a839bebf53b961e7e75773" +checksum = "8619b80c242aa7bd638b5c7ddd952addeecb71f69c75e33f1d47b2804f8f883a" dependencies = [ "android_log-sys", - "env_logger 0.7.1", - "lazy_static", + "env_logger", "log", + "once_cell", ] [[package]] @@ -94,27 +109,73 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "anyhow" -version = "1.0.68" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" [[package]] name = "api" version = "0.1.0" dependencies = [ - "android_logger", - "env_logger 0.9.3", - "jni 0.19.0", "log", - "mobile-entry-point", - "paste", "serde", "serde_json", "tauri", "tauri-build", - "tauri-runtime-wry", + "tauri-plugin-cli", + "tauri-plugin-log", + "tauri-plugin-sample", "tiny_http", "window-shadows", ] @@ -137,9 +198,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", "event-listener", @@ -148,14 +209,14 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" dependencies = [ "async-lock", "async-task", "concurrent-queue", - "fastrand", + "fastrand 1.9.0", "futures-lite", "slab", ] @@ -186,30 +247,48 @@ dependencies = [ "log", "parking", "polling", - "rustix", + "rustix 0.37.23", "slab", - "socket2", + "socket2 0.4.9", "waker-fn", ] [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-process" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" dependencies = [ + "async-io", + "async-lock", + "autocfg", + "blocking", + "cfg-if", "event-listener", + "futures-lite", + "rustix 0.37.23", + "signal-hook", + "windows-sys 0.48.0", ] [[package]] name = "async-recursion" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b015a331cc64ebd1774ba119538573603427eaace0a1950c423ab971f903796" +checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.28", ] [[package]] @@ -220,55 +299,44 @@ checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" [[package]] name = "async-trait" -version = "0.1.66" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84f9ebcc6c1f5b8cb160f6990096a5c127f423fcb6e1ccc46c370cbdfb75dfc" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.28", ] [[package]] name = "atk" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +checksum = "39991bc421ddf72f70159011b323ff49b0f783cc676a7287c59453da2e2531cf" dependencies = [ "atk-sys", - "bitflags", + "bitflags 1.3.2", "glib", "libc", ] [[package]] name = "atk-sys" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +checksum = "11ad703eb64dc058024f0e57ccfa069e15a413b98dbd50a1a950e743b7f11148" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] name = "atomic-waker" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "debc29dde2e69f9e47506b525f639ed42300fc014a3e007832592448fa8e4599" - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" [[package]] name = "autocfg" @@ -277,16 +345,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] -name = "base64" -version = "0.13.1" +name = "backtrace" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] [[package]] name = "base64" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "bitflags" @@ -294,6 +371,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + [[package]] name = "block" version = "0.1.6" @@ -302,25 +385,26 @@ checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "blocking" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" +checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" dependencies = [ "async-channel", "async-lock", "async-task", "atomic-waker", - "fastrand", + "fastrand 1.9.0", "futures-lite", + "log", ] [[package]] @@ -345,25 +429,26 @@ dependencies = [ ] [[package]] -name = "bstr" -version = "0.2.17" +name = "bumpalo" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "memchr", -] +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] -name = "bumpalo" -version = "3.12.0" +name = "byte-unit" +version = "4.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "da78b32057b8fdfc352504708feeba7216dcd65a2c9ab02978cbd288d1279b6c" +dependencies = [ + "serde", + "utf8-width", +] [[package]] name = "bytemuck" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" [[package]] name = "byteorder" @@ -373,52 +458,56 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" dependencies = [ "serde", ] [[package]] name = "cairo-rs" -version = "0.15.12" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-sys-rs", "glib", "libc", + "once_cell", "thiserror", ] [[package]] name = "cairo-sys-rs" -version = "0.15.1" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421" dependencies = [ "glib-sys", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] name = "cargo_toml" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f83bc2e401ed041b7057345ebc488c005efa0341d5541ce7004d30458d0090b" +checksum = "599aa35200ffff8f04c1925aa1acc92fa2e08874379ef42e210a80e527e60838" dependencies = [ "serde", - "toml 0.7.3", + "toml", ] [[package]] name = "cc" -version = "1.0.79" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "libc", +] [[package]] name = "cesu8" @@ -439,20 +528,12 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7" -dependencies = [ - "smallvec", -] - -[[package]] -name = "cfg-expr" -version = "0.11.0" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0357a6402b295ca3a86bc148e84df46c02e41f41fef186bda662557ef6328aa" +checksum = "b40ccee03b5175c18cde8f37e7d2a33bcef6f8ec8f7cc0d81090d1bb380949c9" dependencies = [ "smallvec", + "target-lexicon", ] [[package]] @@ -463,12 +544,12 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", - "num-integer", "num-traits", "serde", "winapi", @@ -482,9 +563,9 @@ checksum = "cca491388666e04d7248af3f60f0c40cfb0991c72205595d7c396e3510207d1a" [[package]] name = "cipher" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", @@ -492,27 +573,30 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.23" +version = "4.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa" dependencies = [ - "atty", - "bitflags", + "anstream", + "anstyle", "clap_lex", - "indexmap", "strsim", - "termcolor", - "textwrap", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "cocoa" @@ -520,39 +604,45 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "block", "cocoa-foundation", "core-foundation", - "core-graphics", - "foreign-types", + "core-graphics 0.22.3", + "foreign-types 0.3.2", "libc", "objc", ] [[package]] -name = "cocoa-foundation" -version = "0.1.0" +name = "cocoa" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "block", + "cocoa-foundation", "core-foundation", - "core-graphics-types", - "foreign-types", + "core-graphics 0.23.1", + "foreign-types 0.5.0", "libc", "objc", ] [[package]] -name = "codespan-reporting" -version = "0.11.1" +name = "cocoa-foundation" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6" dependencies = [ - "termcolor", - "unicode-width", + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "foreign-types 0.3.2", + "libc", + "objc", ] [[package]] @@ -561,6 +651,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "combine" version = "4.6.6" @@ -573,9 +669,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" dependencies = [ "crossbeam-utils", ] @@ -598,9 +694,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "core-graphics" @@ -608,30 +704,42 @@ version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ - "bitflags", + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" +dependencies = [ + "bitflags 1.3.2", "core-foundation", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] [[package]] name = "core-graphics-types" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" +checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", - "foreign-types", "libc", ] [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] @@ -647,9 +755,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if", "crossbeam-utils", @@ -657,9 +765,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.14" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] @@ -689,17 +797,17 @@ dependencies = [ "proc-macro2", "quote", "smallvec", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] name = "cssparser-macros" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 1.0.107", + "syn 2.0.28", ] [[package]] @@ -709,7 +817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" dependencies = [ "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -721,61 +829,11 @@ dependencies = [ "cipher", ] -[[package]] -name = "cty" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" - -[[package]] -name = "cxx" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.15", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.15", -] - [[package]] name = "darling" -version = "0.20.1" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" dependencies = [ "darling_core", "darling_macro", @@ -783,27 +841,36 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.1" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] name = "darling_macro" -version = "0.20.1" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.15", + "syn 2.0.28", +] + +[[package]] +name = "deranged" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +dependencies = [ + "serde", ] [[package]] @@ -814,7 +881,7 @@ checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -826,29 +893,20 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version 0.4.0", - "syn 1.0.107", + "rustc_version", + "syn 1.0.109", ] [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - [[package]] name = "dirs-next" version = "2.0.0" @@ -859,17 +917,6 @@ dependencies = [ "dirs-sys-next", ] -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -889,24 +936,37 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "dtoa" -version = "0.4.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" [[package]] name = "dtoa-short" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03329ae10e79ede66c9ce4dc930aa8599043b0743008548680f25b91502d6" +checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" dependencies = [ "dtoa", ] [[package]] name = "dunce" -version = "1.0.3" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "embed-resource" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c" +checksum = "f7f1e82a60222fc67bfd50d752a9c89da5cce4c39ed39decc84a443b07bbd69a" +dependencies = [ + "cc", + "rustc_version", + "toml", + "vswhom", + "winreg 0.11.0", +] [[package]] name = "embed_plist" @@ -916,9 +976,9 @@ checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" dependencies = [ "cfg-if", ] @@ -941,41 +1001,34 @@ checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] name = "env_logger" -version = "0.7.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" dependencies = [ "log", "regex", ] [[package]] -name = "env_logger" -version = "0.9.3" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -996,47 +1049,59 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fastrand" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] -name = "field-offset" -version = "0.3.4" +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "fdeflate" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" dependencies = [ - "memoffset 0.6.5", - "rustc_version 0.3.3", + "simd-adler32", ] [[package]] -name = "filetime" -version = "0.2.19" +name = "fern" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" +checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "windows-sys 0.42.0", + "log", ] [[package]] -name = "flate2" -version = "1.0.25" +name = "field-offset" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ - "crc32fast", - "miniz_oxide", + "memoffset 0.9.0", + "rustc_version", ] [[package]] -name = "fnv" +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" @@ -1047,7 +1112,28 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", ] [[package]] @@ -1056,11 +1142,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -1077,24 +1169,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -1103,17 +1195,17 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-lite" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -1124,32 +1216,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.28", ] [[package]] name = "futures-sink" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", "futures-io", @@ -1173,11 +1265,11 @@ dependencies = [ [[package]] name = "gdk" -version = "0.15.4" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +checksum = "aa9cb33da481c6c040404a11f8212d193889e9b435db2c14fd86987f630d3ce1" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "gdk-pixbuf", "gdk-sys", @@ -1189,11 +1281,11 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.15.11" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +checksum = "c3578c60dee9d029ad86593ed88cb40f35c1b83360e12498d055022385dd9a05" dependencies = [ - "bitflags", + "bitflags 1.3.2", "gdk-pixbuf-sys", "gio", "glib", @@ -1202,22 +1294,22 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016" dependencies = [ "gio-sys", "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] name = "gdk-sys" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1227,40 +1319,54 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps 6.0.3", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4511710212ed3020b61a8622a37aa6f0dd2a84516575da92e9b96928dcbe83ba" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", ] [[package]] name = "gdkx11-sys" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b7f8c7a84b407aa9b143877e267e848ff34106578b64d1e0a24bf550716178" +checksum = "9fa2bf8b5b8c414bc5d05e48b271896d0fd3ddb57464a3108438082da61de6af" dependencies = [ "gdk-sys", "glib-sys", "libc", - "system-deps 6.0.3", + "system-deps", "x11", ] [[package]] name = "generator" -version = "0.7.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266041a359dfa931b370ef684cceb84b166beb14f7f0421f4a6a3d0c446d12e" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" dependencies = [ "cc", "libc", "log", "rustversion", - "windows 0.39.0", + "windows", ] [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1279,9 +1385,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", @@ -1298,47 +1404,58 @@ dependencies = [ "polyval", ] +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + [[package]] name = "gio" -version = "0.15.12" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092" dependencies = [ - "bitflags", + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-io", + "futures-util", "gio-sys", "glib", "libc", "once_cell", + "pin-project-lite", + "smallvec", "thiserror", ] [[package]] name = "gio-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" +checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.3", + "system-deps", "winapi", ] [[package]] name = "glib" -version = "0.15.12" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +checksum = "16aa2475c9debed5a32832cb5ff2af5a3f9e1ab9e69df58eaadc1ab2004d6eba" dependencies = [ - "bitflags", + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-executor", "futures-task", + "futures-util", + "gio-sys", "glib-macros", "glib-sys", "gobject-sys", @@ -1350,27 +1467,27 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.15.11" +version = "0.16.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" +checksum = "fb1a9325847aa46f1e96ffea37611b9d51fc4827e67f79e7de502a297560a67b" dependencies = [ "anyhow", - "heck 0.4.0", + "heck", "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] name = "glib-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" dependencies = [ "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -1379,38 +1496,25 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" -[[package]] -name = "globset" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" -dependencies = [ - "aho-corasick", - "bstr", - "fnv", - "log", - "regex", -] - [[package]] name = "gobject-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" dependencies = [ "glib-sys", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] name = "gtk" -version = "0.15.5" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +checksum = "e4d3507d43908c866c805f74c9dd593c0ce7ba5c38e576e41846639cdcd4bee6" dependencies = [ "atk", - "bitflags", + "bitflags 1.3.2", "cairo-rs", "field-offset", "futures-channel", @@ -1428,9 +1532,9 @@ dependencies = [ [[package]] name = "gtk-sys" -version = "0.15.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +checksum = "89b5f8946685d5fe44497007786600c2f368ff6b1e61a16251c89f72a97520a3" dependencies = [ "atk-sys", "cairo-sys-rs", @@ -1441,28 +1545,28 @@ dependencies = [ "gobject-sys", "libc", "pango-sys", - "system-deps 6.0.3", + "system-deps", ] [[package]] name = "gtk3-macros" -version = "0.15.4" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9" +checksum = "096eb63c6fedf03bafe65e5924595785eaf1bcb7200dac0f2cbe9c9738f05ad8" dependencies = [ "anyhow", "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] name = "h2" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" dependencies = [ "bytes", "fnv", @@ -1470,7 +1574,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -1484,43 +1588,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] -name = "heck" -version = "0.3.3" +name = "hashbrown" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" [[package]] name = "heck" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.2.6" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "hex" @@ -1530,27 +1613,27 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "html5ever" -version = "0.25.2" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" dependencies = [ "log", "mac", "markup5ever", "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] name = "http" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", - "itoa 1.0.5", + "itoa 1.0.9", ] [[package]] @@ -1578,21 +1661,15 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" - -[[package]] -name = "humantime" -version = "2.1.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.23" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -1603,60 +1680,36 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.5", + "itoa 1.0.9", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", "want", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper", - "native-tls", - "tokio", - "tokio-native-tls", -] - [[package]] name = "iana-time-zone" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows 0.48.0", + "windows", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" -dependencies = [ - "cxx", - "cxx-build", -] - -[[package]] -name = "ico" -version = "0.2.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031530fe562d8c8d71c0635013d6d155bbfe8ba0aa4b4d2d24ce8af6b71047bd" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "byteorder", - "png", + "cc", ] [[package]] @@ -1677,37 +1730,19 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", ] -[[package]] -name = "ignore" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" -dependencies = [ - "crossbeam-utils", - "globset", - "lazy_static", - "log", - "memchr", - "regex", - "same-file", - "thread_local", - "walkdir", - "winapi-util", -] - [[package]] name = "image" -version = "0.24.5" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" dependencies = [ "bytemuck", "byteorder", @@ -1718,22 +1753,24 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", "serde", ] [[package]] -name = "infer" -version = "0.9.0" +name = "indexmap" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f178e61cdbfe084aa75a2f4f7a25a5bb09701a47ae1753608f194b15783c937a" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ - "cfb", + "equivalent", + "hashbrown 0.14.0", + "serde", ] [[package]] @@ -1745,6 +1782,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "infer" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb33622da908807a06f9513c19b3c1ad50fab3e4137d82a78107d502075aa199" +dependencies = [ + "cfb", +] + [[package]] name = "inout" version = "0.1.3" @@ -1765,20 +1811,31 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.1", + "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "ipnet" -version = "2.7.1" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix 0.38.8", + "windows-sys 0.48.0", +] [[package]] name = "itoa" @@ -1788,59 +1845,47 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "javascriptcore-rs" -version = "0.16.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" +checksum = "4cfcc681b896b083864a4a3c3b3ea196f14ff66b8641a68fde209c6d84434056" dependencies = [ - "bitflags", + "bitflags 1.3.2", "glib", "javascriptcore-rs-sys", ] [[package]] name = "javascriptcore-rs-sys" -version = "0.4.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "905fbb87419c5cde6e3269537e4ea7d46431f3008c5d057e915ef3f115e7793c" +checksum = "b0983ba5b3ab9a0c0918de02c42dc71f795d6de08092f88a98ce9fdfdee4ba91" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 5.0.0", -] - -[[package]] -name = "jni" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", + "system-deps", ] [[package]] name = "jni" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", + "cfg-if", "combine", "jni-sys", "log", "thiserror", "walkdir", + "windows-sys 0.45.0", ] [[package]] @@ -1851,9 +1896,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -1871,13 +1916,25 @@ dependencies = [ ] [[package]] -name = "kuchiki" -version = "0.8.1" +name = "keyboard-types" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7668b7cff6a51fe61cdde64cd27c8a220786f399501b57ebe36f7d8112fd68" +dependencies = [ + "bitflags 1.3.2", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" dependencies = [ "cssparser", "html5ever", + "indexmap 1.9.3", "matches", "selectors", ] @@ -1890,9 +1947,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libappindicator" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2d3cb96d092b4824cb306c9e544c856a4cb6210c1081945187f7f1924b47e8" +checksum = "89e1edfdc9b0853358306c6dfb4b77c79c779174256fe93d80c0b5ebca451a2f" dependencies = [ "glib", "gtk", @@ -1903,9 +1960,9 @@ dependencies = [ [[package]] name = "libappindicator-sys" -version = "0.7.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b3b6681973cea8cc3bce7391e6d7d5502720b80a581c9a95c9cbaf592826aa" +checksum = "08fcb2bea89cee9613982501ec83eaa2d09256b24540ae463c52a28906163918" dependencies = [ "gtk-sys", "libloading", @@ -1914,9 +1971,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.139" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libloading" @@ -1938,25 +1995,22 @@ dependencies = [ ] [[package]] -name = "link-cplusplus" -version = "1.0.8" +name = "linux-raw-sys" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.3.1" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -1964,11 +2018,11 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" dependencies = [ - "cfg-if", + "value-bag", ] [[package]] @@ -1992,19 +2046,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" -[[package]] -name = "mac-notification-sys" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e72d50edb17756489e79d52eb146927bec8eba9dd48faadf9ef08bca3791ad5" -dependencies = [ - "cc", - "dirs-next", - "objc-foundation", - "objc_id", - "time", -] - [[package]] name = "malloc_buf" version = "0.0.6" @@ -2016,13 +2057,13 @@ dependencies = [ [[package]] name = "markup5ever" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" dependencies = [ "log", - "phf 0.8.0", - "phf_codegen", + "phf 0.10.1", + "phf_codegen 0.10.0", "string_cache", "string_cache_codegen", "tendril", @@ -2034,14 +2075,14 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] name = "matches" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" @@ -2051,104 +2092,79 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" -version = "0.6.5" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] name = "memoffset" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] [[package]] name = "mime" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" - -[[package]] -name = "mime_guess" -version = "2.0.4" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minisign-verify" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", + "simd-adler32", ] [[package]] name = "mio" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.42.0", -] - -[[package]] -name = "mobile-entry-point" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bef5a90018326583471cccca10424d7b3e770397b02f03276543cbb9b6a1a6" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", + "windows-sys 0.48.0", ] [[package]] -name = "native-tls" -version = "0.2.11" +name = "muda" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "47e33f46fb20f85553edb85e30a6a5231567f4103276ccdb5a2050613d253b1d" dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", + "cocoa 0.25.0", + "crossbeam-channel", + "gdk", + "gdk-pixbuf", + "gtk", + "keyboard-types", + "objc", + "once_cell", + "png", + "thiserror", + "windows-sys 0.48.0", ] [[package]] name = "ndk" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" dependencies = [ - "bitflags", + "bitflags 1.3.2", "jni-sys", "ndk-sys", "num_enum", + "raw-window-handle", "thiserror", ] @@ -2160,9 +2176,9 @@ checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "ndk-sys" -version = "0.3.0" +version = "0.4.1+23.1.7779620" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" dependencies = [ "jni-sys", ] @@ -2179,11 +2195,10 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "libc", "memoffset 0.7.1", - "pin-utils", "static_assertions", ] @@ -2193,18 +2208,6 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" -[[package]] -name = "notify-rust" -version = "4.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ce656bb6d22a93ae276a23de52d1aec5ba4db3ece3c0eb79dfd5add7384db6a" -dependencies = [ - "mac-notification-sys", - "serde", - "tauri-winrt-notification", - "zbus", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2238,42 +2241,42 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi", "libc", ] [[package]] name = "num_enum" -version = "0.5.7" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.7" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -2295,17 +2298,6 @@ dependencies = [ "objc_exception", ] -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - [[package]] name = "objc_exception" version = "0.1.2" @@ -2325,71 +2317,25 @@ dependencies = [ ] [[package]] -name = "once_cell" -version = "1.17.0" +name = "object" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "open" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" dependencies = [ - "pathdiff", - "windows-sys 0.42.0", -] - -[[package]] -name = "openssl" -version = "0.10.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518915b97df115dd36109bfa429a48b8f737bd05508cf9588977b599648926d2" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", + "memchr", ] [[package]] -name = "openssl-probe" -version = "0.1.5" +name = "once_cell" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] -name = "openssl-sys" -version = "0.9.83" +name = "opaque-debug" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666416d899cf077260dac8698d60a60b435a46d57e82acb1be3d0dad87284e5b" -dependencies = [ - "autocfg", - "cc", - "libc", - "pkg-config", - "vcpkg", -] +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "ordered-stream" @@ -2401,33 +2347,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "os_info" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5209b2162b2c140df493a93689e04f8deab3a67634f5bc7a553c0a98e5b8d399" -dependencies = [ - "log", - "serde", - "winapi", -] - -[[package]] -name = "os_pipe" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6a252f1f8c11e84b3ab59d7a488e48e4478a93937e027076638c49536204639" -dependencies = [ - "libc", - "windows-sys 0.42.0", -] - -[[package]] -name = "os_str_bytes" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" - [[package]] name = "overload" version = "0.1.1" @@ -2436,11 +2355,12 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pango" -version = "0.15.10" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94" dependencies = [ - "bitflags", + "bitflags 1.3.2", + "gio", "glib", "libc", "once_cell", @@ -2449,21 +2369,21 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] name = "parking" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" [[package]] name = "parking_lot" @@ -2477,44 +2397,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.6" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", - "windows-sys 0.42.0", + "windows-targets 0.48.1", ] -[[package]] -name = "paste" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" - -[[package]] -name = "pathdiff" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" - [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" - -[[package]] -name = "pest" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4257b4a04d91f7e9e6290be5d3da4804dd5784fafde3a497d73eb2b4a158c30a" -dependencies = [ - "thiserror", - "ucd-trie", -] +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "phf" @@ -2548,6 +2446,16 @@ dependencies = [ "phf_shared 0.8.0", ] +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -2579,7 +2487,7 @@ dependencies = [ "proc-macro-hack", "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -2593,7 +2501,7 @@ dependencies = [ "proc-macro-hack", "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -2616,9 +2524,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -2628,57 +2536,58 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "plist" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5329b8f106a176ab0dce4aae5da86bfcb139bb74fb00882859e03745011f3635" +checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06" dependencies = [ - "base64 0.13.1", - "indexmap", + "base64", + "indexmap 1.9.3", "line-wrap", - "quick-xml 0.26.0", + "quick-xml", "serde", "time", ] [[package]] name = "png" -version = "0.17.7" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", + "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "polling" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e1f879b2998099c2d69ab9605d145d5b661195627eccc680002c4918a7fb6fa" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", - "bitflags", + "bitflags 1.3.2", "cfg-if", "concurrent-queue", "libc", "log", "pin-project-lite", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "polyval" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" dependencies = [ "cfg-if", "cpufeatures", @@ -2700,13 +2609,12 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro-crate" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "thiserror", - "toml 0.5.10", + "toml_edit", ] [[package]] @@ -2718,7 +2626,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", "version_check", ] @@ -2741,36 +2649,27 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] @@ -2835,7 +2734,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.10", ] [[package]] @@ -2858,12 +2757,9 @@ dependencies = [ [[package]] name = "raw-window-handle" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a" -dependencies = [ - "cty", -] +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" [[package]] name = "redox_syscall" @@ -2871,7 +2767,16 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", ] [[package]] @@ -2880,20 +2785,21 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.8", - "redox_syscall", + "getrandom 0.2.10", + "redox_syscall 0.2.16", "thiserror", ] [[package]] name = "regex" -version = "1.7.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-automata 0.3.6", + "regex-syntax 0.7.4", ] [[package]] @@ -2902,31 +2808,39 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ - "regex-syntax", + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.4", ] [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] -name = "remove_dir_all" -version = "0.5.3" +name = "regex-syntax" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "reqwest" -version = "0.11.13" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ - "base64 0.13.1", + "base64", "bytes", "encoding_rs", "futures-core", @@ -2935,13 +2849,10 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-tls", "ipnet", "js-sys", "log", "mime", - "mime_guess", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -2949,83 +2860,69 @@ dependencies = [ "serde_json", "serde_urlencoded", "tokio", - "tokio-native-tls", "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", - "winreg", + "winreg 0.10.1", ] [[package]] -name = "rfd" -version = "0.10.0" +name = "rustc-demangle" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" -dependencies = [ - "block", - "dispatch", - "glib-sys", - "gobject-sys", - "gtk-sys", - "js-sys", - "lazy_static", - "log", - "objc", - "objc-foundation", - "objc_id", - "raw-window-handle", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows 0.37.0", -] +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc_version" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 0.11.0", + "semver", ] [[package]] -name = "rustc_version" -version = "0.4.0" +name = "rustix" +version = "0.37.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" dependencies = [ - "semver 1.0.16", + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", ] [[package]] name = "rustix" -version = "0.37.3" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b24138615de35e32031d041a09032ef3487a616d901ca4db224e7d557efae2" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags", + "bitflags 2.4.0", "errno", - "io-lifetimes", "libc", - "linux-raw-sys", - "windows-sys 0.45.0", + "linux-raw-sys 0.4.5", + "windows-sys 0.48.0", ] [[package]] name = "rustversion" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.12" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "safemem" @@ -3042,15 +2939,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" -dependencies = [ - "windows-sys 0.42.0", -] - [[package]] name = "scoped-tls" version = "1.0.1" @@ -3059,38 +2947,9 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" - -[[package]] -name = "security-framework" -version = "2.7.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" -dependencies = [ - "core-foundation-sys", - "libc", -] +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "selectors" @@ -3098,14 +2957,14 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cssparser", "derive_more", "fxhash", "log", "matches", "phf 0.8.0", - "phf_codegen", + "phf_codegen 0.8.0", "precomputed-hash", "servo_arc", "smallvec", @@ -3114,78 +2973,57 @@ dependencies = [ [[package]] name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" -dependencies = [ - "serde", -] - -[[package]] -name = "semver-parser" -version = "0.10.2" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.160" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" dependencies = [ - "itoa 1.0.5", + "itoa 1.0.9", "ryu", "serde", ] [[package]] name = "serde_repr" -version = "0.1.10" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" +checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.28", ] [[package]] name = "serde_spanned" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ "serde", ] @@ -3197,21 +3035,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.5", + "itoa 1.0.9", "ryu", "serde", ] [[package]] name = "serde_with" -version = "3.0.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513" +checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49" dependencies = [ - "base64 0.21.0", + "base64", "chrono", "hex", - "indexmap", + "indexmap 1.9.3", + "indexmap 2.0.0", "serde", "serde_json", "serde_with_macros", @@ -3220,14 +3059,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.0.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070" +checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -3249,7 +3088,7 @@ checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -3275,9 +3114,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", @@ -3294,15 +3133,30 @@ dependencies = [ ] [[package]] -name = "shared_child" -version = "1.0.0" +name = "signal-hook" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", - "winapi", + "signal-hook-registry", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "siphasher" version = "0.3.10" @@ -3311,55 +3165,65 @@ checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" [[package]] name = "slab" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", ] [[package]] -name = "soup2" -version = "0.2.1" +name = "socket2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" dependencies = [ - "bitflags", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "soup3" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82bc46048125fefd69d30b32b9d263d6556c9ffe82a7a7df181a86d912da5616" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", "gio", "glib", "libc", "once_cell", - "soup2-sys", + "soup3-sys", ] [[package]] -name = "soup2-sys" -version = "0.2.0" +name = "soup3-sys" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" +checksum = "014bbeb1c4cdb30739dc181e8d98b7908f124d9555843afa89b5570aaf4ec62b" dependencies = [ - "bitflags", "gio-sys", "glib-sys", "gobject-sys", "libc", - "system-deps 5.0.0", + "system-deps", ] [[package]] @@ -3370,9 +3234,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "state" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" +checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" dependencies = [ "loom", ] @@ -3385,9 +3249,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "string_cache" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" dependencies = [ "new_debug_unreachable", "once_cell", @@ -3416,37 +3280,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] -name = "strum" -version = "0.22.0" +name = "subtle" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7ac893c7d471c8a21f31cfe213ec4f6d9afeed25537c772e08ef3f005f8729e" -dependencies = [ - "strum_macros", -] +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] -name = "strum_macros" -version = "0.22.0" +name = "swift-rs" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339f799d8b549e3744c7ac7feb216383e4005d94bdb22561b3ab8f3b808ae9fb" +checksum = "1bbdb58577b6301f8d17ae2561f32002a5bae056d444e0f69e611e504a276204" dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "syn 1.0.107", + "base64", + "serde", + "serde_json", ] -[[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - [[package]] name = "syn" -version = "1.0.107" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -3455,72 +3309,46 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "sys-locale" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a11bd9c338fdba09f7881ab41551932ad42e405f61d01e8406baea71c07aee" -dependencies = [ - "js-sys", - "libc", - "wasm-bindgen", - "web-sys", - "windows-sys 0.45.0", -] - [[package]] name = "system-deps" -version = "5.0.0" +version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e" +checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3" dependencies = [ - "cfg-expr 0.9.1", - "heck 0.3.3", + "cfg-expr", + "heck", "pkg-config", - "toml 0.5.10", - "version-compare 0.0.11", -] - -[[package]] -name = "system-deps" -version = "6.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff" -dependencies = [ - "cfg-expr 0.11.0", - "heck 0.4.0", - "pkg-config", - "toml 0.5.10", - "version-compare 0.1.1", + "toml", + "version-compare", ] [[package]] name = "tao" -version = "0.16.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704522803dda895767f69198af8351b0a3f4fe2e293c3ca54cce0ecba05a97f2" +checksum = "60279ecb16c33a6cef45cd37a9602455c190942d20e360bd8499bff49f2a48f3" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "cc", - "cocoa", + "cocoa 0.24.1", "core-foundation", - "core-graphics", + "core-graphics 0.22.3", "crossbeam-channel", - "dirs-next", "dispatch", "gdk", "gdk-pixbuf", "gdk-sys", + "gdkwayland-sys", "gdkx11-sys", "gio", "glib", @@ -3528,9 +3356,8 @@ dependencies = [ "gtk", "image", "instant", - "jni 0.20.0", + "jni", "lazy_static", - "libappindicator", "libc", "log", "ndk", @@ -3545,156 +3372,189 @@ dependencies = [ "serde", "tao-macros", "unicode-segmentation", + "url", "uuid", - "windows 0.39.0", + "windows", "windows-implement", "x11-dl", + "zbus", ] [[package]] name = "tao-macros" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b6fcd8245d45a39ffc8715183d92ae242750eb57b285eb3bcd63dfd512afd09" +checksum = "ec114582505d158b669b136e6851f85840c109819d77c42bb7c0709f727d18c2" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] -name = "tar" -version = "0.4.38" +name = "target-lexicon" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" -dependencies = [ - "filetime", - "libc", - "xattr", -] +checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" [[package]] name = "tauri" -version = "1.3.0" +version = "2.0.0-alpha.10" dependencies = [ "anyhow", - "base64 0.21.0", "bytes", - "clap", - "cocoa", + "cocoa 0.25.0", "dirs-next", "embed_plist", - "encoding_rs", - "flate2", "futures-util", "glib", "glob", "gtk", - "heck 0.4.0", + "heck", "http", - "ico 0.2.0", - "ignore", - "infer 0.9.0", - "minisign-verify", - "notify-rust", + "ico", + "infer 0.15.0", + "jni", + "libc", + "log", + "mime", + "muda", "objc", "once_cell", - "open", - "os_info", - "os_pipe", "percent-encoding", "png", "rand 0.8.5", "raw-window-handle", - "regex", "reqwest", - "rfd", - "semver 1.0.16", "serde", "serde_json", "serde_repr", "serialize-to-javascript", - "shared_child", "state", - "sys-locale", - "tar", + "swift-rs", + "tauri-build", "tauri-macros", "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "tempfile", "thiserror", - "time", "tokio", + "tray-icon", "url", "uuid", "webkit2gtk", "webview2-com", - "win7-notifications", - "windows 0.39.0", - "zip", + "windows", ] [[package]] name = "tauri-build" -version = "1.3.0" +version = "2.0.0-alpha.6" dependencies = [ "anyhow", "cargo_toml", - "heck 0.4.0", + "heck", "json-patch", + "plist", "quote", - "semver 1.0.16", + "semver", "serde", "serde_json", + "swift-rs", "tauri-codegen", "tauri-utils", "tauri-winres", + "walkdir", ] [[package]] name = "tauri-codegen" -version = "1.3.0" +version = "2.0.0-alpha.6" dependencies = [ - "base64 0.21.0", + "base64", "brotli", - "ico 0.3.0", + "ico", "json-patch", "plist", "png", "proc-macro2", "quote", - "regex", - "semver 1.0.16", + "semver", "serde", "serde_json", "sha2", "tauri-utils", "thiserror", "time", + "url", "uuid", "walkdir", ] [[package]] name = "tauri-macros" -version = "1.3.0" +version = "2.0.0-alpha.6" dependencies = [ - "heck 0.4.0", + "heck", "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", "tauri-codegen", "tauri-utils", ] +[[package]] +name = "tauri-plugin-cli" +version = "0.0.0" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=next#4a10f218f0e1fdd66a549dc0bf16be3efb17ea49" +dependencies = [ + "clap", + "log", + "serde", + "serde_json", + "tauri", + "thiserror", +] + +[[package]] +name = "tauri-plugin-log" +version = "0.0.0" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=next#4a10f218f0e1fdd66a549dc0bf16be3efb17ea49" +dependencies = [ + "android_logger", + "byte-unit", + "cocoa 0.24.1", + "fern", + "log", + "objc", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-build", + "time", +] + +[[package]] +name = "tauri-plugin-sample" +version = "0.1.0" +dependencies = [ + "log", + "serde", + "tauri", + "tauri-build", + "thiserror", +] + [[package]] name = "tauri-runtime" -version = "0.13.0" +version = "0.13.0-alpha.6" dependencies = [ "gtk", "http", "http-range", + "jni", "rand 0.8.5", "raw-window-handle", "serde", @@ -3703,16 +3563,16 @@ dependencies = [ "thiserror", "url", "uuid", - "webview2-com", - "windows 0.39.0", + "windows", ] [[package]] name = "tauri-runtime-wry" -version = "0.13.0" +version = "0.13.0-alpha.6" dependencies = [ - "cocoa", + "cocoa 0.24.1", "gtk", + "jni", "percent-encoding", "rand 0.8.5", "raw-window-handle", @@ -3721,30 +3581,30 @@ dependencies = [ "uuid", "webkit2gtk", "webview2-com", - "windows 0.39.0", + "windows", "wry", ] [[package]] name = "tauri-utils" -version = "1.3.0" +version = "2.0.0-alpha.6" dependencies = [ "aes-gcm", "brotli", "ctor", "dunce", - "getrandom 0.2.8", + "getrandom 0.2.10", "glob", - "heck 0.4.0", + "heck", "html5ever", "infer 0.12.0", "json-patch", - "kuchiki", + "kuchikiki", "memchr", "phf 0.10.1", "proc-macro2", "quote", - "semver 1.0.16", + "semver", "serde", "serde_json", "serde_with", @@ -3752,42 +3612,30 @@ dependencies = [ "thiserror", "url", "walkdir", - "windows 0.39.0", + "windows", ] [[package]] name = "tauri-winres" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b7a78dc04f75fb5ab815e66ac561c81e92a968a40f29e7c21afd152d694fad8" -dependencies = [ - "toml 0.5.10", - "version_check", -] - -[[package]] -name = "tauri-winrt-notification" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58de036c4d2e20717024de2a3c4bf56c301f07b21bc8ef9b57189fce06f1f3b" +checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb" dependencies = [ - "quick-xml 0.23.1", - "strum", - "windows 0.39.0", + "embed-resource", + "toml", ] [[package]] name = "tempfile" -version = "3.3.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" dependencies = [ "cfg-if", - "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", + "fastrand 2.0.0", + "redox_syscall 0.3.5", + "rustix 0.38.8", + "windows-sys 0.48.0", ] [[package]] @@ -3801,21 +3649,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "thin-slice" version = "0.1.1" @@ -3824,51 +3657,63 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] name = "thread_local" -version = "1.1.4" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ + "cfg-if", "once_cell", ] [[package]] name = "time" -version = "0.3.15" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" dependencies = [ - "itoa 1.0.5", + "deranged", + "itoa 1.0.9", "libc", "num_threads", "serde", + "time-core", "time-macros", ] +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + [[package]] name = "time-macros" -version = "0.2.4" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +dependencies = [ + "time-core", +] [[package]] name = "tiny_http" @@ -3894,42 +3739,31 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.24.2" +version = "1.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" +checksum = "40de3a2ba249dcb097e01be5e67a5ff53cf250397715a071a81543e8a832a920" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", - "memchr", "mio", "num_cpus", "pin-project-lite", - "socket2", - "windows-sys 0.42.0", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" -dependencies = [ - "native-tls", - "tokio", + "socket2 0.5.3", + "windows-sys 0.48.0", ] [[package]] name = "tokio-util" -version = "0.7.4" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", @@ -3941,18 +3775,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" -dependencies = [ - "serde", -] - -[[package]] -name = "toml" -version = "0.7.3" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" dependencies = [ "serde", "serde_spanned", @@ -3962,20 +3787,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.8" +version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" dependencies = [ - "indexmap", + "indexmap 2.0.0", "serde", "serde_spanned", "toml_datetime", @@ -4002,20 +3827,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.28", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", "valuable", @@ -4034,9 +3859,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ "matchers", "nu-ansi-term", @@ -4050,6 +3875,25 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tray-icon" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b0e5bec13da15e62330e9bcf8b9fd42489b5acfe29ac8fec7ed659dbee21d9" +dependencies = [ + "cocoa 0.25.0", + "core-graphics 0.23.1", + "crossbeam-channel", + "dirs-next", + "libappindicator", + "muda", + "objc", + "once_cell", + "png", + "thiserror", + "windows-sys 0.48.0", +] + [[package]] name = "treediff" version = "4.0.2" @@ -4071,12 +3915,6 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" -[[package]] -name = "ucd-trie" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" - [[package]] name = "uds_windows" version = "1.0.2" @@ -4087,26 +3925,17 @@ dependencies = [ "winapi", ] -[[package]] -name = "unicase" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -dependencies = [ - "version_check", -] - [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.6" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "unicode-normalization" @@ -4119,21 +3948,15 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" - -[[package]] -name = "unicode-width" -version = "0.1.10" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "universal-hash" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", "subtle", @@ -4141,9 +3964,9 @@ dependencies = [ [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", "idna", @@ -4157,13 +3980,25 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-width" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" -version = "1.2.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.10", ] [[package]] @@ -4173,16 +4008,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version-compare" -version = "0.0.11" +name = "value-bag" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" [[package]] name = "version-compare" @@ -4196,6 +4025,26 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "waker-fn" version = "1.1.0" @@ -4204,22 +4053,20 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "walkdir" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" dependencies = [ "same-file", - "winapi", "winapi-util", ] [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -4237,9 +4084,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -4247,24 +4094,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.28", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.33" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if", "js-sys", @@ -4274,9 +4121,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4284,28 +4131,41 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.28", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "wasm-streams" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] [[package]] name = "web-sys" -version = "0.3.60" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -4313,11 +4173,11 @@ dependencies = [ [[package]] name = "webkit2gtk" -version = "0.18.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" +checksum = "3ba4cce9085e0fb02575cfd45c328740dde78253cba516b1e8be2ca0f57bd8bf" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "gdk", "gdk-sys", @@ -4331,20 +4191,18 @@ dependencies = [ "javascriptcore-rs", "libc", "once_cell", - "soup2", + "soup3", "webkit2gtk-sys", ] [[package]] name = "webkit2gtk-sys" -version = "0.18.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" +checksum = "f4489eb24e8cf0a3d0555fd3a8f7adec2a5ece34c1e7b7c9a62da7822fd40a59" dependencies = [ - "atk-sys", - "bitflags", + "bitflags 1.3.2", "cairo-sys-rs", - "gdk-pixbuf-sys", "gdk-sys", "gio-sys", "glib-sys", @@ -4352,60 +4210,50 @@ dependencies = [ "gtk-sys", "javascriptcore-rs-sys", "libc", - "pango-sys", "pkg-config", - "soup2-sys", - "system-deps 6.0.3", + "soup3-sys", + "system-deps", ] [[package]] name = "webview2-com" -version = "0.19.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178" +checksum = "79e563ffe8e84d42e43ffacbace8780c0244fc8910346f334613559d92e203ad" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows 0.39.0", + "windows", "windows-implement", + "windows-interface", ] [[package]] name = "webview2-com-macros" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaebe196c01691db62e9e4ca52c5ef1e4fd837dcae27dae3ada599b5a8fd05ac" +checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.28", ] [[package]] name = "webview2-com-sys" -version = "0.19.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7" +checksum = "19d39576804304cf9ead192467ef47f7859a1a12fec3bd459d5ba34b8cd65ed5" dependencies = [ "regex", "serde", "serde_json", "thiserror", - "windows 0.39.0", + "windows", "windows-bindgen", "windows-metadata", ] -[[package]] -name = "win7-notifications" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "210952d7163b9ed83a6fd9754ab2a101d14480f8491b5f1d6292771d88dbee70" -dependencies = [ - "once_cell", - "windows-sys 0.36.1", -] - [[package]] name = "winapi" version = "0.3.9" @@ -4443,53 +4291,28 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29d30320647cfc3dc45554c8ad825b84831def81f967a2f7589931328ff9b16d" dependencies = [ - "cocoa", + "cocoa 0.24.1", "objc", "raw-window-handle", "windows-sys 0.42.0", ] -[[package]] -name = "windows" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" -dependencies = [ - "windows_aarch64_msvc 0.37.0", - "windows_i686_gnu 0.37.0", - "windows_i686_msvc 0.37.0", - "windows_x86_64_gnu 0.37.0", - "windows_x86_64_msvc 0.37.0", -] - -[[package]] -name = "windows" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" -dependencies = [ - "windows-implement", - "windows_aarch64_msvc 0.39.0", - "windows_i686_gnu 0.39.0", - "windows_i686_msvc 0.39.0", - "windows_x86_64_gnu 0.39.0", - "windows_x86_64_msvc 0.39.0", -] - [[package]] name = "windows" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.0", + "windows-implement", + "windows-interface", + "windows-targets 0.48.1", ] [[package]] name = "windows-bindgen" -version = "0.39.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68003dbd0e38abc0fb85b939240f4bce37c43a5981d3df37ccbaaa981b47cb41" +checksum = "1fe21a77bc54b7312dbd66f041605e098990c98be48cd52967b85b5e60e75ae6" dependencies = [ "windows-metadata", "windows-tokens", @@ -4497,32 +4320,31 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.39.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7" +checksum = "5e2ee588991b9e7e6c8338edf3333fbe4da35dc72092643958ebb43f0ab2c49c" dependencies = [ - "syn 1.0.107", - "windows-tokens", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] -name = "windows-metadata" -version = "0.39.0" +name = "windows-interface" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" +checksum = "e6fb8df20c9bcaa8ad6ab513f7b40104840c8867d5751126e4df3b08388d0cc7" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] [[package]] -name = "windows-sys" -version = "0.36.1" +name = "windows-metadata" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", -] +checksum = "422ee0e5f0e2cc372bb6addbfff9a8add712155cd743df9c15f6ab000f31432d" [[package]] name = "windows-sys" @@ -4530,13 +4352,13 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm 0.42.1", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm 0.42.1", - "windows_x86_64_msvc 0.42.1", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -4545,7 +4367,7 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets 0.42.1", + "windows-targets 0.42.2", ] [[package]] @@ -4554,29 +4376,29 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.1", ] [[package]] name = "windows-targets" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm 0.42.1", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm 0.42.1", - "windows_x86_64_msvc 0.42.1", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", @@ -4589,15 +4411,15 @@ dependencies = [ [[package]] name = "windows-tokens" -version = "0.39.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" +checksum = "b34c9a3b28cb41db7385546f7f9a8179348dffc89923dde66857b1ba5312f6b4" [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" @@ -4607,27 +4429,9 @@ checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" [[package]] name = "windows_aarch64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" @@ -4637,27 +4441,9 @@ checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" - -[[package]] -name = "windows_i686_gnu" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" - -[[package]] -name = "windows_i686_gnu" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" @@ -4667,27 +4453,9 @@ checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" - -[[package]] -name = "windows_i686_msvc" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" - -[[package]] -name = "windows_i686_msvc" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" @@ -4697,27 +4465,9 @@ checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" @@ -4727,9 +4477,9 @@ checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" @@ -4739,27 +4489,9 @@ checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" @@ -4769,9 +4501,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.1" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +checksum = "5504cc7644f4b593cbc05c4a55bf9bd4e94b867c3c0bd440934174d50482427d" dependencies = [ "memchr", ] @@ -4785,16 +4517,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a1a57ff50e9b408431e8f97d5456f2807f8eb2a2cd79b06068fc87f8ecf189" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "wry" -version = "0.24.1" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c846dc4dda988e959869dd0802cd27417c9696e584593e49178aeee28890d25" +checksum = "4c6289018fa3cbc051c13f4ae1a102d80c3f35a527456c75567eb2cad6989020" dependencies = [ - "base64 0.13.1", + "base64", "block", - "cocoa", - "core-graphics", + "cocoa 0.24.1", + "core-graphics 0.22.3", "crossbeam-channel", "dunce", "gdk", @@ -4803,7 +4545,8 @@ dependencies = [ "gtk", "html5ever", "http", - "kuchiki", + "javascriptcore-rs", + "kuchikiki", "libc", "log", "objc", @@ -4812,22 +4555,22 @@ dependencies = [ "serde", "serde_json", "sha2", - "soup2", + "soup3", "tao", "thiserror", "url", "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows 0.39.0", + "windows", "windows-implement", ] [[package]] name = "x11" -version = "2.20.1" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2638d5b9c17ac40575fb54bb461a4b1d2a8d1b4ffcc4ff237d254ec59ddeb82" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" dependencies = [ "libc", "pkg-config", @@ -4835,41 +4578,43 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.20.1" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1536d6965a5d4e573c7ef73a2c15ebcd0b2de3347bdf526c34c297c00ac40f0" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" dependencies = [ - "lazy_static", "libc", + "once_cell", "pkg-config", ] [[package]] -name = "xattr" -version = "0.2.3" +name = "xdg-home" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" dependencies = [ - "libc", + "nix", + "winapi", ] [[package]] name = "zbus" -version = "3.11.1" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dc29e76f558b2cb94190e8605ecfe77dd40f5df8c072951714b4b71a97f5848" +checksum = "31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948" dependencies = [ "async-broadcast", "async-executor", "async-fs", "async-io", "async-lock", + "async-process", "async-recursion", "async-task", "async-trait", + "blocking", "byteorder", "derivative", - "dirs", "enumflags2", "event-listener", "futures-core", @@ -4887,6 +4632,7 @@ dependencies = [ "tracing", "uds_windows", "winapi", + "xdg-home", "zbus_macros", "zbus_names", "zvariant", @@ -4894,45 +4640,34 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "3.11.1" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62a80fd82c011cd08459eaaf1fd83d3090c1b61e6d5284360074a7475af3a85d" +checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "regex", - "syn 1.0.107", + "syn 1.0.109", "zvariant_utils", ] [[package]] name = "zbus_names" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3" +checksum = "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9" dependencies = [ "serde", "static_assertions", "zvariant", ] -[[package]] -name = "zip" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537ce7411d25e54e8ae21a7ce0b15840e7bfcff15b51d697ec3266cc76bdf080" -dependencies = [ - "byteorder", - "crc32fast", - "crossbeam-utils", -] - [[package]] name = "zvariant" -version = "3.12.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe4914a985446d6fd287019b5fceccce38303d71407d9e6e711d44954a05d8" +checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" dependencies = [ "byteorder", "enumflags2", @@ -4944,24 +4679,24 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "3.12.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34c20260af4b28b3275d6676c7e2a6be0d4332e8e0aba4616d34007fd84e462a" +checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b22993dbc4d128a17a3b6c92f1c63872dd67198537ee728d8b5d7c40640a8b" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index 3de7ea83062..a70dda3e237 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -3,11 +3,12 @@ name = "api" version = "0.1.0" description = "An example Tauri Application showcasing the api" edition = "2021" -rust-version = "1.60" +rust-version = "1.65" license = "Apache-2.0 OR MIT" [lib] name = "api_lib" +crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { path = "../../../core/tauri-build", features = ["codegen", "isolation"] } @@ -16,21 +17,30 @@ tauri-build = { path = "../../../core/tauri-build", features = ["codegen", "isol serde_json = "1.0" serde = { version = "1.0", features = [ "derive" ] } tiny_http = "0.11" +log = "0.4" +tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "next" } +tauri-plugin-sample = { path = "./tauri-plugin-sample/" } + +[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] +tauri-plugin-cli = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "next" } + +[patch.crates-io] +tauri = { path = "../../../core/tauri" } +tauri-build = { path = "../../../core/tauri-build" } + +[patch.'https://github.com/tauri-apps/tauri'] +tauri = { path = "../../../core/tauri" } +tauri-build = { path = "../../../core/tauri-build" } [dependencies.tauri] path = "../../../core/tauri" features = [ - "api-all", - "cli", - "global-shortcut", - "http-multipart", + "protocol-asset", "icon-ico", "icon-png", "isolation", "macos-private-api", - "windows7-compat", - "system-tray", - "updater" + "tray-icon" ] [dev-dependencies.tauri] @@ -40,19 +50,6 @@ features = ["test"] [target."cfg(target_os = \"windows\")".dependencies] window-shadows= "0.2" -[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] -log = "0.4" -tauri-runtime-wry = { path = "../../../core/tauri-runtime-wry/" } - -[target.'cfg(target_os = "android")'.dependencies] -android_logger = "0.9.0" -jni = "0.19.0" -paste = "1.0" - -[target.'cfg(target_os = "ios")'.dependencies] -mobile-entry-point = "0.1.0" -env_logger = "0.9.0" - [features] custom-protocol = [ "tauri/custom-protocol" ] diff --git a/examples/api/src-tauri/build.rs b/examples/api/src-tauri/build.rs index b8c24f76e18..2154ff35f5e 100644 --- a/examples/api/src-tauri/build.rs +++ b/examples/api/src-tauri/build.rs @@ -8,5 +8,5 @@ fn main() { codegen = codegen.dev(); } codegen.build(); - tauri_build::build() + tauri_build::build(); } diff --git a/examples/api/src-tauri/src/cmd.rs b/examples/api/src-tauri/src/cmd.rs index 791a621385b..2c92decf772 100644 --- a/examples/api/src-tauri/src/cmd.rs +++ b/examples/api/src-tauri/src/cmd.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use tauri::command; #[derive(Debug, Deserialize)] @@ -14,11 +14,52 @@ pub struct RequestBody { #[command] pub fn log_operation(event: String, payload: Option) { - println!("{} {:?}", event, payload); + log::info!("{} {:?}", event, payload); +} + +#[derive(Serialize)] +pub struct ApiResponse { + message: String, } #[command] -pub fn perform_request(endpoint: String, body: RequestBody) -> String { +pub fn perform_request(endpoint: String, body: RequestBody) -> ApiResponse { println!("{} {:?}", endpoint, body); - "message response".into() + ApiResponse { + message: "message response".into(), + } +} + +#[cfg(all(desktop, not(target_os = "macos")))] +#[command] +pub fn toggle_menu(window: tauri::Window) { + if window.is_menu_visible().unwrap_or_default() { + let _ = window.hide_menu(); + } else { + let _ = window.show_menu(); + } +} + +#[cfg(target_os = "macos")] +#[command] +pub fn toggle_menu( + app: tauri::AppHandle, + app_menu: tauri::State<'_, crate::AppMenu>, +) { + if let Some(menu) = app.remove_menu().unwrap() { + app_menu.0.lock().unwrap().replace(menu); + } else { + app + .set_menu(app_menu.0.lock().unwrap().clone().expect("no app menu")) + .unwrap(); + } +} + +#[cfg(desktop)] +#[command] +pub fn popup_context_menu( + window: tauri::Window, + popup_menu: tauri::State<'_, crate::PopupMenu>, +) { + window.popup_menu(&popup_menu.0).unwrap(); } diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index 4be07f36be2..8af633098e9 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -2,20 +2,36 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + mod cmd; +#[cfg(desktop)] mod tray; use serde::Serialize; -use tauri::{ - api::dialog::{ask, message}, - App, GlobalShortcutManager, Manager, RunEvent, Runtime, WindowBuilder, WindowEvent, WindowUrl, -}; +use tauri::{ipc::Channel, window::WindowBuilder, App, AppHandle, RunEvent, Runtime, WindowUrl}; +use tauri_plugin_sample::{PingRequest, SampleExt}; + +#[cfg(desktop)] +use tauri::Manager; + +pub type SetupHook = Box Result<(), Box> + Send>; +pub type OnEvent = Box; #[derive(Clone, Serialize)] struct Reply { data: String, } +#[cfg(target_os = "macos")] +pub struct AppMenu(pub std::sync::Mutex>>); + +#[cfg(desktop)] +pub struct PopupMenu(tauri::menu::Menu); + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { run_app(tauri::Builder::default(), |_app| {}) @@ -27,32 +43,62 @@ pub fn run_app) + Send + 'static>( ) { #[allow(unused_mut)] let mut builder = builder + .plugin( + tauri_plugin_log::Builder::default() + .level(log::LevelFilter::Info) + .build(), + ) + .plugin(tauri_plugin_sample::init()) .setup(move |app| { - tray::create_tray(app)?; - - #[allow(unused_mut)] - let mut window_builder = WindowBuilder::new(app, "main", WindowUrl::default()) - .user_agent("Tauri API") - .title("Tauri API Validation") - .inner_size(1000., 800.) - .min_inner_size(600., 400.) - .content_protected(true); - - #[cfg(target_os = "windows")] + #[cfg(desktop)] { - window_builder = window_builder.transparent(true).decorations(false); + let handle = app.handle(); + tray::create_tray(&handle)?; + handle.plugin(tauri_plugin_cli::init())?; } - let window = window_builder.build().unwrap(); + #[cfg(target_os = "macos")] + app.manage(AppMenu::(Default::default())); + + #[cfg(desktop)] + app.manage(PopupMenu( + tauri::menu::MenuBuilder::new(app) + .check("Tauri is awesome!") + .text("Do something") + .copy() + .build()?, + )); - #[cfg(target_os = "windows")] + let mut window_builder = WindowBuilder::new(app, "main", WindowUrl::default()); + #[cfg(desktop)] { - let _ = window_shadows::set_shadow(&window, true); + window_builder = window_builder + .title("Tauri API Validation") + .inner_size(1000., 800.) + .min_inner_size(600., 400.) + .content_protected(true) + .menu(tauri::menu::Menu::default(&app.handle())?); } + let window = window_builder.build().unwrap(); + #[cfg(debug_assertions)] window.open_devtools(); + let value = Some("test".to_string()); + let response = app.sample().ping(PingRequest { + value: value.clone(), + on_event: Channel::new(|event| { + println!("got channel event: {:?}", event); + Ok(()) + }), + }); + log::info!("got response: {:?}", response); + if let Ok(res) = response { + assert_eq!(res.value, value); + } + + #[cfg(desktop)] std::thread::spawn(|| { let server = match tiny_http::Server::http("localhost:3003") { Ok(s) => s, @@ -95,16 +141,15 @@ pub fn run_app) + Send + 'static>( }); }); - #[cfg(target_os = "macos")] - { - builder = builder.menu(tauri::Menu::os_default("Tauri API Validation")); - } - #[allow(unused_mut)] let mut app = builder .invoke_handler(tauri::generate_handler![ cmd::log_operation, cmd::perform_request, + #[cfg(desktop)] + cmd::toggle_menu, + #[cfg(desktop)] + cmd::popup_context_menu ]) .build(tauri::tauri_build_context!()) .expect("error while building tauri application"); @@ -112,61 +157,12 @@ pub fn run_app) + Send + 'static>( #[cfg(target_os = "macos")] app.set_activation_policy(tauri::ActivationPolicy::Regular); - app.run(move |app_handle, e| { - match e { - // Application is ready (triggered only once) - RunEvent::Ready => { - let app_handle = app_handle.clone(); - app_handle - .global_shortcut_manager() - .register("CmdOrCtrl+1", move || { - let app_handle = app_handle.clone(); - if let Some(window) = app_handle.get_window("main") { - message( - Some(&window), - "Tauri API", - "CmdOrCtrl+1 global shortcut triggered", - ); - } - }) - .unwrap(); - } - - // Triggered when a window is trying to close - RunEvent::WindowEvent { - label, - event: WindowEvent::CloseRequested { api, .. }, - .. - } => { - // for other windows, we handle it in JS - if label == "main" { - let app_handle = app_handle.clone(); - let window = app_handle.get_window(&label).unwrap(); - // use the exposed close api, and prevent the event loop to close - api.prevent_close(); - // ask the user if he wants to quit - ask( - Some(&window), - "Tauri API", - "Are you sure that you want to close this window?", - move |answer| { - if answer { - // .close() cannot be called on the main thread - std::thread::spawn(move || { - app_handle.get_window(&label).unwrap().close().unwrap(); - }); - } - }, - ); - } - } - #[cfg(not(test))] - RunEvent::ExitRequested { api, .. } => { - // Keep the event loop running even if all windows are closed - // This allow us to catch system tray events when there is no window - api.prevent_exit(); - } - _ => (), + app.run(move |_app_handle, _event| { + #[cfg(all(desktop, not(test)))] + if let RunEvent::ExitRequested { api, .. } = &_event { + // Keep the event loop running even if all windows are closed + // This allow us to catch tray icon events when there is no window + api.prevent_exit(); } }) } diff --git a/examples/api/src-tauri/src/main.rs b/examples/api/src-tauri/src/main.rs index e9885338f1b..33ab0aa16d1 100644 --- a/examples/api/src-tauri/src/main.rs +++ b/examples/api/src-tauri/src/main.rs @@ -6,6 +6,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - #[cfg(desktop)] api_lib::run(); } diff --git a/examples/api/src-tauri/src/tray.rs b/examples/api/src-tauri/src/tray.rs index e69f99b3486..698255061ca 100644 --- a/examples/api/src-tauri/src/tray.rs +++ b/examples/api/src-tauri/src/tray.rs @@ -4,141 +4,113 @@ use std::sync::atomic::{AtomicBool, Ordering}; use tauri::{ - api::{ - dialog::{MessageDialogBuilder, MessageDialogButtons}, - shell, - }, - CustomMenuItem, Manager, Runtime, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowBuilder, - WindowUrl, + menu::{Menu, MenuItem}, + tray::{ClickType, TrayIconBuilder}, + Manager, Runtime, WindowBuilder, WindowUrl, }; -pub fn create_tray(app: &tauri::App) -> tauri::Result<()> { - let mut tray_menu1 = SystemTrayMenu::new() - .add_item(CustomMenuItem::new("toggle", "Toggle")) - .add_item(CustomMenuItem::new("new", "New window")) - .add_item(CustomMenuItem::new("icon_1", "Tray Icon 1")) - .add_item(CustomMenuItem::new("icon_2", "Tray Icon 2")); - +pub fn create_tray(app: &tauri::AppHandle) -> tauri::Result<()> { + let toggle_i = MenuItem::with_id(app, "toggle", "Toggle", true, None); + let new_window_i = MenuItem::with_id(app, "new-window", "New window", true, None); + let icon_i_1 = MenuItem::with_id(app, "icon-1", "Icon 1", true, None); + let icon_i_2 = MenuItem::with_id(app, "icon-2", "Icon 2", true, None); #[cfg(target_os = "macos")] - { - tray_menu1 = tray_menu1.add_item(CustomMenuItem::new("set_title", "Set Title")); - } - - tray_menu1 = tray_menu1 - .add_item(CustomMenuItem::new("switch_menu", "Switch Menu")) - .add_item(CustomMenuItem::new("about", "About")) - .add_item(CustomMenuItem::new("exit_app", "Quit")) - .add_item(CustomMenuItem::new("destroy", "Destroy")); + let set_title_i = MenuItem::with_id(app, "set-title", "Set Title", true, None); + let switch_i = MenuItem::with_id(app, "switch-menu", "Switch Menu", true, None); + let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None); + let remove_tray_i = MenuItem::with_id(app, "remove-tray", "Remove Tray icon", true, None); + let menu1 = Menu::with_items( + app, + &[ + &toggle_i, + &new_window_i, + &icon_i_1, + &icon_i_2, + #[cfg(target_os = "macos")] + &set_title_i, + &switch_i, + &quit_i, + &remove_tray_i, + ], + )?; + let menu2 = Menu::with_items( + app, + &[&toggle_i, &new_window_i, &switch_i, &quit_i, &remove_tray_i], + )?; - let tray_menu2 = SystemTrayMenu::new() - .add_item(CustomMenuItem::new("toggle", "Toggle")) - .add_item(CustomMenuItem::new("new", "New window")) - .add_item(CustomMenuItem::new("switch_menu", "Switch Menu")) - .add_item(CustomMenuItem::new("about", "About")) - .add_item(CustomMenuItem::new("exit_app", "Quit")) - .add_item(CustomMenuItem::new("destroy", "Destroy")); let is_menu1 = AtomicBool::new(true); - let handle = app.handle(); - let tray_id = "my-tray".to_string(); - SystemTray::new() - .with_id(&tray_id) - .with_menu(tray_menu1.clone()) - .with_tooltip("Tauri") - .on_event(move |event| { - let tray_handle = handle.tray_handle_by_id(&tray_id).unwrap(); - match event { - SystemTrayEvent::LeftClick { - position: _, - size: _, - .. - } => { - let window = handle.get_window("main").unwrap(); - window.show().unwrap(); - window.set_focus().unwrap(); + let _ = TrayIconBuilder::with_id("tray-1") + .tooltip("Tauri") + .icon(app.default_window_icon().unwrap().clone()) + .menu(&menu1) + .menu_on_left_click(false) + .on_menu_event(move |app, event| match event.id.as_ref() { + "quit" => { + app.exit(0); + } + "remove-tray" => { + app.remove_tray_by_id("tray-1"); + } + "toggle" => { + if let Some(window) = app.get_window("main") { + let new_title = if window.is_visible().unwrap_or_default() { + let _ = window.hide(); + "Show" + } else { + let _ = window.show(); + let _ = window.set_focus(); + "Hide" + }; + toggle_i.set_text(new_title).unwrap(); } - SystemTrayEvent::MenuItemClick { id, .. } => { - let item_handle = tray_handle.get_item(&id); - match id.as_str() { - "exit_app" => { - // exit the app - handle.exit(0); - } - "destroy" => { - tray_handle.destroy().unwrap(); - } - "toggle" => { - let window = handle.get_window("main").unwrap(); - let new_title = if window.is_visible().unwrap() { - window.hide().unwrap(); - "Show" - } else { - window.show().unwrap(); - "Hide" - }; - item_handle.set_title(new_title).unwrap(); - } - "new" => { - WindowBuilder::new(&handle, "new", WindowUrl::App("index.html".into())) - .title("Tauri") - .build() - .unwrap(); - } - "set_title" => { - #[cfg(target_os = "macos")] - tray_handle.set_title("Tauri").unwrap(); - } - "icon_1" => { - #[cfg(target_os = "macos")] - tray_handle.set_icon_as_template(true).unwrap(); - - tray_handle - .set_icon(tauri::Icon::Raw( - include_bytes!("../../../.icons/tray_icon_with_transparency.png").to_vec(), - )) - .unwrap(); - } - "icon_2" => { - #[cfg(target_os = "macos")] - tray_handle.set_icon_as_template(true).unwrap(); + } + "new-window" => { + let _ = WindowBuilder::new(app, "new", WindowUrl::App("index.html".into())) + .title("Tauri") + .build(); + } + #[cfg(target_os = "macos")] + "set-title" => { + if let Some(tray) = app.tray_by_id("tray-1") { + let _ = tray.set_title(Some("Tauri")); + } + } + i @ "icon-1" | i @ "icon-2" => { + if let Some(tray) = app.tray_by_id("tray-1") { + let _ = tray.set_icon(Some(tauri::Icon::Raw(if i == "icon-1" { + include_bytes!("../../../.icons/icon.ico").to_vec() + } else { + include_bytes!("../../../.icons/tray_icon_with_transparency.png").to_vec() + }))); + } + } + "switch-menu" => { + let flag = is_menu1.load(Ordering::Relaxed); + let (menu, tooltip) = if flag { + (menu2.clone(), "Menu 2") + } else { + (menu1.clone(), "Tauri") + }; + if let Some(tray) = app.tray_by_id("tray-1") { + let _ = tray.set_menu(Some(menu)); + let _ = tray.set_tooltip(Some(tooltip)); + } + is_menu1.store(!flag, Ordering::Relaxed); + } - tray_handle - .set_icon(tauri::Icon::Raw( - include_bytes!("../../../.icons/icon.ico").to_vec(), - )) - .unwrap(); - } - "switch_menu" => { - let flag = is_menu1.load(Ordering::Relaxed); - let (menu, tooltip) = if flag { - (tray_menu2.clone(), "Menu 2") - } else { - (tray_menu1.clone(), "Tauri") - }; - tray_handle.set_menu(menu).unwrap(); - tray_handle.set_tooltip(tooltip).unwrap(); - is_menu1.store(!flag, Ordering::Relaxed); - } - "about" => { - let window = handle.get_window("main").unwrap(); - MessageDialogBuilder::new("About app", "Tauri demo app") - .parent(&window) - .buttons(MessageDialogButtons::OkCancelWithLabels( - "Homepage".into(), - "know it".into(), - )) - .show(move |ok| { - if ok { - shell::open(&window.shell_scope(), "https://tauri.app/", None).unwrap(); - } - }); - } - _ => {} - } + _ => {} + }) + .on_tray_event(|tray, event| { + if event.click_type == ClickType::Left { + let app = tray.app_handle(); + if let Some(window) = app.get_window("main") { + let _ = window.show(); + let _ = window.set_focus(); } - _ => {} } }) - .build(app) - .map(|_| ()) + .build(app); + + Ok(()) } diff --git a/examples/api/src-tauri/tauri-plugin-sample/.gitignore b/examples/api/src-tauri/tauri-plugin-sample/.gitignore new file mode 100644 index 00000000000..24ae128058b --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/.gitignore @@ -0,0 +1 @@ +.tauri diff --git a/examples/sidecar/src-tauri/Cargo.lock b/examples/api/src-tauri/tauri-plugin-sample/Cargo.lock similarity index 90% rename from examples/sidecar/src-tauri/Cargo.lock rename to examples/api/src-tauri/tauri-plugin-sample/Cargo.lock index 10b074a6a77..cb47713eba7 100644 --- a/examples/sidecar/src-tauri/Cargo.lock +++ b/examples/api/src-tauri/tauri-plugin-sample/Cargo.lock @@ -40,9 +40,9 @@ checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] name = "atk" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +checksum = "39991bc421ddf72f70159011b323ff49b0f783cc676a7287c59453da2e2531cf" dependencies = [ "atk-sys", "bitflags", @@ -52,14 +52,29 @@ dependencies = [ [[package]] name = "atk-sys" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +checksum = "11ad703eb64dc058024f0e57ccfa069e15a413b98dbd50a1a950e743b7f11148" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.3", + "system-deps", +] + +[[package]] +name = "attohttpc" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85f766c20e6ae766956f7a2fcc4e0931e79a7e1f48b29132b5d647021114914" +dependencies = [ + "flate2", + "http", + "log", + "serde", + "serde_json", + "serde_urlencoded", + "url", ] [[package]] @@ -76,9 +91,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" [[package]] name = "bitflags" @@ -114,9 +129,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.3.4" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -152,33 +167,34 @@ checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "cairo-rs" -version = "0.15.12" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d" dependencies = [ "bitflags", "cairo-sys-rs", "glib", "libc", + "once_cell", "thiserror", ] [[package]] name = "cairo-sys-rs" -version = "0.15.1" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421" dependencies = [ "glib-sys", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] name = "cargo_toml" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1204fe51a1e56042b8ec31d6407547ecd18f596b66f470dadb9abd9be9c843" +checksum = "2bfbc36312494041e2cdd5f06697b7e89d4b76f42773a0b5556ac290ff22acc2" dependencies = [ "serde", "toml", @@ -207,15 +223,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "cfg-expr" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7" -dependencies = [ - "smallvec", -] - [[package]] name = "cfg-expr" version = "0.11.0" @@ -691,9 +698,9 @@ dependencies = [ [[package]] name = "gdk" -version = "0.15.4" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +checksum = "aa9cb33da481c6c040404a11f8212d193889e9b435db2c14fd86987f630d3ce1" dependencies = [ "bitflags", "cairo-rs", @@ -707,9 +714,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.15.11" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +checksum = "c3578c60dee9d029ad86593ed88cb40f35c1b83360e12498d055022385dd9a05" dependencies = [ "bitflags", "gdk-pixbuf-sys", @@ -720,22 +727,22 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016" dependencies = [ "gio-sys", "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] name = "gdk-sys" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -745,19 +752,33 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps 6.0.3", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4511710212ed3020b61a8622a37aa6f0dd2a84516575da92e9b96928dcbe83ba" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", ] [[package]] name = "gdkx11-sys" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b7f8c7a84b407aa9b143877e267e848ff34106578b64d1e0a24bf550716178" +checksum = "9fa2bf8b5b8c414bc5d05e48b271896d0fd3ddb57464a3108438082da61de6af" dependencies = [ "gdk-sys", "glib-sys", "libc", - "system-deps 6.0.3", + "system-deps", "x11", ] @@ -771,7 +792,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows", + "windows 0.39.0", ] [[package]] @@ -808,45 +829,50 @@ dependencies = [ [[package]] name = "gio" -version = "0.15.12" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092" dependencies = [ "bitflags", "futures-channel", "futures-core", "futures-io", + "futures-util", "gio-sys", "glib", "libc", "once_cell", + "pin-project-lite", + "smallvec", "thiserror", ] [[package]] name = "gio-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" +checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.3", + "system-deps", "winapi", ] [[package]] name = "glib" -version = "0.15.12" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +checksum = "ddd4df61a866ed7259d6189b8bcb1464989a77f1d85d25d002279bbe9dd38b2f" dependencies = [ "bitflags", "futures-channel", "futures-core", "futures-executor", "futures-task", + "futures-util", + "gio-sys", "glib-macros", "glib-sys", "gobject-sys", @@ -858,12 +884,12 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.15.11" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" +checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" dependencies = [ "anyhow", - "heck 0.4.0", + "heck", "proc-macro-crate", "proc-macro-error", "proc-macro2", @@ -873,12 +899,12 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" dependencies = [ "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -902,20 +928,20 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" dependencies = [ "glib-sys", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] name = "gtk" -version = "0.15.5" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +checksum = "e4d3507d43908c866c805f74c9dd593c0ce7ba5c38e576e41846639cdcd4bee6" dependencies = [ "atk", "bitflags", @@ -936,9 +962,9 @@ dependencies = [ [[package]] name = "gtk-sys" -version = "0.15.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +checksum = "89b5f8946685d5fe44497007786600c2f368ff6b1e61a16251c89f72a97520a3" dependencies = [ "atk-sys", "cairo-sys-rs", @@ -949,14 +975,14 @@ dependencies = [ "gobject-sys", "libc", "pango-sys", - "system-deps 6.0.3", + "system-deps", ] [[package]] name = "gtk3-macros" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9" +checksum = "8cfd6557b1018b773e43c8de9d0d13581d6b36190d0501916cbec4731db5ccff" dependencies = [ "anyhow", "proc-macro-crate", @@ -972,15 +998,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.4.0" @@ -1126,9 +1143,9 @@ checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" [[package]] name = "javascriptcore-rs" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" +checksum = "110b9902c80c12bf113c432d0b71c7a94490b294a8234f326fd0abca2fac0b00" dependencies = [ "bitflags", "glib", @@ -1137,14 +1154,14 @@ dependencies = [ [[package]] name = "javascriptcore-rs-sys" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "905fbb87419c5cde6e3269537e4ea7d46431f3008c5d057e915ef3f115e7793c" +checksum = "98a216519a52cd941a733a0ad3f1023cfdb1cd47f3955e8e863ed56f558f916c" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 5.0.0", + "system-deps", ] [[package]] @@ -1459,16 +1476,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" -[[package]] -name = "os_pipe" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6a252f1f8c11e84b3ab59d7a488e48e4478a93937e027076638c49536204639" -dependencies = [ - "libc", - "windows-sys", -] - [[package]] name = "overload" version = "0.1.1" @@ -1477,11 +1484,12 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pango" -version = "0.15.10" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94" dependencies = [ "bitflags", + "gio", "glib", "libc", "once_cell", @@ -1490,14 +1498,14 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -1657,16 +1665,16 @@ checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "plist" -version = "1.4.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5329b8f106a176ab0dce4aae5da86bfcb139bb74fb00882859e03745011f3635" +checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225" dependencies = [ "base64 0.13.1", "indexmap", "line-wrap", - "quick-xml", "serde", "time", + "xml-rs", ] [[package]] @@ -1736,22 +1744,13 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.50" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" dependencies = [ "unicode-ident", ] -[[package]] -name = "quick-xml" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" -dependencies = [ - "memchr", -] - [[package]] name = "quote" version = "1.0.23" @@ -2052,6 +2051,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.5", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "1.14.0" @@ -2126,26 +2137,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shared_child" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "sidecar" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "tauri", - "tauri-build", -] - [[package]] name = "siphasher" version = "0.3.10" @@ -2168,31 +2159,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] -name = "soup2" -version = "0.2.1" +name = "soup3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" +checksum = "82bc46048125fefd69d30b32b9d263d6556c9ffe82a7a7df181a86d912da5616" dependencies = [ "bitflags", + "futures-channel", "gio", "glib", "libc", "once_cell", - "soup2-sys", + "soup3-sys", ] [[package]] -name = "soup2-sys" -version = "0.2.0" +name = "soup3-sys" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" +checksum = "014bbeb1c4cdb30739dc181e8d98b7908f124d9555843afa89b5570aaf4ec62b" dependencies = [ - "bitflags", "gio-sys", "glib-sys", "gobject-sys", "libc", - "system-deps 5.0.0", + "system-deps", ] [[package]] @@ -2242,6 +2233,16 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "swift-rs" +version = "0.3.0" +source = "git+https://github.com/Brendonovich/swift-rs?rev=eb6de914ad57501da5019154d476d45660559999#eb6de914ad57501da5019154d476d45660559999" +dependencies = [ + "base64 0.13.1", + "serde", + "serde_json", +] + [[package]] name = "syn" version = "1.0.107" @@ -2253,37 +2254,23 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "system-deps" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e" -dependencies = [ - "cfg-expr 0.9.1", - "heck 0.3.3", - "pkg-config", - "toml", - "version-compare 0.0.11", -] - [[package]] name = "system-deps" version = "6.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff" dependencies = [ - "cfg-expr 0.11.0", - "heck 0.4.0", + "cfg-expr", + "heck", "pkg-config", "toml", - "version-compare 0.1.1", + "version-compare", ] [[package]] name = "tao" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704522803dda895767f69198af8351b0a3f4fe2e293c3ca54cce0ecba05a97f2" +version = "0.18.0" +source = "git+https://github.com/tauri-apps/tao?branch=dev#382ea30300b8e909cae5896b57c7c623c9825655" dependencies = [ "bitflags", "cairo-rs", @@ -2296,6 +2283,7 @@ dependencies = [ "gdk", "gdk-pixbuf", "gdk-sys", + "gdkwayland-sys", "gdkx11-sys", "gio", "glib", @@ -2320,16 +2308,15 @@ dependencies = [ "tao-macros", "unicode-segmentation", "uuid", - "windows", + "windows 0.44.0", "windows-implement", "x11-dl", ] [[package]] name = "tao-macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b6fcd8245d45a39ffc8715183d92ae242750eb57b285eb3bcd63dfd512afd09" +version = "0.1.1" +source = "git+https://github.com/tauri-apps/tao?branch=dev#382ea30300b8e909cae5896b57c7c623c9825655" dependencies = [ "proc-macro2", "quote", @@ -2349,9 +2336,10 @@ dependencies = [ [[package]] name = "tauri" -version = "1.2.3" +version = "2.0.0-alpha.3" dependencies = [ "anyhow", + "attohttpc", "cocoa", "dirs-next", "embed_plist", @@ -2361,24 +2349,26 @@ dependencies = [ "glib", "glob", "gtk", - "heck 0.4.0", + "heck", "http", "ignore", + "jni", + "libc", + "log", "objc", "once_cell", - "os_pipe", "percent-encoding", "rand 0.8.5", "raw-window-handle", - "regex", "semver 1.0.16", "serde", "serde_json", "serde_repr", "serialize-to-javascript", - "shared_child", "state", + "swift-rs", "tar", + "tauri-build", "tauri-macros", "tauri-runtime", "tauri-runtime-wry", @@ -2390,30 +2380,32 @@ dependencies = [ "uuid", "webkit2gtk", "webview2-com", - "windows", + "windows 0.44.0", ] [[package]] name = "tauri-build" -version = "1.2.1" +version = "2.0.0-alpha.1" dependencies = [ "anyhow", "cargo_toml", - "heck 0.4.0", + "filetime", + "heck", "json-patch", - "quote", "semver 1.0.16", + "serde", "serde_json", - "tauri-codegen", + "swift-rs", "tauri-utils", - "winres", + "tauri-winres", + "walkdir", ] [[package]] name = "tauri-codegen" -version = "1.2.1" +version = "2.0.0-alpha.1" dependencies = [ - "base64 0.20.0", + "base64 0.21.0", "brotli", "ico", "json-patch", @@ -2421,7 +2413,6 @@ dependencies = [ "png", "proc-macro2", "quote", - "regex", "semver 1.0.16", "serde", "serde_json", @@ -2429,15 +2420,16 @@ dependencies = [ "tauri-utils", "thiserror", "time", + "url", "uuid", "walkdir", ] [[package]] name = "tauri-macros" -version = "1.2.1" +version = "2.0.0-alpha.1" dependencies = [ - "heck 0.4.0", + "heck", "proc-macro2", "quote", "syn", @@ -2445,13 +2437,23 @@ dependencies = [ "tauri-utils", ] +[[package]] +name = "tauri-plugin-sample" +version = "0.1.0" +dependencies = [ + "log", + "tauri", + "tauri-build", +] + [[package]] name = "tauri-runtime" -version = "0.12.1" +version = "0.13.0-alpha.1" dependencies = [ "gtk", "http", "http-range", + "jni", "rand 0.8.5", "raw-window-handle", "serde", @@ -2461,15 +2463,16 @@ dependencies = [ "url", "uuid", "webview2-com", - "windows", + "windows 0.44.0", ] [[package]] name = "tauri-runtime-wry" -version = "0.12.2" +version = "0.13.0-alpha.1" dependencies = [ "cocoa", "gtk", + "jni", "percent-encoding", "rand 0.8.5", "raw-window-handle", @@ -2478,18 +2481,18 @@ dependencies = [ "uuid", "webkit2gtk", "webview2-com", - "windows", + "windows 0.44.0", "wry", ] [[package]] name = "tauri-utils" -version = "1.2.1" +version = "2.0.0-alpha.1" dependencies = [ "brotli", "ctor", "glob", - "heck 0.4.0", + "heck", "html5ever", "infer", "json-patch", @@ -2505,7 +2508,17 @@ dependencies = [ "thiserror", "url", "walkdir", - "windows", + "windows 0.44.0", +] + +[[package]] +name = "tauri-winres" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b7a78dc04f75fb5ab815e66ac561c81e92a968a40f29e7c21afd152d694fad8" +dependencies = [ + "toml", + "version_check", ] [[package]] @@ -2612,9 +2625,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.24.2" +version = "1.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" +checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae" dependencies = [ "autocfg", "bytes", @@ -2763,9 +2776,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" dependencies = [ "getrandom 0.2.8", ] @@ -2776,12 +2789,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "version-compare" -version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" - [[package]] name = "version-compare" version = "0.1.1" @@ -2819,9 +2826,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "webkit2gtk" -version = "0.18.2" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" +checksum = "d8eea819afe15eb8dcdff4f19d8bfda540bae84d874c10e6f4b8faf2d6704bd1" dependencies = [ "bitflags", "cairo-rs", @@ -2837,20 +2844,18 @@ dependencies = [ "javascriptcore-rs", "libc", "once_cell", - "soup2", + "soup3", "webkit2gtk-sys", ] [[package]] name = "webkit2gtk-sys" -version = "0.18.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" +checksum = "d0ac7a95ddd3fdfcaf83d8e513b4b1ad101b95b413b6aa6662ed95f284fc3d5b" dependencies = [ - "atk-sys", "bitflags", "cairo-sys-rs", - "gdk-pixbuf-sys", "gdk-sys", "gio-sys", "glib-sys", @@ -2858,21 +2863,20 @@ dependencies = [ "gtk-sys", "javascriptcore-rs-sys", "libc", - "pango-sys", "pkg-config", - "soup2-sys", - "system-deps 6.0.3", + "soup3-sys", + "system-deps", ] [[package]] name = "webview2-com" -version = "0.19.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178" +checksum = "03411e89ec447e29c08b3c086edeb88c5f8fd782cbdd4d6d316bea439be7a244" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.44.0", "windows-implement", ] @@ -2889,15 +2893,15 @@ dependencies = [ [[package]] name = "webview2-com-sys" -version = "0.19.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7" +checksum = "1c0f5ce43e9611c5b2983a33156d6abe31abf39185bad84a6766c80ba1dbf1ab" dependencies = [ "regex", "serde", "serde_json", "thiserror", - "windows", + "windows 0.44.0", "windows-bindgen", "windows-metadata", ] @@ -2939,7 +2943,6 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" dependencies = [ - "windows-implement", "windows_aarch64_msvc 0.39.0", "windows_i686_gnu 0.39.0", "windows_i686_msvc 0.39.0", @@ -2947,11 +2950,22 @@ dependencies = [ "windows_x86_64_msvc 0.39.0", ] +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-targets", +] + [[package]] name = "windows-bindgen" -version = "0.39.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68003dbd0e38abc0fb85b939240f4bce37c43a5981d3df37ccbaaa981b47cb41" +checksum = "222204ecf46521382a4d88b4a1bbefca9f8855697b4ab7d20803901425e061a3" dependencies = [ "windows-metadata", "windows-tokens", @@ -2959,19 +2973,31 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.39.0" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7" +checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f" dependencies = [ + "proc-macro2", + "quote", "syn", - "windows-tokens", ] [[package]] name = "windows-metadata" -version = "0.39.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" +checksum = "ee78911e3f4ce32c1ad9d3c7b0bd95389662ad8d8f1a3155688fed70bd96e2b6" [[package]] name = "windows-sys" @@ -2988,11 +3014,26 @@ dependencies = [ "windows_x86_64_msvc 0.42.1", ] +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.1", +] + [[package]] name = "windows-tokens" -version = "0.39.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" +checksum = "fa4251900975a0d10841c5d4bde79c56681543367ef811f3fabb8d1803b0959b" [[package]] name = "windows_aarch64_gnullvm" @@ -3066,20 +3107,10 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" -[[package]] -name = "winres" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" -dependencies = [ - "toml", -] - [[package]] name = "wry" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c846dc4dda988e959869dd0802cd27417c9696e584593e49178aeee28890d25" +version = "0.26.0" +source = "git+https://github.com/tauri-apps/wry?branch=dev#077eb3a7ca520d07e73f899da60ce23eef941e6f" dependencies = [ "base64 0.13.1", "block", @@ -3102,14 +3133,14 @@ dependencies = [ "serde", "serde_json", "sha2", - "soup2", + "soup3", "tao", "thiserror", "url", "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.44.0", "windows-implement", ] @@ -3125,12 +3156,12 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.20.1" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1536d6965a5d4e573c7ef73a2c15ebcd0b2de3347bdf526c34c297c00ac40f0" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" dependencies = [ - "lazy_static", "libc", + "once_cell", "pkg-config", ] @@ -3142,3 +3173,9 @@ checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" dependencies = [ "libc", ] + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" diff --git a/examples/api/src-tauri/tauri-plugin-sample/Cargo.toml b/examples/api/src-tauri/tauri-plugin-sample/Cargo.toml new file mode 100644 index 00000000000..41c002a1a09 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tauri-plugin-sample" +version = "0.1.0" +edition = "2021" +links = "tauri-plugin-sample" + +[dependencies] +tauri = { path = "../../../../core/tauri" } +log = "0.4" +serde = "1" +thiserror = "1" + +[build-dependencies] +tauri-build = { path = "../../../../core/tauri-build/" } diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/.gitignore b/examples/api/src-tauri/tauri-plugin-sample/android/.gitignore new file mode 100644 index 00000000000..ae485e87f4d --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/.gitignore @@ -0,0 +1,2 @@ +/build +.tauri diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/build.gradle.kts b/examples/api/src-tauri/tauri-plugin-sample/android/build.gradle.kts new file mode 100644 index 00000000000..05d495993ee --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.plugin.sample" + compileSdk = 32 + + defaultConfig { + minSdk = 21 + targetSdk = 33 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.6.0") + implementation("com.google.android.material:material:1.7.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation(project(":tauri-android")) +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/proguard-rules.pro b/examples/api/src-tauri/tauri-plugin-sample/android/proguard-rules.pro new file mode 100644 index 00000000000..481bb434814 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/settings.gradle b/examples/api/src-tauri/tauri-plugin-sample/android/settings.gradle new file mode 100644 index 00000000000..14a752e4336 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/settings.gradle @@ -0,0 +1,2 @@ +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/src/androidTest/java/com/plugin/sample/ExampleInstrumentedTest.kt b/examples/api/src-tauri/tauri-plugin-sample/android/src/androidTest/java/com/plugin/sample/ExampleInstrumentedTest.kt new file mode 100644 index 00000000000..5e343b5db44 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/src/androidTest/java/com/plugin/sample/ExampleInstrumentedTest.kt @@ -0,0 +1,28 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.plugin.sample + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.plugin.sample", appContext.packageName) + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/src/main/AndroidManifest.xml b/examples/api/src-tauri/tauri-plugin-sample/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..a5918e68abc --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/src/main/java/com/plugin/sample/Example.kt b/examples/api/src-tauri/tauri-plugin-sample/android/src/main/java/com/plugin/sample/Example.kt new file mode 100644 index 00000000000..a824086528c --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/src/main/java/com/plugin/sample/Example.kt @@ -0,0 +1,14 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.plugin.sample + +import android.util.Log + +class Example { + fun pong(value: String): String { + Log.i("Pong", value) + return value + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/src/main/java/com/plugin/sample/ExamplePlugin.kt b/examples/api/src-tauri/tauri-plugin-sample/android/src/main/java/com/plugin/sample/ExamplePlugin.kt new file mode 100644 index 00000000000..69f11898eac --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/src/main/java/com/plugin/sample/ExamplePlugin.kt @@ -0,0 +1,30 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.plugin.sample + +import android.app.Activity +import app.tauri.annotation.Command +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.JSObject +import app.tauri.plugin.Plugin +import app.tauri.plugin.Invoke + +@TauriPlugin +class ExamplePlugin(private val activity: Activity): Plugin(activity) { + private val implementation = Example() + + @Command + fun ping(invoke: Invoke) { + val onEvent = invoke.getChannel("onEvent") + val event = JSObject() + event.put("kind", "ping") + onEvent?.send(event) + + val value = invoke.getString("value") ?: "" + val ret = JSObject() + ret.put("value", implementation.pong(value)) + invoke.resolve(ret) + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/android/src/test/java/com/plugin/sample/ExampleUnitTest.kt b/examples/api/src-tauri/tauri-plugin-sample/android/src/test/java/com/plugin/sample/ExampleUnitTest.kt new file mode 100644 index 00000000000..15ca3634927 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/android/src/test/java/com/plugin/sample/ExampleUnitTest.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.plugin.sample + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/build.rs b/examples/api/src-tauri/tauri-plugin-sample/build.rs new file mode 100644 index 00000000000..35bb9b5a57c --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/build.rs @@ -0,0 +1,16 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::process::exit; + +fn main() { + if let Err(error) = tauri_build::mobile::PluginBuilder::new() + .android_path("android") + .ios_path("ios") + .run() + { + println!("{error:#}"); + exit(1); + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/ios/.gitignore b/examples/api/src-tauri/tauri-plugin-sample/ios/.gitignore new file mode 100644 index 00000000000..5922fdaa563 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/ios/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/examples/api/src-tauri/tauri-plugin-sample/ios/Package.swift b/examples/api/src-tauri/tauri-plugin-sample/ios/Package.swift new file mode 100644 index 00000000000..87bf327038c --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/ios/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version:5.3 +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "tauri-plugin-sample", + platforms: [ + .iOS(.v13), + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-sample", + type: .static, + targets: ["tauri-plugin-sample"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(name: "Tauri", path: "../../../../../core/tauri/mobile/ios-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-sample", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/examples/api/src-tauri/tauri-plugin-sample/ios/README.md b/examples/api/src-tauri/tauri-plugin-sample/ios/README.md new file mode 100644 index 00000000000..54590444326 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/ios/README.md @@ -0,0 +1,3 @@ +# Tauri Plugin sample + +A description of this package. diff --git a/examples/api/src-tauri/tauri-plugin-sample/ios/Sources/ExamplePlugin.swift b/examples/api/src-tauri/tauri-plugin-sample/ios/Sources/ExamplePlugin.swift new file mode 100644 index 00000000000..9774ba895dd --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/ios/Sources/ExamplePlugin.swift @@ -0,0 +1,23 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import SwiftRs +import Tauri +import UIKit +import WebKit + +class ExamplePlugin: Plugin { + @objc public func ping(_ invoke: Invoke) throws { + let onEvent = invoke.getChannel("onEvent") + onEvent?.send(["kind": "ping"]) + + let value = invoke.getString("value") + invoke.resolve(["value": value as Any]) + } +} + +@_cdecl("init_plugin_sample") +func initPlugin() -> Plugin { + return ExamplePlugin() +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/ios/Tests/PluginTests/PluginTests.swift b/examples/api/src-tauri/tauri-plugin-sample/ios/Tests/PluginTests/PluginTests.swift new file mode 100644 index 00000000000..99992ce4c33 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/ios/Tests/PluginTests/PluginTests.swift @@ -0,0 +1,12 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import XCTest +@testable import ExamplePlugin + +final class ExamplePluginTests: XCTestCase { + func testExample() throws { + let plugin = ExamplePlugin() + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/src/desktop.rs b/examples/api/src-tauri/tauri-plugin-sample/src/desktop.rs new file mode 100644 index 00000000000..b96655d6ce3 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/src/desktop.rs @@ -0,0 +1,32 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::de::DeserializeOwned; +use tauri::{plugin::PluginApi, AppHandle, Runtime}; + +use crate::models::*; + +pub fn init( + app: &AppHandle, + _api: PluginApi, +) -> crate::Result> { + Ok(Sample(app.clone())) +} + +/// A helper class to access the sample APIs. +pub struct Sample(AppHandle); + +#[derive(serde::Serialize)] +struct Event { + kind: &'static str, +} + +impl Sample { + pub fn ping(&self, payload: PingRequest) -> crate::Result { + let _ = payload.on_event.send(Event { kind: "ping" }); + Ok(PingResponse { + value: payload.value, + }) + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/src/error.rs b/examples/api/src-tauri/tauri-plugin-sample/src/error.rs new file mode 100644 index 00000000000..bd7381ed8dd --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/src/error.rs @@ -0,0 +1,12 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[cfg(mobile)] + #[error(transparent)] + PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), +} + +pub type Result = std::result::Result; diff --git a/examples/api/src-tauri/tauri-plugin-sample/src/lib.rs b/examples/api/src-tauri/tauri-plugin-sample/src/lib.rs new file mode 100644 index 00000000000..39b2d9a3f5c --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/src/lib.rs @@ -0,0 +1,54 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri::{ + plugin::{Builder, TauriPlugin}, + Manager, Runtime, +}; + +pub use models::*; + +#[cfg(desktop)] +mod desktop; +#[cfg(mobile)] +mod mobile; + +mod error; +mod models; + +#[cfg(desktop)] +use desktop::Sample; +#[cfg(mobile)] +use mobile::Sample; + +pub use error::*; + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the sample APIs. +pub trait SampleExt { + fn sample(&self) -> &Sample; +} + +impl> crate::SampleExt for T { + fn sample(&self) -> &Sample { + self.state::>().inner() + } +} + +pub fn init() -> TauriPlugin { + Builder::new("sample") + .setup(|app, api| { + #[cfg(mobile)] + let sample = mobile::init(app, api)?; + #[cfg(desktop)] + let sample = desktop::init(app, api)?; + app.manage(sample); + + Ok(()) + }) + .on_navigation(|window, url| { + println!("navigation {} {url}", window.label()); + true + }) + .build() +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/src/mobile.rs b/examples/api/src-tauri/tauri-plugin-sample/src/mobile.rs new file mode 100644 index 00000000000..74f1114f917 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/src/mobile.rs @@ -0,0 +1,41 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::de::DeserializeOwned; +use tauri::{ + plugin::{PluginApi, PluginHandle}, + AppHandle, Runtime, +}; + +use crate::models::*; + +#[cfg(target_os = "android")] +const PLUGIN_IDENTIFIER: &str = "com.plugin.sample"; + +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_sample); + +// initializes the Kotlin or Swift plugin classes +pub fn init( + _app: &AppHandle, + api: PluginApi, +) -> crate::Result> { + #[cfg(target_os = "android")] + let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "ExamplePlugin")?; + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_sample)?; + Ok(Sample(handle)) +} + +/// A helper class to access the sample APIs. +pub struct Sample(PluginHandle); + +impl Sample { + pub fn ping(&self, payload: PingRequest) -> crate::Result { + self + .0 + .run_mobile_plugin("ping", payload) + .map_err(Into::into) + } +} diff --git a/examples/api/src-tauri/tauri-plugin-sample/src/models.rs b/examples/api/src-tauri/tauri-plugin-sample/src/models.rs new file mode 100644 index 00000000000..3c824a58912 --- /dev/null +++ b/examples/api/src-tauri/tauri-plugin-sample/src/models.rs @@ -0,0 +1,18 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Deserialize, Serialize}; +use tauri::ipc::Channel; + +#[derive(Serialize)] +pub struct PingRequest { + pub value: Option, + #[serde(rename = "onEvent")] + pub on_event: Channel, +} + +#[derive(Debug, Clone, Default, Deserialize)] +pub struct PingResponse { + pub value: Option, +} diff --git a/examples/api/src-tauri/tauri.conf.json b/examples/api/src-tauri/tauri.conf.json index 422f2e6ed00..784733dd675 100644 --- a/examples/api/src-tauri/tauri.conf.json +++ b/examples/api/src-tauri/tauri.conf.json @@ -4,20 +4,14 @@ "distDir": "../dist", "devPath": "http://localhost:5173", "beforeDevCommand": "yarn dev", - "beforeBuildCommand": "yarn build" + "beforeBuildCommand": "yarn build", + "withGlobalTauri": true }, "package": { "productName": "Tauri API", "version": "1.0.0" }, - "tauri": { - "pattern": { - "use": "isolation", - "options": { - "dir": "../isolation-dist/" - } - }, - "macOSPrivateApi": true, + "plugins": { "cli": { "description": "Tauri API example", "args": [ @@ -32,12 +26,15 @@ "name": "theme", "takesValue": true, "description": "App theme", - "possibleValues": ["light", "dark", "system"] + "possibleValues": [ + "light", + "dark", + "system" + ] }, { "short": "v", "name": "verbose", - "multipleOccurrences": true, "description": "Verbosity level" } ], @@ -53,7 +50,16 @@ ] } } + } + }, + "tauri": { + "pattern": { + "use": "isolation", + "options": { + "dir": "../isolation-dist/" + } }, + "macOSPrivateApi": true, "bundle": { "active": true, "identifier": "com.tauri.api", @@ -73,64 +79,39 @@ } } } - } - }, - "updater": { - "active": true, - "dialog": false, - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK", - "endpoints": [ - "https://tauri-update-server.vercel.app/update/{{target}}/{{current_version}}" - ] - }, - "allowlist": { - "all": true, - "fs": { - "scope": { - "allow": ["$APPDATA/db/**", "$DOWNLOAD/**", "$RESOURCE/**"], - "deny": ["$APPDATA/db/*.stronghold"] - } }, - "shell": { - "open": true, - "scope": [ - { - "name": "sh", - "cmd": "sh", - "args": ["-c", { "validator": "\\S+" }] - }, - { - "name": "cmd", - "cmd": "cmd", - "args": ["/C", { "validator": "\\S+" }] - } - ] - }, - "protocol": { - "asset": true, - "assetScope": { - "allow": ["$APPDATA/db/**", "$RESOURCE/**"], - "deny": ["$APPDATA/db/*.stronghold"] + "updater": { + "active": true, + "pubkey": "asdasd", + "windows": { + "installMode": "passive" } - }, - "http": { - "scope": ["http://localhost:3003"] } }, "windows": [], "security": { "csp": { "default-src": "'self' customprotocol: asset:", - "font-src": ["https://fonts.gstatic.com"], + "connect-src": "ipc: https://ipc.localhost", + "font-src": [ + "https://fonts.gstatic.com" + ], "img-src": "'self' asset: https://asset.localhost blob: data:", "style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com" }, - "freezePrototype": true - }, - "systemTray": { - "iconPath": "../../.icons/tray_icon_with_transparency.png", - "iconAsTemplate": true, - "menuOnLeftClick": false + "freezePrototype": true, + "assetProtocol": { + "enable": true, + "scope": { + "allow": [ + "$APPDATA/db/**", + "$RESOURCE/**" + ], + "deny": [ + "$APPDATA/db/*.stronghold" + ] + } + } } } } diff --git a/examples/api/src/App.svelte b/examples/api/src/App.svelte index d736e05c924..2d85287b551 100644 --- a/examples/api/src/App.svelte +++ b/examples/api/src/App.svelte @@ -1,45 +1,18 @@ - -{#if isWindows} -
- Tauri API Validation - - - {#if isDark} -
- {:else} -
- {/if} - - -
- - - {#if isWindowMaximized} -
- {:else} -
- {/if} - - -
- - -
-{/if} -
open('https://tauri.app/')} class="self-center p-7 cursor-pointer" src="tauri_logo.png" alt="Tauri logo" /> - {#if !isWindows} - - {#if isDark} - Switch to Light mode -
- {:else} - Switch to Dark mode -
- {/if} - -
-
-
- {/if} + + {#if isDark} + Switch to Light mode +
+ {:else} + Switch to Dark mode +
+ {/if} + +
+
+
- import { show, hide } from '@tauri-apps/api/app' - - export let onMessage - - function showApp() { - hideApp() - .then(() => { - setTimeout(() => { - show() - .then(() => onMessage('Shown app')) - .catch(onMessage) - }, 2000) - }) - .catch(onMessage) - } - - function hideApp() { - return hide() - .then(() => onMessage('Hide app')) - .catch(onMessage) - } - - -
- - -
diff --git a/examples/api/src/views/Cli.svelte b/examples/api/src/views/Cli.svelte deleted file mode 100644 index 9da4aa17eff..00000000000 --- a/examples/api/src/views/Cli.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - -

- This binary can be run from the terminal and takes the following arguments: - -

-  --config <PATH>
-  --theme <light|dark|system>
-  --verbose
- - Additionally, it has a update --background subcommand. -

-
-
- Note that the arguments are only parsed, not implemented. -
-
-
- diff --git a/examples/api/src/views/Clipboard.svelte b/examples/api/src/views/Clipboard.svelte deleted file mode 100644 index da1f1d7e4a5..00000000000 --- a/examples/api/src/views/Clipboard.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - -
- - - -
diff --git a/examples/api/src/views/Dialog.svelte b/examples/api/src/views/Dialog.svelte deleted file mode 100644 index bdeca61ac80..00000000000 --- a/examples/api/src/views/Dialog.svelte +++ /dev/null @@ -1,118 +0,0 @@ - - -
- - -
- -
-
- - -
-
- - -
-
- - diff --git a/examples/api/src/views/FileSystem.svelte b/examples/api/src/views/FileSystem.svelte deleted file mode 100644 index d8465cb37b4..00000000000 --- a/examples/api/src/views/FileSystem.svelte +++ /dev/null @@ -1,106 +0,0 @@ - - -
-
- - -
-
-
- - -
-
- -
- - diff --git a/examples/api/src/views/Http.svelte b/examples/api/src/views/Http.svelte deleted file mode 100644 index 16e6d47c4b9..00000000000 --- a/examples/api/src/views/Http.svelte +++ /dev/null @@ -1,99 +0,0 @@ - - -
- -
-