snapshot of https://github.com/tauri-apps/tauri.git (dev, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
This commit is contained in:
2
packages/api/.gitignore
vendored
Normal file
2
packages/api/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Build output
|
||||
/dist/
|
||||
1056
packages/api/CHANGELOG.md
Normal file
1056
packages/api/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
177
packages/api/LICENSE-APACHE-2.0
Normal file
177
packages/api/LICENSE-APACHE-2.0
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
21
packages/api/LICENSE-MIT
Normal file
21
packages/api/LICENSE-MIT
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 - Present Tauri Apps Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
52
packages/api/README.md
Normal file
52
packages/api/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# @tauri-apps/api
|
||||
|
||||
<img align="right" src="https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png" height="128" width="128">
|
||||
|
||||
[](https://github.com/tauri-apps/tauri/tree/dev)
|
||||
[](https://opencollective.com/tauri)
|
||||
[](https://github.com/tauri-apps/tauri/actions/workflows/lint-js.yml)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri?ref=badge_shield)
|
||||
[](https://discord.gg/SpmNs4S)
|
||||
[](https://tauri.app)
|
||||
[](https://good-labs.github.io/greater-good-affirmation)
|
||||
[](https://opencollective.com/tauri)
|
||||
|
||||
| Component | Version |
|
||||
| --------------- | ----------------------------------------------------- |
|
||||
| @tauri-apps/api |  |
|
||||
|
||||
## About Tauri
|
||||
|
||||
Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily.
|
||||
|
||||
Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task.
|
||||
|
||||
## This module
|
||||
|
||||
This is a typescript library that creates `cjs` and `esm` JavaScript endpoints for you to import into your Frontend framework so that the Webview can call and listen to backend activity. We also ship the pure typescript, because for some frameworks this is more optimal. It uses the message passing of webviews to their hosts.
|
||||
|
||||
To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document.
|
||||
|
||||
## Installation
|
||||
|
||||
The preferred method is to install this module locally as a dependency:
|
||||
|
||||
```
|
||||
$ pnpm add @tauri-apps/api
|
||||
$ yarn add @tauri-apps/api
|
||||
$ npm add @tauri-apps/api
|
||||
```
|
||||
|
||||
## Semver
|
||||
|
||||
**tauri** is following [Semantic Versioning 2.0](https://semver.org/).
|
||||
|
||||
## Licenses
|
||||
|
||||
Code: (c) 2019 - 2021 - The Tauri Programme within The Commons Conservancy.
|
||||
|
||||
MIT or MIT/Apache 2.0 where applicable.
|
||||
|
||||
Logo: CC-BY-NC-ND
|
||||
|
||||
- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum)
|
||||
61
packages/api/eslint.config.js
Normal file
61
packages/api/eslint.config.js
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import globals from 'globals'
|
||||
import eslint from '@eslint/js'
|
||||
import prettierConfig from 'eslint-config-prettier'
|
||||
import securityPlugin from 'eslint-plugin-security'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
export default [
|
||||
eslint.configs.recommended,
|
||||
prettierConfig,
|
||||
securityPlugin.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
__statics: true,
|
||||
process: true
|
||||
},
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: import.meta.dirname
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-console': 'error',
|
||||
'no-debugger': 'error',
|
||||
'no-process-exit': 'off',
|
||||
'security/detect-non-literal-fs-filename': 'warn',
|
||||
'security/detect-unsafe-regex': 'error',
|
||||
'security/detect-buffer-noassert': 'error',
|
||||
'security/detect-child-process': 'warn',
|
||||
'security/detect-disable-mustache-escape': 'error',
|
||||
'security/detect-eval-with-expression': 'error',
|
||||
'security/detect-no-csrf-before-method-override': 'error',
|
||||
'security/detect-non-literal-regexp': 'error',
|
||||
'security/detect-non-literal-require': 'warn',
|
||||
'security/detect-object-injection': 'warn',
|
||||
'security/detect-possible-timing-attacks': 'error',
|
||||
'security/detect-pseudoRandomBytes': 'error',
|
||||
'space-before-function-paren': 'off',
|
||||
'@typescript-eslint/default-param-last': 'off',
|
||||
'@typescript-eslint/strict-boolean-expressions': 0,
|
||||
'no-return-await': 'warn',
|
||||
'@typescript-eslint/return-await': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
63
packages/api/package.json
Normal file
63
packages/api/package.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "@tauri-apps/api",
|
||||
"version": "2.11.1",
|
||||
"description": "Tauri API definitions",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tauri-apps/tauri.git"
|
||||
},
|
||||
"contributors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tauri-apps/tauri/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tauri-apps/tauri#readme",
|
||||
"type": "module",
|
||||
"main": "./index.cjs",
|
||||
"module": "./index.js",
|
||||
"types": "./index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.js",
|
||||
"require": "./index.cjs",
|
||||
"types": "./index.d.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./*.js",
|
||||
"require": "./*.cjs",
|
||||
"types": "./*.d.ts"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c --configPlugin typescript",
|
||||
"build:debug": "rollup -c --configPlugin typescript",
|
||||
"npm-pack": "pnpm build && cd ./dist && npm pack",
|
||||
"npm-publish": "pnpm build && cd ./dist && pnpm publish --access public --loglevel silly --no-git-checks",
|
||||
"ts:check": "tsc --noEmit",
|
||||
"eslint:check": "eslint src/**/*.ts",
|
||||
"eslint:fix": "eslint src/**/*.ts --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@rollup/plugin-terser": "1.0.0",
|
||||
"@rollup/plugin-typescript": "12.3.0",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/node": "^24.11.0",
|
||||
"eslint": "^10.0.2",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-security": "4.0.1",
|
||||
"fast-glob": "3.3.3",
|
||||
"globals": "^17.4.0",
|
||||
"rollup": "4.62.4",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.58.2"
|
||||
}
|
||||
}
|
||||
117
packages/api/rollup.config.ts
Normal file
117
packages/api/rollup.config.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { defineConfig, Plugin, RollupLog } from 'rollup'
|
||||
import typescript from '@rollup/plugin-typescript'
|
||||
import terser from '@rollup/plugin-terser'
|
||||
import fg from 'fast-glob'
|
||||
import { basename, dirname, join } from 'path'
|
||||
import { copyFileSync, opendirSync, rmSync, Dir } from 'fs'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
// cleanup dist dir
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
cleanDir(join(__dirname, './dist'))
|
||||
|
||||
const modules = fg.sync(['!./src/*.d.ts', './src/*.ts'])
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
input: Object.fromEntries(modules.map((p) => [basename(p, '.ts'), p])),
|
||||
output: [
|
||||
{
|
||||
format: 'esm',
|
||||
dir: './dist',
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: 'src',
|
||||
entryFileNames: (chunkInfo) => {
|
||||
if (chunkInfo.name.includes('node_modules')) {
|
||||
return externalLibPath(chunkInfo.name) + '.js'
|
||||
}
|
||||
|
||||
return '[name].js'
|
||||
}
|
||||
},
|
||||
{
|
||||
format: 'cjs',
|
||||
dir: './dist',
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: 'src',
|
||||
entryFileNames: (chunkInfo) => {
|
||||
if (chunkInfo.name.includes('node_modules')) {
|
||||
return externalLibPath(chunkInfo.name) + '.cjs'
|
||||
}
|
||||
|
||||
return '[name].cjs'
|
||||
}
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
typescript({
|
||||
declaration: true,
|
||||
declarationDir: './dist',
|
||||
rootDir: 'src'
|
||||
}),
|
||||
makeFlatPackageInDist()
|
||||
],
|
||||
onwarn
|
||||
},
|
||||
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
format: 'iife',
|
||||
name: '__TAURI_IIFE__',
|
||||
footer: 'window.__TAURI__ = __TAURI_IIFE__',
|
||||
file: '../../crates/tauri/scripts/bundle.global.js'
|
||||
},
|
||||
plugins: [typescript(), terser()],
|
||||
onwarn
|
||||
}
|
||||
])
|
||||
|
||||
function externalLibPath(path: string) {
|
||||
return `external/${basename(dirname(path))}/${basename(path)}`
|
||||
}
|
||||
|
||||
function onwarn(warning: RollupLog) {
|
||||
// deny warnings by default
|
||||
throw Object.assign(new Error(), warning)
|
||||
}
|
||||
|
||||
function makeFlatPackageInDist(): Plugin {
|
||||
return {
|
||||
name: 'makeFlatPackageInDist',
|
||||
writeBundle() {
|
||||
// copy necessary files like `CHANGELOG.md` , `README.md` and Licenses to `./dist`
|
||||
fg.sync('(LICENSE*|*.md|package.json)').forEach((f) =>
|
||||
copyFileSync(f, `dist/${f}`)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanDir(path: string) {
|
||||
let dir: Dir
|
||||
try {
|
||||
dir = opendirSync(path)
|
||||
} catch (err: any) {
|
||||
switch (err.code) {
|
||||
case 'ENOENT':
|
||||
return // Noop when directory don't exists.
|
||||
case 'ENOTDIR':
|
||||
throw new Error(`'${path}' is not a directory.`)
|
||||
default:
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
let file = dir.readSync()
|
||||
while (file) {
|
||||
const filePath = join(path, file.name)
|
||||
rmSync(filePath, { recursive: true })
|
||||
file = dir.readSync()
|
||||
}
|
||||
dir.closeSync()
|
||||
}
|
||||
421
packages/api/src/app.ts
Normal file
421
packages/api/src/app.ts
Normal file
@@ -0,0 +1,421 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* Application metadata and lifecycle APIs: version and identifier, theme and dock
|
||||
* visibility, data stores and exiting the app.
|
||||
*
|
||||
* This package is also accessible with `window.__TAURI__.app` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`.
|
||||
*
|
||||
* @remarks Only the read-only commands are enabled by `core:app:default`
|
||||
* (`allow-version`, `allow-name`, `allow-tauri-version`, `allow-identifier`,
|
||||
* `allow-bundle-type`, `allow-supports-multiple-windows`, `allow-register-listener`
|
||||
* and `allow-remove-listener`). Every other function in this module documents the
|
||||
* permission it needs, which you must add to a capability yourself.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { addPluginListener, invoke, PluginListener } from './core'
|
||||
import { Image } from './image'
|
||||
import { Theme } from './window'
|
||||
|
||||
/**
|
||||
* Identifier type used for data stores on macOS and iOS.
|
||||
*
|
||||
* Represents a 128-bit identifier, commonly expressed as a 16-byte UUID.
|
||||
*
|
||||
* @since 2.4.0
|
||||
*/
|
||||
export type DataStoreIdentifier = [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number
|
||||
]
|
||||
|
||||
/**
|
||||
* Bundle type of the current application.
|
||||
*
|
||||
* @see {@linkcode getBundleType}
|
||||
*
|
||||
* @since 2.7.0
|
||||
*/
|
||||
export enum BundleType {
|
||||
/** Windows NSIS */
|
||||
Nsis = 'nsis',
|
||||
/** Windows MSI */
|
||||
Msi = 'msi',
|
||||
/** Linux Debian package */
|
||||
Deb = 'deb',
|
||||
/** Linux RPM */
|
||||
Rpm = 'rpm',
|
||||
/** Linux AppImage */
|
||||
AppImage = 'appimage',
|
||||
/** macOS app bundle */
|
||||
App = 'app'
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the application version.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { getVersion } from '@tauri-apps/api/app';
|
||||
* const appVersion = await getVersion();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function getVersion(): Promise<string> {
|
||||
return invoke('plugin:app|version')
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the application name.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { getName } from '@tauri-apps/api/app';
|
||||
* const appName = await getName();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function getName(): Promise<string> {
|
||||
return invoke('plugin:app|name')
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Tauri framework version used by this application.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { getTauriVersion } from '@tauri-apps/api/app';
|
||||
* const tauriVersion = await getTauriVersion();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function getTauriVersion(): Promise<string> {
|
||||
return invoke('plugin:app|tauri_version')
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the application identifier.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { getIdentifier } from '@tauri-apps/api/app';
|
||||
* const identifier = await getIdentifier();
|
||||
* ```
|
||||
*
|
||||
* @returns The application identifier as configured in `tauri.conf.json`.
|
||||
*
|
||||
* @since 2.4.0
|
||||
*/
|
||||
async function getIdentifier(): Promise<string> {
|
||||
return invoke('plugin:app|identifier')
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the application on macOS. This function does not automatically
|
||||
* focus any specific app window.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { show } from '@tauri-apps/api/app';
|
||||
* await show();
|
||||
* ```
|
||||
*
|
||||
* @remarks Requires the `core:app:allow-app-show` permission (not included in `core:app:default`).
|
||||
*
|
||||
* @since 1.2.0
|
||||
*/
|
||||
async function show(): Promise<void> {
|
||||
return invoke('plugin:app|app_show')
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the application on macOS.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { hide } from '@tauri-apps/api/app';
|
||||
* await hide();
|
||||
* ```
|
||||
*
|
||||
* @remarks Requires the `core:app:allow-app-hide` permission (not included in `core:app:default`).
|
||||
*
|
||||
* @since 1.2.0
|
||||
*/
|
||||
async function hide(): Promise<void> {
|
||||
return invoke('plugin:app|app_hide')
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the data store identifiers on macOS and iOS.
|
||||
*
|
||||
* See https://developer.apple.com/documentation/webkit/wkwebsitedatastore for more information.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { fetchDataStoreIdentifiers } from '@tauri-apps/api/app';
|
||||
* const ids = await fetchDataStoreIdentifiers();
|
||||
* ```
|
||||
*
|
||||
* @remarks Requires the `core:app:allow-fetch-data-store-identifiers` permission
|
||||
* (not included in `core:app:default`).
|
||||
*
|
||||
* @since 2.4.0
|
||||
*/
|
||||
async function fetchDataStoreIdentifiers(): Promise<DataStoreIdentifier[]> {
|
||||
return invoke('plugin:app|fetch_data_store_identifiers')
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the data store with the given identifier.
|
||||
*
|
||||
* Note that any webview using this data store should be closed before running this API.
|
||||
*
|
||||
* See https://developer.apple.com/documentation/webkit/wkwebsitedatastore for more information.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { fetchDataStoreIdentifiers, removeDataStore } from '@tauri-apps/api/app';
|
||||
* for (const id of (await fetchDataStoreIdentifiers())) {
|
||||
* await removeDataStore(id);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @remarks Requires the `core:app:allow-remove-data-store` permission (not included
|
||||
* in `core:app:default`).
|
||||
*
|
||||
* @since 2.4.0
|
||||
*/
|
||||
async function removeDataStore(uuid: DataStoreIdentifier): Promise<void> {
|
||||
return invoke('plugin:app|remove_data_store', { uuid })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default window icon.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { defaultWindowIcon } from '@tauri-apps/api/app';
|
||||
* const icon = await defaultWindowIcon();
|
||||
* ```
|
||||
*
|
||||
* @remarks Requires the `core:app:allow-default-window-icon` permission (not
|
||||
* included in `core:app:default`).
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
async function defaultWindowIcon(): Promise<Image | null> {
|
||||
return invoke<number | null>('plugin:app|default_window_icon').then((rid) =>
|
||||
rid ? new Image(rid) : null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the application's theme. Pass in `null` or `undefined` to follow
|
||||
* the system theme.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { setTheme } from '@tauri-apps/api/app';
|
||||
* await setTheme('dark');
|
||||
* ```
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **iOS / Android:** Unsupported.
|
||||
*
|
||||
* @remarks Requires the `core:app:allow-set-app-theme` permission (not included in
|
||||
* `core:app:default`).
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function setTheme(theme?: Theme | null): Promise<void> {
|
||||
return invoke('plugin:app|set_app_theme', { theme })
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dock visibility for the application on macOS.
|
||||
*
|
||||
* @param visible - Whether the dock should be visible or not.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { setDockVisibility } from '@tauri-apps/api/app';
|
||||
* await setDockVisibility(false);
|
||||
* ```
|
||||
*
|
||||
* @remarks Requires the `core:app:allow-set-dock-visibility` permission (not
|
||||
* included in `core:app:default`).
|
||||
*
|
||||
* @since 2.5.0
|
||||
*/
|
||||
async function setDockVisibility(visible: boolean): Promise<void> {
|
||||
return invoke('plugin:app|set_dock_visibility', { visible })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the application bundle type.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { getBundleType } from '@tauri-apps/api/app';
|
||||
* const type = await getBundleType();
|
||||
* ```
|
||||
*
|
||||
* @since 2.5.0
|
||||
*/
|
||||
async function getBundleType(): Promise<BundleType> {
|
||||
return invoke('plugin:app|bundle_type')
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for the onBackButtonPress event.
|
||||
*
|
||||
* @since 2.9.0
|
||||
*/
|
||||
type OnBackButtonPressPayload = {
|
||||
/** Whether the webview canGoBack property is true. */
|
||||
canGoBack: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to the Android hardware/gesture back button.
|
||||
*
|
||||
* Registering a handler takes over the default behavior, so the app no longer
|
||||
* navigates back or closes on its own: decide what to do inside the handler,
|
||||
* using `payload.canGoBack` to know whether the webview has history to go back to.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** Supported.
|
||||
* - **Windows / Linux / macOS / iOS:** Unsupported, the handler is never called.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { onBackButtonPress } from '@tauri-apps/api/app';
|
||||
* import { exit } from '@tauri-apps/api/app';
|
||||
*
|
||||
* const listener = await onBackButtonPress(({ canGoBack }) => {
|
||||
* if (canGoBack) {
|
||||
* window.history.back();
|
||||
* } else {
|
||||
* void exit(0);
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // stop handling the back button
|
||||
* await listener.unregister();
|
||||
* ```
|
||||
*
|
||||
* @param handler Called on every back button press.
|
||||
* @returns A listener handle, call `unregister()` on it to restore the default behavior.
|
||||
*
|
||||
* @since 2.9.0
|
||||
*/
|
||||
async function onBackButtonPress(
|
||||
handler: (payload: OnBackButtonPressPayload) => void
|
||||
): Promise<PluginListener> {
|
||||
return addPluginListener<OnBackButtonPressPayload>(
|
||||
'app',
|
||||
'back-button',
|
||||
handler
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the current platform can show more than one window at a time.
|
||||
*
|
||||
* Use it to hide or disable multi-window features on platforms where creating a
|
||||
* second window is not possible.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Windows / Linux / macOS:** Always `true`.
|
||||
* - **Android:** `true` on API level 32 (Android 12L) and above.
|
||||
* - **iOS:** Reflects [`UIApplication.supportsMultipleScenes`](https://developer.apple.com/documentation/uikit/uiapplication/supportsmultiplescenes),
|
||||
* so it is `true` on iPadOS and `false` on iPhone.
|
||||
*
|
||||
* See the [mobile multiwindow guide](https://tauri.app/learn/mobile-multiwindow/)
|
||||
* for how windows behave on mobile.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { supportsMultipleWindows } from '@tauri-apps/api/app';
|
||||
* import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
*
|
||||
* if (await supportsMultipleWindows()) {
|
||||
* new WebviewWindow('settings', { url: '/settings' });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @since 2.11.0
|
||||
*/
|
||||
async function supportsMultipleWindows(): Promise<boolean> {
|
||||
return invoke('plugin:app|supports_multiple_windows')
|
||||
}
|
||||
|
||||
/**
|
||||
* Exits the app with the given exit code.
|
||||
*
|
||||
* This is the same as the `exit` function of the `@tauri-apps/plugin-process` plugin,
|
||||
* but does not require a plugin to be installed.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android**: The activity is finished instead of the process being killed,
|
||||
* so the app closes with the system transition; `code` is ignored.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { exit } from '@tauri-apps/api/app';
|
||||
* await exit(1);
|
||||
* ```
|
||||
*
|
||||
* @param code The exit code to use. Defaults to `0`.
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @remarks Requires the `core:app:allow-exit` permission (not included in
|
||||
* `core:app:default`).
|
||||
*
|
||||
* @since 2.12.0
|
||||
*/
|
||||
async function exit(code = 0): Promise<void> {
|
||||
return invoke('plugin:app|exit', { code })
|
||||
}
|
||||
|
||||
export {
|
||||
getName,
|
||||
getVersion,
|
||||
getTauriVersion,
|
||||
getIdentifier,
|
||||
show,
|
||||
hide,
|
||||
defaultWindowIcon,
|
||||
setTheme,
|
||||
fetchDataStoreIdentifiers,
|
||||
removeDataStore,
|
||||
setDockVisibility,
|
||||
getBundleType,
|
||||
type OnBackButtonPressPayload,
|
||||
onBackButtonPress,
|
||||
supportsMultipleWindows,
|
||||
exit
|
||||
}
|
||||
610
packages/api/src/core.ts
Normal file
610
packages/api/src/core.ts
Normal file
@@ -0,0 +1,610 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* Invoke your custom commands.
|
||||
*
|
||||
* This package is also accessible with `window.__TAURI__.core` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
/**
|
||||
* A key to be used to implement a special function
|
||||
* on your types that define how your type should be serialized
|
||||
* when passing across the IPC.
|
||||
* @example
|
||||
* Given a type in Rust that looks like this
|
||||
* ```rs
|
||||
* #[derive(serde::Serialize, serde::Deserialize)
|
||||
* enum UserId {
|
||||
* String(String),
|
||||
* Number(u32),
|
||||
* }
|
||||
* ```
|
||||
* `UserId::String("id")` would be serialized into `{ String: "id" }`
|
||||
* and so we need to pass the same structure back to Rust
|
||||
* ```ts
|
||||
* import { SERIALIZE_TO_IPC_FN } from "@tauri-apps/api/core"
|
||||
*
|
||||
* class UserIdString {
|
||||
* id
|
||||
* constructor(id) {
|
||||
* this.id = id
|
||||
* }
|
||||
*
|
||||
* [SERIALIZE_TO_IPC_FN]() {
|
||||
* return { String: this.id }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* class UserIdNumber {
|
||||
* id
|
||||
* constructor(id) {
|
||||
* this.id = id
|
||||
* }
|
||||
*
|
||||
* [SERIALIZE_TO_IPC_FN]() {
|
||||
* return { Number: this.id }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* type UserId = UserIdString | UserIdNumber
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
// if this value changes, make sure to update it in:
|
||||
// 1. ipc.js
|
||||
// 2. process-ipc-message-fn.js
|
||||
export const SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__'
|
||||
|
||||
/**
|
||||
* Stores the callback in a known location, and returns an identifier that can be passed to the backend.
|
||||
* The backend uses the identifier to `eval()` the callback.
|
||||
*
|
||||
* @return An unique identifier associated with the callback function.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
function transformCallback<T = unknown>(
|
||||
// TODO: Make this not optional in v3
|
||||
callback?: (response: T) => void,
|
||||
once = false
|
||||
): number {
|
||||
return window.__TAURI_INTERNALS__.transformCallback(callback, once)
|
||||
}
|
||||
|
||||
/**
|
||||
* A message channel used to stream values from Rust to the frontend.
|
||||
*
|
||||
* A `Channel` is the JavaScript counterpart of [`tauri::ipc::Channel`](https://docs.rs/tauri/2/tauri/ipc/struct.Channel.html).
|
||||
* Pass an instance as a command argument and the Rust command can declare a
|
||||
* `tauri::ipc::Channel<T>` parameter with the same name; every value sent from
|
||||
* Rust is then delivered to {@linkcode Channel.onmessage}. Messages are delivered
|
||||
* in the order they were sent, even when they arrive out of order.
|
||||
*
|
||||
* The channel stays alive until the Rust `Channel` is dropped, so it can be used
|
||||
* for long-running work such as download progress or log streaming.
|
||||
*
|
||||
* Raw byte payloads are delivered to the frontend as an `ArrayBuffer` rather than
|
||||
* a JSON value, so declare the channel as `Channel<ArrayBuffer>` when the Rust
|
||||
* side sends `tauri::ipc::InvokeResponseBody::Raw` — for example through a
|
||||
* `Channel<tauri::ipc::Response>` whose values are built with
|
||||
* `tauri::ipc::Response::new(bytes)`.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Channel, invoke } from '@tauri-apps/api/core';
|
||||
*
|
||||
* const onEvent = new Channel<string>();
|
||||
* onEvent.onmessage = (message) => {
|
||||
* console.log(`got download event ${message}`);
|
||||
* };
|
||||
*
|
||||
* await invoke('download', { url: 'https://tauri.app', onEvent });
|
||||
* ```
|
||||
*
|
||||
* The matching Rust command:
|
||||
* ```rust
|
||||
* #[tauri::command]
|
||||
* async fn download(url: String, on_event: tauri::ipc::Channel<String>) -> tauri::Result<()> {
|
||||
* on_event.send("started".to_string())?;
|
||||
* on_event.send("finished".to_string())?;
|
||||
* Ok(())
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class Channel<T = unknown> {
|
||||
/**
|
||||
* The callback id returned from {@linkcode transformCallback}.
|
||||
*
|
||||
* This is the value sent across the IPC, it is used by the Rust side to
|
||||
* address this channel and should not be changed.
|
||||
*/
|
||||
id: number
|
||||
#onmessage: (response: T) => void
|
||||
|
||||
// the index is used as a mechanism to preserve message order
|
||||
#nextMessageIndex = 0
|
||||
#pendingMessages: T[] = []
|
||||
#messageEndIndex: number | undefined
|
||||
|
||||
constructor(onmessage?: (response: T) => void) {
|
||||
this.#onmessage = onmessage || (() => {})
|
||||
|
||||
this.id = transformCallback<
|
||||
// Normal message
|
||||
| { message: T; index: number }
|
||||
// Message when the channel gets dropped in the rust side
|
||||
| { end: true; index: number }
|
||||
>((rawMessage) => {
|
||||
const index = rawMessage.index
|
||||
|
||||
if ('end' in rawMessage) {
|
||||
if (index == this.#nextMessageIndex) {
|
||||
this.cleanupCallback()
|
||||
} else {
|
||||
this.#messageEndIndex = index
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const message = rawMessage.message
|
||||
// Process the message if we're at the right order
|
||||
if (index == this.#nextMessageIndex) {
|
||||
this.#onmessage(message)
|
||||
this.#nextMessageIndex += 1
|
||||
|
||||
// process pending messages
|
||||
while (this.#nextMessageIndex in this.#pendingMessages) {
|
||||
const message = this.#pendingMessages[this.#nextMessageIndex]
|
||||
this.#onmessage(message)
|
||||
// eslint-disable-next-line @typescript-eslint/no-array-delete
|
||||
delete this.#pendingMessages[this.#nextMessageIndex]
|
||||
this.#nextMessageIndex += 1
|
||||
}
|
||||
|
||||
if (this.#nextMessageIndex === this.#messageEndIndex) {
|
||||
this.cleanupCallback()
|
||||
}
|
||||
}
|
||||
// Queue the message if we're not
|
||||
else {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
this.#pendingMessages[index] = message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private cleanupCallback() {
|
||||
window.__TAURI_INTERNALS__.unregisterCallback(this.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler called for every message sent by the Rust side of this channel.
|
||||
*
|
||||
* Assigning a new handler replaces the previous one; messages that arrived
|
||||
* before a handler was set are not replayed, so set it (or pass it to the
|
||||
* constructor) before sending the channel to the backend.
|
||||
*/
|
||||
set onmessage(handler: (response: T) => void) {
|
||||
this.#onmessage = handler
|
||||
}
|
||||
|
||||
get onmessage(): (response: T) => void {
|
||||
return this.#onmessage
|
||||
}
|
||||
|
||||
[SERIALIZE_TO_IPC_FN]() {
|
||||
return `__CHANNEL__:${this.id}`
|
||||
}
|
||||
|
||||
toJSON(): string {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
return this[SERIALIZE_TO_IPC_FN]()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A handle to a listener registered with {@linkcode addPluginListener}.
|
||||
*
|
||||
* Keep the returned instance around and call {@linkcode PluginListener.unregister}
|
||||
* when the listener goes out of scope, otherwise the plugin keeps sending events
|
||||
* to a handler nothing uses anymore.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class PluginListener {
|
||||
/** The plugin name the listener is registered to. */
|
||||
plugin: string
|
||||
/** The plugin event name the listener is registered to. */
|
||||
event: string
|
||||
/** The id of the {@linkcode Channel} backing this listener. */
|
||||
channelId: number
|
||||
|
||||
constructor(plugin: string, event: string, channelId: number) {
|
||||
this.plugin = plugin
|
||||
this.event = event
|
||||
this.channelId = channelId
|
||||
}
|
||||
|
||||
/** Removes this listener from the plugin, so its handler stops being called. */
|
||||
async unregister(): Promise<void> {
|
||||
return invoke(`plugin:${this.plugin}|remove_listener`, {
|
||||
event: this.event,
|
||||
channelId: this.channelId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to a plugin event.
|
||||
*
|
||||
* This is meant to be used by plugin authors to wrap the `register_listener`
|
||||
* command their mobile plugin implements; application code normally calls the
|
||||
* wrapper the plugin exposes instead of this function.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { addPluginListener } from '@tauri-apps/api/core';
|
||||
*
|
||||
* interface ScanEvent {
|
||||
* value: string
|
||||
* }
|
||||
*
|
||||
* const listener = await addPluginListener<ScanEvent>(
|
||||
* 'barcode-scanner',
|
||||
* 'scan',
|
||||
* (payload) => console.log('scanned', payload.value)
|
||||
* );
|
||||
*
|
||||
* // stop listening when the scanner screen is closed
|
||||
* await listener.unregister();
|
||||
* ```
|
||||
*
|
||||
* @param plugin The plugin name, as used in the `plugin:<name>|<command>` IPC command format.
|
||||
* @param event The plugin event name.
|
||||
* @param cb The callback executed for each event payload.
|
||||
* @returns The listener object to stop listening to the events.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function addPluginListener<T>(
|
||||
plugin: string,
|
||||
event: string,
|
||||
cb: (payload: T) => void
|
||||
): Promise<PluginListener> {
|
||||
const handler = new Channel<T>(cb)
|
||||
try {
|
||||
await invoke(`plugin:${plugin}|register_listener`, {
|
||||
event,
|
||||
handler
|
||||
})
|
||||
return new PluginListener(plugin, event, handler.id)
|
||||
} catch {
|
||||
// TODO(v3): remove this fallback
|
||||
// note: we must try with camelCase here for backwards compatibility
|
||||
await invoke(`plugin:${plugin}|registerListener`, { event, handler })
|
||||
return new PluginListener(plugin, event, handler.id)
|
||||
}
|
||||
}
|
||||
|
||||
type PermissionState = 'granted' | 'denied' | 'prompt' | 'prompt-with-rationale'
|
||||
|
||||
/**
|
||||
* Get permission state for a plugin.
|
||||
*
|
||||
* This should be used by plugin authors to wrap their actual implementation,
|
||||
* it calls the `plugin:<name>|check_permissions` command implemented by the
|
||||
* mobile plugin and returns its permission status object without prompting
|
||||
* the user.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { checkPermissions, type PermissionState } from '@tauri-apps/api/core';
|
||||
*
|
||||
* interface Permissions {
|
||||
* camera: PermissionState
|
||||
* }
|
||||
*
|
||||
* const status = await checkPermissions<Permissions>('barcode-scanner');
|
||||
* if (status.camera === 'prompt') {
|
||||
* // ask the user, see requestPermissions
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param plugin The plugin name, as used in the `plugin:<name>|<command>` IPC command format.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function checkPermissions<T>(plugin: string): Promise<T> {
|
||||
return invoke(`plugin:${plugin}|check_permissions`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Request permissions.
|
||||
*
|
||||
* This should be used by plugin authors to wrap their actual implementation,
|
||||
* it calls the `plugin:<name>|request_permissions` command implemented by the
|
||||
* mobile plugin, which shows the native permission prompt when needed, and
|
||||
* returns the resulting permission status object.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { requestPermissions, type PermissionState } from '@tauri-apps/api/core';
|
||||
*
|
||||
* interface Permissions {
|
||||
* camera: PermissionState
|
||||
* }
|
||||
*
|
||||
* const status = await requestPermissions<Permissions>('barcode-scanner');
|
||||
* if (status.camera !== 'granted') {
|
||||
* throw new Error('camera permission denied');
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param plugin The plugin name, as used in the `plugin:<name>|<command>` IPC command format.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function requestPermissions<T>(plugin: string): Promise<T> {
|
||||
return invoke(`plugin:${plugin}|request_permissions`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Command arguments.
|
||||
*
|
||||
* An object is the usual form: each key becomes a command parameter, serialized
|
||||
* as JSON (`application/octet-stream` is used instead when a value implements
|
||||
* {@linkcode SERIALIZE_TO_IPC_FN}, see its documentation).
|
||||
*
|
||||
* The `ArrayBuffer`, `Uint8Array` and `number[]` variants are a *raw payload*:
|
||||
* the whole request body is sent as `application/octet-stream` instead of JSON,
|
||||
* which avoids the cost of base64/array encoding for large binary data. The
|
||||
* command then has a single [`tauri::ipc::Request`](https://docs.rs/tauri/2/tauri/ipc/struct.Request.html)
|
||||
* parameter and reads the bytes from it:
|
||||
*
|
||||
* ```rust
|
||||
* #[tauri::command]
|
||||
* fn upload(request: tauri::ipc::Request<'_>) -> Result<(), String> {
|
||||
* let tauri::ipc::InvokeBody::Raw(bytes) = request.body() else {
|
||||
* return Err("expected a raw body".into());
|
||||
* };
|
||||
* println!("got {} bytes", bytes.len());
|
||||
* Ok(())
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ```typescript
|
||||
* import { invoke } from '@tauri-apps/api/core';
|
||||
* await invoke('upload', new Uint8Array([1, 2, 3]));
|
||||
* ```
|
||||
*
|
||||
* A command that returns [`tauri::ipc::Response`](https://docs.rs/tauri/2/tauri/ipc/struct.Response.html)
|
||||
* likewise resolves to an `ArrayBuffer` on the frontend instead of a JSON value.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
type InvokeArgs = Record<string, unknown> | number[] | ArrayBuffer | Uint8Array
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
interface InvokeOptions {
|
||||
/**
|
||||
* Headers to send along with the IPC request.
|
||||
*
|
||||
* They are readable on the Rust side through
|
||||
* [`tauri::ipc::Request::headers`](https://docs.rs/tauri/2/tauri/ipc/struct.Request.html#method.headers)
|
||||
* and are useful to pass metadata (such as a content type for a raw payload)
|
||||
* without adding it to the command arguments.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { invoke } from '@tauri-apps/api/core';
|
||||
* await invoke('upload', new Uint8Array([1, 2, 3]), {
|
||||
* headers: { 'x-file-name': 'image.png' }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
headers: HeadersInit
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the backend.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { invoke } from '@tauri-apps/api/core';
|
||||
* await invoke('login', { user: 'tauri', password: 'poiwe3h4r5ip3yrhtew9ty' });
|
||||
* ```
|
||||
*
|
||||
* @param cmd The command name.
|
||||
* @param args The optional arguments to pass to the command.
|
||||
* @param options The request options.
|
||||
* @return A promise resolving or rejecting to the backend response.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function invoke<T>(
|
||||
cmd: string,
|
||||
args: InvokeArgs = {},
|
||||
options?: InvokeOptions
|
||||
): Promise<T> {
|
||||
return window.__TAURI_INTERNALS__.invoke(cmd, args, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a device file path to a URL that can be loaded by the webview.
|
||||
*
|
||||
* The asset protocol must be enabled and the files you want to expose must be
|
||||
* included in its scope. The protocol origins must also be allowed by the
|
||||
* relevant [`app.security.csp`](https://v2.tauri.app/reference/config/#csp-1)
|
||||
* directive. For example, this configuration allows images from the user's
|
||||
* downloads directory:
|
||||
*
|
||||
* ```json
|
||||
* {
|
||||
* "app": {
|
||||
* "security": {
|
||||
* "assetProtocol": {
|
||||
* "enable": true,
|
||||
* "scope": ["$DOWNLOAD/**"]
|
||||
* },
|
||||
* "csp": "default-src 'self'; img-src 'self' asset: http://asset.localhost"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* See [`assetProtocol`](https://v2.tauri.app/reference/config/#assetprotocolconfig)
|
||||
* for the available scope variables and platform-specific protocol details.
|
||||
*
|
||||
* @param filePath The file path.
|
||||
* @param protocol The protocol to use. Defaults to `asset`. You only need to set this when using a custom protocol.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { downloadDir, join } from '@tauri-apps/api/path';
|
||||
* import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
* const downloads = await downloadDir();
|
||||
* const filePath = await join(downloads, 'photo.png');
|
||||
* const assetUrl = convertFileSrc(filePath);
|
||||
*
|
||||
* const image = document.getElementById('my-image') as HTMLImageElement;
|
||||
* image.src = assetUrl;
|
||||
* ```
|
||||
*
|
||||
* @return the URL that can be used as source on the webview.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
function convertFileSrc(filePath: string, protocol = 'asset'): string {
|
||||
return window.__TAURI_INTERNALS__.convertFileSrc(filePath, protocol)
|
||||
}
|
||||
|
||||
/**
|
||||
* A rust-backed resource stored through `tauri::Manager::resources_table` API.
|
||||
*
|
||||
* The resource lives in the main process and does not exist
|
||||
* in the Javascript world, and thus will not be cleaned up automatically
|
||||
* except on application exit. If you want to clean it up early, call {@linkcode Resource.close} or use [Explicit Resource Management].
|
||||
*
|
||||
* Several API types are resources and inherit this behavior, among them `Menu`,
|
||||
* `TrayIcon`, `Image`, `Webview` and the menu item classes.
|
||||
*
|
||||
* To support older browsers with [Explicit Resource Management], use a supported compiler (e.g. tsc) or bundler (e.g. rollup).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Resource, invoke } from '@tauri-apps/api/core';
|
||||
*
|
||||
* export class DatabaseHandle extends Resource {
|
||||
* static async open(path: string): Promise<DatabaseHandle> {
|
||||
* const rid: number = await invoke('open_db', { path });
|
||||
* return new DatabaseHandle(rid);
|
||||
* }
|
||||
*
|
||||
* async execute(sql: string): Promise<void> {
|
||||
* await invoke('execute_sql', { rid: this.rid, sql });
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Only asynchronous disposal is implemented (`Symbol.asyncDispose`), because closing
|
||||
* a resource is an IPC call. Use `await using`; the synchronous `using` form does
|
||||
* **not** work with resources.
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* await using db = await DatabaseHandle.open('test.db');
|
||||
* await db.execute('SELECT *');
|
||||
* // `db` is closed here, by awaiting `db[Symbol.asyncDispose]()`
|
||||
* ```
|
||||
*
|
||||
* To support older browsers, add the following to the globals (e.g. adding to the HTML file):
|
||||
*
|
||||
* ```javascript
|
||||
* Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose");
|
||||
* ```
|
||||
*
|
||||
* And for the compiler, for example `tsc`, `rollup`, `vite`, add the following to `tsconfig.json`:
|
||||
*
|
||||
* ```json
|
||||
* {
|
||||
* "compilerOptions": {
|
||||
* "target": "es2022",
|
||||
* "lib": ["es2022", "esnext.disposable", "dom"]
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* [Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management
|
||||
*/
|
||||
export class Resource {
|
||||
readonly #rid: number
|
||||
|
||||
get rid(): number {
|
||||
return this.#rid
|
||||
}
|
||||
|
||||
constructor(rid: number) {
|
||||
this.#rid = rid
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys and cleans up this resource from memory.
|
||||
* **You should not call any method on this object anymore and should drop any reference to it.**
|
||||
*
|
||||
* @remarks Uses the `core:resources:allow-close` permission, which is part of
|
||||
* the `core:resources:default` permission set enabled by default.
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
return invoke('plugin:resources|close', {
|
||||
rid: this.rid
|
||||
})
|
||||
}
|
||||
|
||||
async [Symbol.asyncDispose]() {
|
||||
await this.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the code is running inside a Tauri webview.
|
||||
*
|
||||
* Useful for frontends that are also served on the web, to guard calls to APIs
|
||||
* that only exist inside the application.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isTauri } from '@tauri-apps/api/core';
|
||||
* import { getVersion } from '@tauri-apps/api/app';
|
||||
*
|
||||
* const version = isTauri() ? await getVersion() : 'web';
|
||||
* ```
|
||||
*
|
||||
* @returns `true` when running inside a Tauri app, `false` otherwise (e.g. in a
|
||||
* browser or in unit tests).
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
function isTauri(): boolean {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||
return !!((globalThis as any) || window).isTauri
|
||||
}
|
||||
|
||||
export type { InvokeArgs, InvokeOptions }
|
||||
|
||||
export {
|
||||
transformCallback,
|
||||
Channel,
|
||||
PluginListener,
|
||||
addPluginListener,
|
||||
PermissionState,
|
||||
checkPermissions,
|
||||
requestPermissions,
|
||||
invoke,
|
||||
convertFileSrc,
|
||||
isTauri
|
||||
}
|
||||
476
packages/api/src/dpi.ts
Normal file
476
packages/api/src/dpi.ts
Normal file
@@ -0,0 +1,476 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* Size and position types for working with logical and physical pixels.
|
||||
*
|
||||
* Window and webview APIs report sizes and positions in **physical pixels** (actual
|
||||
* screen pixels), while most browser APIs and the window creation options use
|
||||
* **logical pixels** (physical pixels divided by the monitor scale factor). The
|
||||
* classes in this module make the unit explicit and convert between the two.
|
||||
*
|
||||
* ```typescript
|
||||
* import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
*
|
||||
* const appWindow = getCurrentWindow();
|
||||
* const size = await appWindow.innerSize(); // PhysicalSize
|
||||
* const logical = size.toLogical(await appWindow.scaleFactor());
|
||||
* ```
|
||||
*
|
||||
* They also serialize into the shape `tauri::Size` / `tauri::Position` expect, so
|
||||
* they can be passed straight to your own commands, see {@linkcode Size} and
|
||||
* {@linkcode Position}.
|
||||
*
|
||||
* This package is also accessible with `window.__TAURI__.dpi` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { SERIALIZE_TO_IPC_FN } from './core'
|
||||
|
||||
/**
|
||||
* A size represented in logical pixels.
|
||||
* Logical pixels are scaled according to the window's DPI scale.
|
||||
* Most browser APIs (i.e. `MouseEvent`'s `clientX`) will return logical pixels.
|
||||
*
|
||||
* For logical-pixel-based position, see {@linkcode LogicalPosition}.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class LogicalSize {
|
||||
readonly type = 'Logical'
|
||||
width: number
|
||||
height: number
|
||||
|
||||
constructor(width: number, height: number)
|
||||
constructor(object: { Logical: { width: number; height: number } })
|
||||
constructor(object: { width: number; height: number })
|
||||
constructor(
|
||||
...args:
|
||||
| [number, number]
|
||||
| [{ width: number; height: number }]
|
||||
| [{ Logical: { width: number; height: number } }]
|
||||
) {
|
||||
if (args.length === 1) {
|
||||
if ('Logical' in args[0]) {
|
||||
this.width = args[0].Logical.width
|
||||
this.height = args[0].Logical.height
|
||||
} else {
|
||||
this.width = args[0].width
|
||||
this.height = args[0].height
|
||||
}
|
||||
} else {
|
||||
this.width = args[0]
|
||||
this.height = args[1]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the logical size to a physical one.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { LogicalSize } from '@tauri-apps/api/dpi';
|
||||
* import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
*
|
||||
* const appWindow = getCurrentWindow();
|
||||
* const factor = await appWindow.scaleFactor();
|
||||
* const size = new LogicalSize(400, 500);
|
||||
* const physical = size.toPhysical(factor);
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
toPhysical(scaleFactor: number): PhysicalSize {
|
||||
return new PhysicalSize(this.width * scaleFactor, this.height * scaleFactor)
|
||||
}
|
||||
|
||||
[SERIALIZE_TO_IPC_FN]() {
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
return this[SERIALIZE_TO_IPC_FN]()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A size represented in physical pixels.
|
||||
*
|
||||
* Physical pixels represent actual screen pixels, and are DPI-independent.
|
||||
* For high-DPI windows, this means that any point in the window on the screen
|
||||
* will have a different position in logical pixels {@linkcode LogicalSize}.
|
||||
*
|
||||
* For physical-pixel-based position, see {@linkcode PhysicalPosition}.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class PhysicalSize {
|
||||
readonly type = 'Physical'
|
||||
width: number
|
||||
height: number
|
||||
|
||||
constructor(width: number, height: number)
|
||||
constructor(object: { Physical: { width: number; height: number } })
|
||||
constructor(object: { width: number; height: number })
|
||||
constructor(
|
||||
...args:
|
||||
| [number, number]
|
||||
| [{ width: number; height: number }]
|
||||
| [{ Physical: { width: number; height: number } }]
|
||||
) {
|
||||
if (args.length === 1) {
|
||||
if ('Physical' in args[0]) {
|
||||
this.width = args[0].Physical.width
|
||||
this.height = args[0].Physical.height
|
||||
} else {
|
||||
this.width = args[0].width
|
||||
this.height = args[0].height
|
||||
}
|
||||
} else {
|
||||
this.width = args[0]
|
||||
this.height = args[1]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the physical size to a logical one.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
* const appWindow = getCurrentWindow();
|
||||
* const factor = await appWindow.scaleFactor();
|
||||
* const size = await appWindow.innerSize(); // PhysicalSize
|
||||
* const logical = size.toLogical(factor);
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
toLogical(scaleFactor: number): LogicalSize {
|
||||
return new LogicalSize(this.width / scaleFactor, this.height / scaleFactor)
|
||||
}
|
||||
|
||||
[SERIALIZE_TO_IPC_FN]() {
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
return this[SERIALIZE_TO_IPC_FN]()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A size represented either in physical or in logical pixels.
|
||||
*
|
||||
* This type is basically a union type of {@linkcode LogicalSize} and {@linkcode PhysicalSize}
|
||||
* but comes in handy when using `tauri::Size` in Rust as an argument to a command, as this class
|
||||
* automatically serializes into a valid format so it can be deserialized correctly into `tauri::Size`
|
||||
*
|
||||
* So instead of
|
||||
* ```typescript
|
||||
* import { invoke } from '@tauri-apps/api/core';
|
||||
* import { LogicalSize, PhysicalSize } from '@tauri-apps/api/dpi';
|
||||
*
|
||||
* const size: LogicalSize | PhysicalSize = someFunction(); // where someFunction returns either LogicalSize or PhysicalSize
|
||||
* const validSize = size instanceof LogicalSize
|
||||
* ? { Logical: { width: size.width, height: size.height } }
|
||||
* : { Physical: { width: size.width, height: size.height } }
|
||||
* await invoke("do_something_with_size", { size: validSize });
|
||||
* ```
|
||||
*
|
||||
* You can just use {@linkcode Size}
|
||||
* ```typescript
|
||||
* import { invoke } from '@tauri-apps/api/core';
|
||||
* import { LogicalSize, PhysicalSize, Size } from '@tauri-apps/api/dpi';
|
||||
*
|
||||
* const size: LogicalSize | PhysicalSize = someFunction(); // where someFunction returns either LogicalSize or PhysicalSize
|
||||
* const validSize = new Size(size);
|
||||
* await invoke("do_something_with_size", { size: validSize });
|
||||
* ```
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
class Size {
|
||||
size: LogicalSize | PhysicalSize
|
||||
|
||||
constructor(size: LogicalSize | PhysicalSize) {
|
||||
this.size = size
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the wrapped size to a logical one.
|
||||
*
|
||||
* Returns the inner value untouched when it already is a {@linkcode LogicalSize}.
|
||||
*
|
||||
* @param scaleFactor The monitor scale factor, e.g. `await appWindow.scaleFactor()`.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
toLogical(scaleFactor: number): LogicalSize {
|
||||
return this.size instanceof LogicalSize
|
||||
? this.size
|
||||
: this.size.toLogical(scaleFactor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the wrapped size to a physical one.
|
||||
*
|
||||
* Returns the inner value untouched when it already is a {@linkcode PhysicalSize}.
|
||||
*
|
||||
* @param scaleFactor The monitor scale factor, e.g. `await appWindow.scaleFactor()`.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
toPhysical(scaleFactor: number): PhysicalSize {
|
||||
return this.size instanceof PhysicalSize
|
||||
? this.size
|
||||
: this.size.toPhysical(scaleFactor)
|
||||
}
|
||||
|
||||
[SERIALIZE_TO_IPC_FN]() {
|
||||
return {
|
||||
[`${this.size.type}`]: {
|
||||
width: this.size.width,
|
||||
height: this.size.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
return this[SERIALIZE_TO_IPC_FN]()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A position represented in logical pixels.
|
||||
* For an explanation of what logical pixels are, see description of {@linkcode LogicalSize}.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class LogicalPosition {
|
||||
readonly type = 'Logical'
|
||||
x: number
|
||||
y: number
|
||||
|
||||
constructor(x: number, y: number)
|
||||
constructor(object: { Logical: { x: number; y: number } })
|
||||
constructor(object: { x: number; y: number })
|
||||
constructor(
|
||||
...args:
|
||||
| [number, number]
|
||||
| [{ x: number; y: number }]
|
||||
| [{ Logical: { x: number; y: number } }]
|
||||
) {
|
||||
if (args.length === 1) {
|
||||
if ('Logical' in args[0]) {
|
||||
this.x = args[0].Logical.x
|
||||
this.y = args[0].Logical.y
|
||||
} else {
|
||||
this.x = args[0].x
|
||||
this.y = args[0].y
|
||||
}
|
||||
} else {
|
||||
this.x = args[0]
|
||||
this.y = args[1]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the logical position to a physical one.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { LogicalPosition } from '@tauri-apps/api/dpi';
|
||||
* import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
*
|
||||
* const appWindow = getCurrentWindow();
|
||||
* const factor = await appWindow.scaleFactor();
|
||||
* const position = new LogicalPosition(400, 500);
|
||||
* const physical = position.toPhysical(factor);
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
toPhysical(scaleFactor: number): PhysicalPosition {
|
||||
return new PhysicalPosition(this.x * scaleFactor, this.y * scaleFactor)
|
||||
}
|
||||
|
||||
[SERIALIZE_TO_IPC_FN]() {
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
return this[SERIALIZE_TO_IPC_FN]()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A position represented in physical pixels.
|
||||
*
|
||||
* For an explanation of what physical pixels are, see description of {@linkcode PhysicalSize}.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class PhysicalPosition {
|
||||
readonly type = 'Physical'
|
||||
x: number
|
||||
y: number
|
||||
|
||||
constructor(x: number, y: number)
|
||||
constructor(object: { Physical: { x: number; y: number } })
|
||||
constructor(object: { x: number; y: number })
|
||||
constructor(
|
||||
...args:
|
||||
| [number, number]
|
||||
| [{ x: number; y: number }]
|
||||
| [{ Physical: { x: number; y: number } }]
|
||||
) {
|
||||
if (args.length === 1) {
|
||||
if ('Physical' in args[0]) {
|
||||
this.x = args[0].Physical.x
|
||||
this.y = args[0].Physical.y
|
||||
} else {
|
||||
this.x = args[0].x
|
||||
this.y = args[0].y
|
||||
}
|
||||
} else {
|
||||
this.x = args[0]
|
||||
this.y = args[1]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the physical position to a logical one.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { PhysicalPosition } from '@tauri-apps/api/dpi';
|
||||
* import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
*
|
||||
* const appWindow = getCurrentWindow();
|
||||
* const factor = await appWindow.scaleFactor();
|
||||
* const position = new PhysicalPosition(400, 500);
|
||||
* const logical = position.toLogical(factor);
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
toLogical(scaleFactor: number): LogicalPosition {
|
||||
return new LogicalPosition(this.x / scaleFactor, this.y / scaleFactor)
|
||||
}
|
||||
|
||||
[SERIALIZE_TO_IPC_FN]() {
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
return this[SERIALIZE_TO_IPC_FN]()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A position represented either in physical or in logical pixels.
|
||||
*
|
||||
* This type is basically a union type of {@linkcode LogicalPosition} and {@linkcode PhysicalPosition}
|
||||
* but comes in handy when using `tauri::Position` in Rust as an argument to a command, as this class
|
||||
* automatically serializes into a valid format so it can be deserialized correctly into `tauri::Position`
|
||||
*
|
||||
* So instead of
|
||||
* ```typescript
|
||||
* import { invoke } from '@tauri-apps/api/core';
|
||||
* import { LogicalPosition, PhysicalPosition } from '@tauri-apps/api/dpi';
|
||||
*
|
||||
* const position: LogicalPosition | PhysicalPosition = someFunction(); // where someFunction returns either LogicalPosition or PhysicalPosition
|
||||
* const validPosition = position instanceof LogicalPosition
|
||||
* ? { Logical: { x: position.x, y: position.y } }
|
||||
* : { Physical: { x: position.x, y: position.y } }
|
||||
* await invoke("do_something_with_position", { position: validPosition });
|
||||
* ```
|
||||
*
|
||||
* You can just use {@linkcode Position}
|
||||
* ```typescript
|
||||
* import { invoke } from '@tauri-apps/api/core';
|
||||
* import { LogicalPosition, PhysicalPosition, Position } from '@tauri-apps/api/dpi';
|
||||
*
|
||||
* const position: LogicalPosition | PhysicalPosition = someFunction(); // where someFunction returns either LogicalPosition or PhysicalPosition
|
||||
* const validPosition = new Position(position);
|
||||
* await invoke("do_something_with_position", { position: validPosition });
|
||||
* ```
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
class Position {
|
||||
position: LogicalPosition | PhysicalPosition
|
||||
|
||||
constructor(position: LogicalPosition | PhysicalPosition) {
|
||||
this.position = position
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the wrapped position to a logical one.
|
||||
*
|
||||
* Returns the inner value untouched when it already is a {@linkcode LogicalPosition}.
|
||||
*
|
||||
* @param scaleFactor The monitor scale factor, e.g. `await appWindow.scaleFactor()`.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
toLogical(scaleFactor: number): LogicalPosition {
|
||||
return this.position instanceof LogicalPosition
|
||||
? this.position
|
||||
: this.position.toLogical(scaleFactor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the wrapped position to a physical one.
|
||||
*
|
||||
* Returns the inner value untouched when it already is a {@linkcode PhysicalPosition}.
|
||||
*
|
||||
* @param scaleFactor The monitor scale factor, e.g. `await appWindow.scaleFactor()`.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
toPhysical(scaleFactor: number): PhysicalPosition {
|
||||
return this.position instanceof PhysicalPosition
|
||||
? this.position
|
||||
: this.position.toPhysical(scaleFactor)
|
||||
}
|
||||
|
||||
[SERIALIZE_TO_IPC_FN]() {
|
||||
return {
|
||||
[`${this.position.type}`]: {
|
||||
x: this.position.x,
|
||||
y: this.position.y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
return this[SERIALIZE_TO_IPC_FN]()
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
LogicalPosition,
|
||||
LogicalSize,
|
||||
Size,
|
||||
PhysicalPosition,
|
||||
PhysicalSize,
|
||||
Position
|
||||
}
|
||||
312
packages/api/src/event.ts
Normal file
312
packages/api/src/event.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* The event system allows you to emit events to the backend and listen to events from it.
|
||||
*
|
||||
* This package is also accessible with `window.__TAURI__.event` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`.
|
||||
*
|
||||
* @remarks All commands used by this module (`core:event:allow-listen`,
|
||||
* `allow-unlisten`, `allow-emit` and `allow-emit-to`) are part of the
|
||||
* `core:event:default` permission set, which is enabled by default, so no extra
|
||||
* capability configuration is needed.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { invoke, transformCallback } from './core'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__TAURI_EVENT_PLUGIN_INTERNALS__: {
|
||||
unregisterListener: (event: string, eventId: number) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The target of an event, used to filter which listeners receive it and which
|
||||
* listeners a given emit reaches.
|
||||
*
|
||||
* - `Any`: matches every target (the default).
|
||||
* - `AnyLabel`: matches any window, webview or webview window with the given label.
|
||||
* - `App`: the application itself, i.e. listeners registered with `app.listen` on the Rust side.
|
||||
* - `Window` / `Webview` / `WebviewWindow`: the specific target with that label.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
type EventTarget =
|
||||
| { kind: 'Any' }
|
||||
| { kind: 'AnyLabel'; label: string }
|
||||
| { kind: 'App' }
|
||||
| { kind: 'Window'; label: string }
|
||||
| { kind: 'Webview'; label: string }
|
||||
| { kind: 'WebviewWindow'; label: string }
|
||||
|
||||
interface Event<T> {
|
||||
/** Event name */
|
||||
event: EventName
|
||||
/** Event identifier used to unlisten */
|
||||
id: number
|
||||
/** Event payload */
|
||||
payload: T
|
||||
}
|
||||
|
||||
type EventCallback<T> = (event: Event<T>) => void
|
||||
|
||||
// TODO(v3): mark this as Promise<void>
|
||||
type UnlistenFn = () => void
|
||||
|
||||
type EventName = `${TauriEvent}` | (string & Record<never, never>)
|
||||
|
||||
interface Options {
|
||||
/**
|
||||
* The event target to listen to, defaults to `{ kind: 'Any' }`, see {@link EventTarget}.
|
||||
*
|
||||
* If a string is provided, it is used as the label of an `AnyLabel` target,
|
||||
* i.e. `{ kind: 'AnyLabel', label: <the string> }`.
|
||||
*/
|
||||
target?: string | EventTarget
|
||||
}
|
||||
|
||||
/**
|
||||
* The built-in event names emitted by Tauri itself.
|
||||
*
|
||||
* These are the raw names behind the `on*` helpers of the `Window` and `Webview`
|
||||
* classes (e.g. `Window.onResized` listens to {@linkcode TauriEvent.WINDOW_RESIZED}).
|
||||
* Prefer those helpers when one exists, since they also decode the payload into
|
||||
* the matching class (`PhysicalSize`, `PhysicalPosition`, ...).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { listen, TauriEvent } from '@tauri-apps/api/event';
|
||||
* const unlisten = await listen(TauriEvent.WINDOW_DESTROYED, (event) => {
|
||||
* console.log('a window was destroyed', event.payload);
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
enum TauriEvent {
|
||||
/** A window was resized. Payload: the new inner size, in physical pixels. See `Window.onResized`. */
|
||||
WINDOW_RESIZED = 'tauri://resize',
|
||||
/** A window was moved. Payload: the new outer position, in physical pixels. See `Window.onMoved`. */
|
||||
WINDOW_MOVED = 'tauri://move',
|
||||
/**
|
||||
* The user requested a window to be closed (e.g. clicked the close button).
|
||||
* See `Window.onCloseRequested`, which also handles preventing the close.
|
||||
*/
|
||||
WINDOW_CLOSE_REQUESTED = 'tauri://close-requested',
|
||||
/** A window was destroyed, i.e. it is gone and its label can be reused. */
|
||||
WINDOW_DESTROYED = 'tauri://destroyed',
|
||||
/** A window gained focus. See `Window.onFocusChanged`. */
|
||||
WINDOW_FOCUS = 'tauri://focus',
|
||||
/** A window lost focus. See `Window.onFocusChanged`. */
|
||||
WINDOW_BLUR = 'tauri://blur',
|
||||
/**
|
||||
* The scale factor of the monitor a window is on changed, or the window moved to
|
||||
* a monitor with a different scale factor. See `Window.onScaleChanged`.
|
||||
*/
|
||||
WINDOW_SCALE_FACTOR_CHANGED = 'tauri://scale-change',
|
||||
/** The system or window theme changed. See `Window.onThemeChanged`. */
|
||||
WINDOW_THEME_CHANGED = 'tauri://theme-changed',
|
||||
/** A new window was created. */
|
||||
WINDOW_CREATED = 'tauri://window-created',
|
||||
/**
|
||||
* The window's event loop was suspended.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** emitted when the activity is paused.
|
||||
* - **Other platforms:** never emitted.
|
||||
*/
|
||||
WINDOW_SUSPENDED = 'tauri://suspended',
|
||||
/**
|
||||
* The window's event loop was resumed after being suspended.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** emitted when the activity is resumed.
|
||||
* - **Other platforms:** never emitted.
|
||||
*/
|
||||
WINDOW_RESUMED = 'tauri://resumed',
|
||||
/** A new webview was created. */
|
||||
WEBVIEW_CREATED = 'tauri://webview-created',
|
||||
/** The user dragged files onto a webview. See `Webview.onDragDropEvent`. */
|
||||
DRAG_ENTER = 'tauri://drag-enter',
|
||||
/** The user is moving dragged files over a webview. See `Webview.onDragDropEvent`. */
|
||||
DRAG_OVER = 'tauri://drag-over',
|
||||
/** The user dropped files onto a webview. See `Webview.onDragDropEvent`. */
|
||||
DRAG_DROP = 'tauri://drag-drop',
|
||||
/** The drag operation left the webview or was cancelled. See `Webview.onDragDropEvent`. */
|
||||
DRAG_LEAVE = 'tauri://drag-leave'
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the event listener associated with the given name and id.
|
||||
*
|
||||
* @ignore
|
||||
* @param event The event name
|
||||
* @param eventId Event identifier
|
||||
* @returns
|
||||
*/
|
||||
async function _unlisten(event: string, eventId: number): Promise<void> {
|
||||
window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(event, eventId)
|
||||
await invoke('plugin:event|unlisten', {
|
||||
event,
|
||||
eventId
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to an emitted event to any {@link EventTarget|target}.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { listen } from '@tauri-apps/api/event';
|
||||
* const unlisten = await listen<string>('error', (event) => {
|
||||
* console.log(`Got error, payload: ${event.payload}`);
|
||||
* });
|
||||
*
|
||||
* // call unlisten when your handler goes out of scope e.g. the component is unmounted
|
||||
* unlisten();
|
||||
* ```
|
||||
*
|
||||
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
|
||||
* @param handler Event handler callback.
|
||||
* @param options Event listening options.
|
||||
* @returns A promise resolving to a function to unlisten to the event.
|
||||
*
|
||||
* @remarks Listeners bound to a window or webview are removed automatically when
|
||||
* that window or webview is destroyed, so you do not need to unlisten just to
|
||||
* avoid leaking across a window close. You should still call the returned
|
||||
* function when the listener's own scope ends — for example on page navigation
|
||||
* or when a component unmounts — otherwise the handler keeps running for the
|
||||
* lifetime of the webview.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function listen<T>(
|
||||
event: EventName,
|
||||
handler: EventCallback<T>,
|
||||
options?: Options
|
||||
): Promise<UnlistenFn> {
|
||||
const target: EventTarget =
|
||||
typeof options?.target === 'string'
|
||||
? { kind: 'AnyLabel', label: options.target }
|
||||
: (options?.target ?? { kind: 'Any' })
|
||||
return invoke<number>('plugin:event|listen', {
|
||||
event,
|
||||
target,
|
||||
handler: transformCallback(handler)
|
||||
}).then((eventId) => {
|
||||
return async () => _unlisten(event, eventId)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens once to an emitted event to any {@link EventTarget|target}.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { once } from '@tauri-apps/api/event';
|
||||
* interface LoadedPayload {
|
||||
* loggedIn: boolean,
|
||||
* token: string
|
||||
* }
|
||||
* const unlisten = await once<LoadedPayload>('loaded', (event) => {
|
||||
* console.log(`App is loaded, loggedIn: ${event.payload.loggedIn}, token: ${event.payload.token}`);
|
||||
* });
|
||||
*
|
||||
* // call unlisten when your handler goes out of scope e.g. the component is unmounted
|
||||
* unlisten();
|
||||
* ```
|
||||
*
|
||||
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
|
||||
* @param handler Event handler callback.
|
||||
* @param options Event listening options.
|
||||
* @returns A promise resolving to a function to unlisten to the event.
|
||||
*
|
||||
* @remarks The listener removes itself after the first event, and listeners bound
|
||||
* to a window or webview are also removed automatically when that target is
|
||||
* destroyed. Still call the returned function when the listener's own scope ends
|
||||
* before the event arrives — for example on page navigation or component unmount.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function once<T>(
|
||||
event: EventName,
|
||||
handler: EventCallback<T>,
|
||||
options?: Options
|
||||
): Promise<UnlistenFn> {
|
||||
return listen<T>(
|
||||
event,
|
||||
(eventData) => {
|
||||
void _unlisten(event, eventData.id)
|
||||
handler(eventData)
|
||||
},
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event to all {@link EventTarget|targets}.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { emit } from '@tauri-apps/api/event';
|
||||
* await emit('frontend-loaded', { loggedIn: true, token: 'authToken' });
|
||||
* ```
|
||||
*
|
||||
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
|
||||
* @param payload Event payload.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function emit<T>(event: string, payload?: T): Promise<void> {
|
||||
await invoke('plugin:event|emit', {
|
||||
event,
|
||||
payload
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event to all {@link EventTarget|targets} matching the given target.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { emitTo } from '@tauri-apps/api/event';
|
||||
* await emitTo('main', 'frontend-loaded', { loggedIn: true, token: 'authToken' });
|
||||
* ```
|
||||
*
|
||||
* @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object.
|
||||
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
|
||||
* @param payload Event payload.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function emitTo<T>(
|
||||
target: EventTarget | string,
|
||||
event: string,
|
||||
payload?: T
|
||||
): Promise<void> {
|
||||
const eventTarget: EventTarget =
|
||||
typeof target === 'string' ? { kind: 'AnyLabel', label: target } : target
|
||||
await invoke('plugin:event|emit_to', {
|
||||
target: eventTarget,
|
||||
event,
|
||||
payload
|
||||
})
|
||||
}
|
||||
|
||||
export type {
|
||||
Event,
|
||||
EventTarget,
|
||||
EventCallback,
|
||||
UnlistenFn,
|
||||
EventName,
|
||||
Options
|
||||
}
|
||||
|
||||
export { listen, once, emit, emitTo, TauriEvent }
|
||||
64
packages/api/src/global.d.ts
vendored
Normal file
64
packages/api/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/** @ignore */
|
||||
|
||||
import type { invoke, transformCallback, convertFileSrc } from './core'
|
||||
|
||||
/** @ignore */
|
||||
declare global {
|
||||
interface Window {
|
||||
/**
|
||||
* The whole `@tauri-apps/api` package, exposed as a global object.
|
||||
*
|
||||
* Only defined when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri)
|
||||
* is set to `true` in `tauri.conf.json`. It is meant for vanilla JavaScript
|
||||
* frontends that do not use a bundler:
|
||||
*
|
||||
* ```js
|
||||
* const { event, window: tauriWindow, path } = window.__TAURI__;
|
||||
* ```
|
||||
*/
|
||||
__TAURI__?: typeof import('./index')
|
||||
__TAURI_INTERNALS__: {
|
||||
invoke: typeof invoke
|
||||
transformCallback: typeof transformCallback
|
||||
unregisterCallback: (id: number) => void
|
||||
runCallback: (id: number, data: unknown) => void
|
||||
callbacks: Map<number, (data: unknown) => void>
|
||||
convertFileSrc: typeof convertFileSrc
|
||||
ipc: (message: {
|
||||
cmd: string
|
||||
callback: number
|
||||
error: number
|
||||
payload: unknown
|
||||
options?: InvokeOptions
|
||||
}) => void
|
||||
metadata: {
|
||||
currentWindow: WindowDef
|
||||
currentWebview: WebviewDef
|
||||
}
|
||||
plugins: {
|
||||
path: {
|
||||
sep: string
|
||||
delimiter: string
|
||||
}
|
||||
}
|
||||
}
|
||||
__TAURI_EVENT_PLUGIN_INTERNALS__: {
|
||||
unregisterListener: (event: string, eventId: number) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @ignore */
|
||||
interface WebviewDef {
|
||||
windowLabel: string
|
||||
label: string
|
||||
}
|
||||
|
||||
/** @ignore */
|
||||
interface WindowDef {
|
||||
label: string
|
||||
}
|
||||
243
packages/api/src/image.ts
Normal file
243
packages/api/src/image.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* Load and inspect images to use as window icons, tray icons and menu item icons.
|
||||
*
|
||||
* Images live on the Rust side (they extend {@linkcode Resource}), the frontend only
|
||||
* holds a handle to them. Call `close()` when an image is no longer needed, or let
|
||||
* it be released when the app exits.
|
||||
*
|
||||
* @remarks {@linkcode Image.fromPath} and {@linkcode Image.fromBytes} decode PNG and ICO
|
||||
* files, which requires the `image-png` and/or `image-ico` Cargo features of the
|
||||
* `tauri` crate:
|
||||
* ```toml
|
||||
* [dependencies]
|
||||
* tauri = { version = "2", features = ["image-png"] }
|
||||
* ```
|
||||
* {@linkcode Image.new} takes raw RGBA data and needs no extra feature.
|
||||
*
|
||||
* @remarks All commands used by this module are part of the `core:image:default`
|
||||
* permission set, which is enabled by default.
|
||||
*
|
||||
* This package is also accessible with `window.__TAURI__.image` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { Resource, invoke } from './core'
|
||||
import { NativeIcon } from './menu/iconMenuItem'
|
||||
|
||||
/** Image dimensions, in pixels. */
|
||||
export interface ImageSize {
|
||||
/** Image width, in pixels. */
|
||||
width: number
|
||||
/** Image height, in pixels. */
|
||||
height: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A type that can be passed to Rust side as [`tauri::image::JsImage`](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html) through {@linkcode transformImage}
|
||||
*
|
||||
* Values of this type must go through {@linkcode transformImage} before being placed in `invoke` arguments;
|
||||
* an {@linkcode Image} instance is not serializable on its own.
|
||||
*
|
||||
* ## Variants
|
||||
*
|
||||
* - **string:** Path to an image in the filesystem. Maps to [`JsImage::Path`](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html#variant.Path)
|
||||
* - **Uint8Array | ArrayBuffer | number[]:** ICO or PNG image in raw bytes. Maps to [`JsImage::Bytes`](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html#variant.Bytes)
|
||||
* - **Image:** An image that was previously loaded with the API and is stored in the resource table. Maps to [`JsImage::Resource`](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html#variant.Resource)
|
||||
*
|
||||
* The `string` and bytes variants require the `image-ico` or `image-png` Cargo features.
|
||||
* To enable them, change your Cargo.toml file:
|
||||
* ```toml
|
||||
* [dependencies]
|
||||
* tauri = { version = "...", features = ["...", "image-png"] }
|
||||
* ```
|
||||
*
|
||||
* The Rust [`JsImage::Rgba`](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html#variant.Rgba) variant is intentionally not exposed here;
|
||||
* use {@linkcode Image.new} to create an image from raw RGBA data instead.
|
||||
*/
|
||||
export type JsImage = string | Uint8Array | ArrayBuffer | number[] | Image
|
||||
|
||||
/** A type that represents an icon that can be used in menu items. */
|
||||
export type MenuIcon = JsImage | NativeIcon
|
||||
|
||||
/**
|
||||
* An RGBA Image in row-major order from top to bottom.
|
||||
*
|
||||
* Instances are created with {@linkcode Image.new}, {@linkcode Image.fromBytes} or
|
||||
* {@linkcode Image.fromPath} and can be passed wherever a {@linkcode JsImage} is
|
||||
* accepted, such as `Window.setIcon`, `TrayIcon.setIcon` or an icon menu item.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Image } from '@tauri-apps/api/image';
|
||||
* import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
*
|
||||
* const icon = await Image.fromPath('icons/icon.png');
|
||||
* await getCurrentWindow().setIcon(icon);
|
||||
* await icon.close();
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export class Image extends Resource {
|
||||
/**
|
||||
* Creates an Image from a resource ID. For internal use only.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
constructor(rid: number) {
|
||||
super(rid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Image using RGBA data, in row-major order from top to bottom, and with specified width and height.
|
||||
*
|
||||
* The buffer must contain exactly `width * height * 4` bytes.
|
||||
* Unlike {@linkcode Image.fromBytes} and {@linkcode Image.fromPath} this does not
|
||||
* decode an image format, so it requires no extra Cargo feature.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Image } from '@tauri-apps/api/image';
|
||||
*
|
||||
* // a 1x1 opaque red image
|
||||
* const image = await Image.new(new Uint8Array([255, 0, 0, 255]), 1, 1);
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
static async new(
|
||||
rgba: number[] | Uint8Array | ArrayBuffer,
|
||||
width: number,
|
||||
height: number
|
||||
): Promise<Image> {
|
||||
return invoke<number>('plugin:image|new', {
|
||||
rgba,
|
||||
width,
|
||||
height
|
||||
}).then((rid) => new Image(rid))
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new image using the provided bytes by inferring the file format.
|
||||
*
|
||||
* Only `ico` and `png` are supported (based on activated feature flag).
|
||||
*
|
||||
* Note that you need the `image-ico` or `image-png` Cargo features to use this API.
|
||||
* To enable it, change your Cargo.toml file:
|
||||
* ```toml
|
||||
* [dependencies]
|
||||
* tauri = { version = "...", features = ["...", "image-png"] }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Image } from '@tauri-apps/api/image';
|
||||
*
|
||||
* const bytes = await fetch('/icon.png').then((r) => r.arrayBuffer());
|
||||
* const image = await Image.fromBytes(bytes);
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
static async fromBytes(
|
||||
bytes: number[] | Uint8Array | ArrayBuffer
|
||||
): Promise<Image> {
|
||||
return invoke<number>('plugin:image|from_bytes', {
|
||||
bytes
|
||||
}).then((rid) => new Image(rid))
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new image using the provided path.
|
||||
*
|
||||
* Only `ico` and `png` are supported (based on activated feature flag).
|
||||
*
|
||||
* Note that you need the `image-ico` or `image-png` Cargo features to use this API.
|
||||
* To enable it, change your Cargo.toml file:
|
||||
* ```toml
|
||||
* [dependencies]
|
||||
* tauri = { version = "...", features = ["...", "image-png"] }
|
||||
* ```
|
||||
*
|
||||
* The path is resolved on the Rust side, so it must be a path on the user's
|
||||
* machine (for example one returned by the path APIs or a bundled resource
|
||||
* resolved with `resolveResource`), not a frontend asset URL.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Image } from '@tauri-apps/api/image';
|
||||
* import { resolveResource } from '@tauri-apps/api/path';
|
||||
*
|
||||
* const image = await Image.fromPath(await resolveResource('icons/icon.png'));
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
static async fromPath(path: string): Promise<Image> {
|
||||
return invoke<number>('plugin:image|from_path', { path }).then(
|
||||
(rid) => new Image(rid)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RGBA data for this image, in row-major order from top to bottom.
|
||||
*
|
||||
* The returned buffer has `width * height * 4` bytes, see {@linkcode Image.size}.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Image } from '@tauri-apps/api/image';
|
||||
*
|
||||
* const image = await Image.fromPath('icons/icon.png');
|
||||
* const rgba = await image.rgba();
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async rgba(): Promise<Uint8Array<ArrayBuffer>> {
|
||||
return invoke<number[]>('plugin:image|rgba', {
|
||||
rid: this.rid
|
||||
}).then((buffer) => new Uint8Array(buffer))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of this image, in pixels.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Image } from '@tauri-apps/api/image';
|
||||
*
|
||||
* const image = await Image.fromPath('icons/icon.png');
|
||||
* const { width, height } = await image.size();
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async size(): Promise<ImageSize> {
|
||||
return invoke<ImageSize>('plugin:image|size', { rid: this.rid })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms image from various types into a type acceptable by Rust.
|
||||
*
|
||||
* See [`tauri::image::JsImage`](https://docs.rs/tauri/2/tauri/image/enum.JsImage.html) for more information.
|
||||
* Note the API signature is not stable and might change.
|
||||
*/
|
||||
export function transformImage<T>(image: JsImage | null): T {
|
||||
const ret =
|
||||
image == null
|
||||
? null
|
||||
: typeof image === 'string'
|
||||
? image
|
||||
: image instanceof Image
|
||||
? image.rid
|
||||
: image
|
||||
|
||||
return ret as T
|
||||
}
|
||||
52
packages/api/src/index.ts
Normal file
52
packages/api/src/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* The Tauri API allows you to interface with the backend layer.
|
||||
*
|
||||
* This module exposes all other modules as an object where the key is the module name, and the value is the module exports.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { event, window, path } from '@tauri-apps/api'
|
||||
* ```
|
||||
*
|
||||
* ### Vanilla JS API
|
||||
*
|
||||
* The above import syntax is for JavaScript/TypeScript with a bundler. If you're using vanilla JavaScript, you can use the global `window.__TAURI__` object instead. It requires `app.withGlobalTauri` configuration option enabled.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const { event, window: tauriWindow, path } = window.__TAURI__;
|
||||
* ```
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import * as app from './app'
|
||||
import * as core from './core'
|
||||
import * as dpi from './dpi'
|
||||
import * as event from './event'
|
||||
import * as image from './image'
|
||||
import * as menu from './menu'
|
||||
import * as mocks from './mocks'
|
||||
import * as path from './path'
|
||||
import * as tray from './tray'
|
||||
import * as webview from './webview'
|
||||
import * as webviewWindow from './webviewWindow'
|
||||
import * as window from './window'
|
||||
|
||||
export {
|
||||
app,
|
||||
core,
|
||||
dpi,
|
||||
event,
|
||||
image,
|
||||
menu,
|
||||
mocks,
|
||||
path,
|
||||
tray,
|
||||
webview,
|
||||
webviewWindow,
|
||||
window
|
||||
}
|
||||
31
packages/api/src/menu.ts
Normal file
31
packages/api/src/menu.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* Build native application, window and context menus.
|
||||
*
|
||||
* A {@linkcode Menu} holds items ({@linkcode MenuItem}, {@linkcode CheckMenuItem},
|
||||
* {@linkcode IconMenuItem}, {@linkcode PredefinedMenuItem}) and {@linkcode Submenu}s.
|
||||
* Use `Menu.setAsAppMenu()` for the macOS application menu, `Menu.setAsWindowMenu()`
|
||||
* for the Windows/Linux window menu bar, or `Menu.popup()` for a context menu.
|
||||
*
|
||||
* Menus live on the Rust side, the frontend only holds handles to them, so keep a
|
||||
* reference to a menu for as long as it is in use.
|
||||
*
|
||||
* This package is also accessible with `window.__TAURI__.menu` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`.
|
||||
*
|
||||
* @remarks All commands used by this module are part of the `core:menu:default`
|
||||
* permission set, which is enabled by default, so no extra capability
|
||||
* configuration is needed. Menu item icons additionally require the `image-png` /
|
||||
* `image-ico` Cargo features of the `tauri` crate.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
export * from './menu/submenu'
|
||||
export * from './menu/menuItem'
|
||||
export * from './menu/menu'
|
||||
export * from './menu/checkMenuItem'
|
||||
export * from './menu/iconMenuItem'
|
||||
export * from './menu/predefinedMenuItem'
|
||||
164
packages/api/src/menu/base.ts
Normal file
164
packages/api/src/menu/base.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Channel, invoke, Resource } from '../core'
|
||||
import { transformImage } from '../image'
|
||||
import { CheckMenuItemOptions } from './checkMenuItem'
|
||||
import { IconMenuItemOptions } from './iconMenuItem'
|
||||
import { MenuOptions } from './menu'
|
||||
import { MenuItemOptions } from './menuItem'
|
||||
import { PredefinedMenuItemOptions } from './predefinedMenuItem'
|
||||
import { SubmenuOptions } from './submenu'
|
||||
|
||||
/**
|
||||
* The kind of a menu item, used internally to route IPC calls to the right
|
||||
* Rust-side type.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
export type ItemKind =
|
||||
| 'MenuItem'
|
||||
| 'Predefined'
|
||||
| 'Check'
|
||||
| 'Icon'
|
||||
| 'Submenu'
|
||||
| 'Menu'
|
||||
|
||||
type MenuItemOptionsAlias =
|
||||
| MenuItemOptions
|
||||
| SubmenuOptions
|
||||
| IconMenuItemOptions
|
||||
| PredefinedMenuItemOptions
|
||||
| CheckMenuItemOptions
|
||||
|
||||
function injectChannel(i: MenuItemOptionsAlias): MenuItemOptionsAlias & {
|
||||
handler?: Channel<string>
|
||||
} {
|
||||
if ('items' in i) {
|
||||
i.items = i.items?.map((item) =>
|
||||
'rid' in item ? item : injectChannel(item)
|
||||
)
|
||||
} else if ('action' in i && i.action) {
|
||||
const handler = new Channel<string>()
|
||||
handler.onmessage = i.action
|
||||
delete i.action
|
||||
return { ...i, handler }
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a menu or menu item on the Rust side. Implementation detail of the
|
||||
* `new` static methods of the menu classes.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
export async function newMenu(
|
||||
kind: ItemKind,
|
||||
opts?:
|
||||
| MenuOptions
|
||||
| MenuItemOptions
|
||||
| SubmenuOptions
|
||||
| PredefinedMenuItemOptions
|
||||
| CheckMenuItemOptions
|
||||
| IconMenuItemOptions
|
||||
): Promise<[number, string]> {
|
||||
const handler = new Channel<string>()
|
||||
|
||||
if (opts && typeof opts === 'object') {
|
||||
if ('action' in opts && opts.action) {
|
||||
handler.onmessage = opts.action
|
||||
delete opts.action
|
||||
}
|
||||
|
||||
// about predefined menu item icon
|
||||
if (
|
||||
'item' in opts
|
||||
&& opts.item
|
||||
&& typeof opts.item === 'object'
|
||||
&& 'About' in opts.item
|
||||
&& opts.item.About
|
||||
&& typeof opts.item.About === 'object'
|
||||
&& 'icon' in opts.item.About
|
||||
&& opts.item.About.icon
|
||||
) {
|
||||
opts.item.About.icon = transformImage(opts.item.About.icon)
|
||||
}
|
||||
|
||||
// icon menu item icon
|
||||
if ('icon' in opts && opts.icon) {
|
||||
opts.icon = transformImage(opts.icon)
|
||||
}
|
||||
|
||||
// submenu items
|
||||
if ('items' in opts && opts.items) {
|
||||
function prepareItem(
|
||||
i: { rid: number; kind: string } | MenuItemOptionsAlias
|
||||
): [number, string] | MenuItemOptionsAlias {
|
||||
if ('rid' in i) {
|
||||
return [i.rid, i.kind]
|
||||
}
|
||||
|
||||
if ('item' in i && typeof i.item === 'object' && i.item.About?.icon) {
|
||||
i.item.About.icon = transformImage(i.item.About.icon)
|
||||
}
|
||||
|
||||
if ('icon' in i && i.icon) {
|
||||
i.icon = transformImage(i.icon)
|
||||
}
|
||||
|
||||
if ('items' in i && i.items) {
|
||||
// @ts-expect-error the `prepareItem` return doesn't exactly match
|
||||
// this is fine, because the difference is in `[number, string]` variant
|
||||
i.items = i.items.map(prepareItem)
|
||||
}
|
||||
|
||||
return injectChannel(i)
|
||||
}
|
||||
|
||||
// @ts-expect-error the `prepareItem` return doesn't exactly match
|
||||
// this is fine, because the difference is in `[number, string]` variant
|
||||
opts.items = opts.items.map(prepareItem)
|
||||
}
|
||||
}
|
||||
|
||||
return invoke('plugin:menu|new', {
|
||||
kind,
|
||||
options: opts,
|
||||
handler
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* The base class of every menu and menu item type.
|
||||
*
|
||||
* It is not constructible on its own: create a {@linkcode Menu}, {@linkcode Submenu},
|
||||
* {@linkcode MenuItem}, {@linkcode CheckMenuItem}, {@linkcode IconMenuItem} or
|
||||
* {@linkcode PredefinedMenuItem} instead. It provides the `id` shared by all of
|
||||
* them and, through {@linkcode Resource}, the `rid` and `close()` used to release
|
||||
* the Rust-side object.
|
||||
*/
|
||||
export class MenuItemBase extends Resource {
|
||||
/** @ignore */
|
||||
readonly #id: string
|
||||
/** @ignore */
|
||||
readonly #kind: ItemKind
|
||||
|
||||
/** The id of this item. */
|
||||
get id(): string {
|
||||
return this.#id
|
||||
}
|
||||
|
||||
/** @ignore */
|
||||
get kind(): string {
|
||||
return this.#kind
|
||||
}
|
||||
|
||||
/** @ignore */
|
||||
protected constructor(rid: number, id: string, kind: ItemKind) {
|
||||
super(rid)
|
||||
this.#id = id
|
||||
this.#kind = kind
|
||||
}
|
||||
}
|
||||
86
packages/api/src/menu/checkMenuItem.ts
Normal file
86
packages/api/src/menu/checkMenuItem.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { MenuItemBase, newMenu } from './base'
|
||||
import { invoke } from '../core'
|
||||
import { type MenuItemOptions } from '../menu'
|
||||
|
||||
/** Options for creating a new check menu item. */
|
||||
export interface CheckMenuItemOptions extends MenuItemOptions {
|
||||
/** Whether the new check menu item is enabled or not. */
|
||||
checked?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A check menu item inside a {@linkcode Menu} or {@linkcode Submenu}
|
||||
* and usually contains a text and a check mark or a similar toggle
|
||||
* that corresponds to a checked and unchecked states.
|
||||
*/
|
||||
export class CheckMenuItem extends MenuItemBase {
|
||||
/** @ignore */
|
||||
protected constructor(rid: number, id: string) {
|
||||
super(rid, id, 'Check')
|
||||
}
|
||||
|
||||
/** Create a new check menu item. */
|
||||
static async new(opts: CheckMenuItemOptions): Promise<CheckMenuItem> {
|
||||
return newMenu('Check', opts).then(
|
||||
([rid, id]) => new CheckMenuItem(rid, id)
|
||||
)
|
||||
}
|
||||
|
||||
/** Returns the text of this check menu item. */
|
||||
async text(): Promise<string> {
|
||||
return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind })
|
||||
}
|
||||
|
||||
/** Sets the text for this check menu item. */
|
||||
async setText(text: string): Promise<void> {
|
||||
return invoke('plugin:menu|set_text', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
text
|
||||
})
|
||||
}
|
||||
|
||||
/** Returns whether this check menu item is enabled or not. */
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind })
|
||||
}
|
||||
|
||||
/** Sets whether this check menu item is enabled or not. */
|
||||
async setEnabled(enabled: boolean): Promise<void> {
|
||||
return invoke('plugin:menu|set_enabled', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
enabled
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the accelerator for this check menu item, or removes it when given `null`.
|
||||
*
|
||||
* See {@linkcode MenuItemOptions.accelerator} for the accepted format.
|
||||
*/
|
||||
async setAccelerator(accelerator: string | null): Promise<void> {
|
||||
return invoke('plugin:menu|set_accelerator', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
accelerator
|
||||
})
|
||||
}
|
||||
|
||||
/** Returns whether this check menu item is checked or not. */
|
||||
async isChecked(): Promise<boolean> {
|
||||
return invoke('plugin:menu|is_checked', { rid: this.rid })
|
||||
}
|
||||
|
||||
/** Sets whether this check menu item is checked or not. */
|
||||
async setChecked(checked: boolean): Promise<void> {
|
||||
return invoke('plugin:menu|set_checked', {
|
||||
rid: this.rid,
|
||||
checked
|
||||
})
|
||||
}
|
||||
}
|
||||
211
packages/api/src/menu/iconMenuItem.ts
Normal file
211
packages/api/src/menu/iconMenuItem.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { MenuItemBase, newMenu } from './base'
|
||||
import { type MenuItemOptions } from '../menu'
|
||||
import { invoke } from '../core'
|
||||
import { transformImage, MenuIcon } from '../image'
|
||||
|
||||
/**
|
||||
* A native Icon to be used for the menu item
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **Windows / Linux**: Unsupported.
|
||||
*/
|
||||
export enum NativeIcon {
|
||||
/** An add item template image. */
|
||||
Add = 'Add',
|
||||
/** Advanced preferences toolbar icon for the preferences window. */
|
||||
Advanced = 'Advanced',
|
||||
/** A Bluetooth template image. */
|
||||
Bluetooth = 'Bluetooth',
|
||||
/** Bookmarks image suitable for a template. */
|
||||
Bookmarks = 'Bookmarks',
|
||||
/** A caution image. */
|
||||
Caution = 'Caution',
|
||||
/** A color panel toolbar icon. */
|
||||
ColorPanel = 'ColorPanel',
|
||||
/** A column view mode template image. */
|
||||
ColumnView = 'ColumnView',
|
||||
/** A computer icon. */
|
||||
Computer = 'Computer',
|
||||
/** An enter full-screen mode template image. */
|
||||
EnterFullScreen = 'EnterFullScreen',
|
||||
/** Permissions for all users. */
|
||||
Everyone = 'Everyone',
|
||||
/** An exit full-screen mode template image. */
|
||||
ExitFullScreen = 'ExitFullScreen',
|
||||
/** A cover flow view mode template image. */
|
||||
FlowView = 'FlowView',
|
||||
/** A folder image. */
|
||||
Folder = 'Folder',
|
||||
/** A burnable folder icon. */
|
||||
FolderBurnable = 'FolderBurnable',
|
||||
/** A smart folder icon. */
|
||||
FolderSmart = 'FolderSmart',
|
||||
/** A link template image. */
|
||||
FollowLinkFreestanding = 'FollowLinkFreestanding',
|
||||
/** A font panel toolbar icon. */
|
||||
FontPanel = 'FontPanel',
|
||||
/** A `go back` template image. */
|
||||
GoLeft = 'GoLeft',
|
||||
/** A `go forward` template image. */
|
||||
GoRight = 'GoRight',
|
||||
/** Home image suitable for a template. */
|
||||
Home = 'Home',
|
||||
/** An iChat Theater template image. */
|
||||
IChatTheater = 'IChatTheater',
|
||||
/** An icon view mode template image. */
|
||||
IconView = 'IconView',
|
||||
/** An information toolbar icon. */
|
||||
Info = 'Info',
|
||||
/** A template image used to denote invalid data. */
|
||||
InvalidDataFreestanding = 'InvalidDataFreestanding',
|
||||
/** A generic left-facing triangle template image. */
|
||||
LeftFacingTriangle = 'LeftFacingTriangle',
|
||||
/** A list view mode template image. */
|
||||
ListView = 'ListView',
|
||||
/** A locked padlock template image. */
|
||||
LockLocked = 'LockLocked',
|
||||
/** An unlocked padlock template image. */
|
||||
LockUnlocked = 'LockUnlocked',
|
||||
/** A horizontal dash, for use in menus. */
|
||||
MenuMixedState = 'MenuMixedState',
|
||||
/** A check mark template image, for use in menus. */
|
||||
MenuOnState = 'MenuOnState',
|
||||
/** A MobileMe icon. */
|
||||
MobileMe = 'MobileMe',
|
||||
/** A drag image for multiple items. */
|
||||
MultipleDocuments = 'MultipleDocuments',
|
||||
/** A network icon. */
|
||||
Network = 'Network',
|
||||
/** A path button template image. */
|
||||
Path = 'Path',
|
||||
/** General preferences toolbar icon for the preferences window. */
|
||||
PreferencesGeneral = 'PreferencesGeneral',
|
||||
/** A Quick Look template image. */
|
||||
QuickLook = 'QuickLook',
|
||||
/** A refresh template image. */
|
||||
RefreshFreestanding = 'RefreshFreestanding',
|
||||
/** A refresh template image. */
|
||||
Refresh = 'Refresh',
|
||||
/** A remove item template image. */
|
||||
Remove = 'Remove',
|
||||
/** A reveal contents template image. */
|
||||
RevealFreestanding = 'RevealFreestanding',
|
||||
/** A generic right-facing triangle template image. */
|
||||
RightFacingTriangle = 'RightFacingTriangle',
|
||||
/** A share view template image. */
|
||||
Share = 'Share',
|
||||
/** A slideshow template image. */
|
||||
Slideshow = 'Slideshow',
|
||||
/** A badge for a `smart` item. */
|
||||
SmartBadge = 'SmartBadge',
|
||||
/** Small green indicator, similar to iChat's available image. */
|
||||
StatusAvailable = 'StatusAvailable',
|
||||
/** Small clear indicator. */
|
||||
StatusNone = 'StatusNone',
|
||||
/** Small yellow indicator, similar to iChat's idle image. */
|
||||
StatusPartiallyAvailable = 'StatusPartiallyAvailable',
|
||||
/** Small red indicator, similar to iChat's unavailable image. */
|
||||
StatusUnavailable = 'StatusUnavailable',
|
||||
/** A stop progress template image. */
|
||||
StopProgressFreestanding = 'StopProgressFreestanding',
|
||||
/** A stop progress button template image. */
|
||||
StopProgress = 'StopProgress',
|
||||
/** An image of the empty trash can. */
|
||||
TrashEmpty = 'TrashEmpty',
|
||||
/** An image of the full trash can. */
|
||||
TrashFull = 'TrashFull',
|
||||
/** Permissions for a single user. */
|
||||
User = 'User',
|
||||
/** User account toolbar icon for the preferences window. */
|
||||
UserAccounts = 'UserAccounts',
|
||||
/** Permissions for a group of users. */
|
||||
UserGroup = 'UserGroup',
|
||||
/** Permissions for guests. */
|
||||
UserGuest = 'UserGuest'
|
||||
}
|
||||
|
||||
/** Options for creating a new icon menu item. */
|
||||
export interface IconMenuItemOptions extends MenuItemOptions {
|
||||
/**
|
||||
* Icon to be used for the new icon menu item.
|
||||
*
|
||||
* Note that you may need the `image-ico` or `image-png` Cargo features to use this API.
|
||||
* To enable it, change your Cargo.toml file:
|
||||
* ```toml
|
||||
* [dependencies]
|
||||
* tauri = { version = "...", features = ["...", "image-png"] }
|
||||
* ```
|
||||
*/
|
||||
icon?: MenuIcon
|
||||
}
|
||||
|
||||
/**
|
||||
* An icon menu item inside a {@linkcode Menu} or {@linkcode Submenu}
|
||||
* and usually contains an icon and a text.
|
||||
*/
|
||||
export class IconMenuItem extends MenuItemBase {
|
||||
/** @ignore */
|
||||
protected constructor(rid: number, id: string) {
|
||||
super(rid, id, 'Icon')
|
||||
}
|
||||
|
||||
/** Create a new icon menu item. */
|
||||
static async new(opts: IconMenuItemOptions): Promise<IconMenuItem> {
|
||||
return newMenu('Icon', opts).then(([rid, id]) => new IconMenuItem(rid, id))
|
||||
}
|
||||
|
||||
/** Returns the text of this icon menu item. */
|
||||
async text(): Promise<string> {
|
||||
return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind })
|
||||
}
|
||||
|
||||
/** Sets the text for this icon menu item. */
|
||||
async setText(text: string): Promise<void> {
|
||||
return invoke('plugin:menu|set_text', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
text
|
||||
})
|
||||
}
|
||||
|
||||
/** Returns whether this icon menu item is enabled or not. */
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind })
|
||||
}
|
||||
|
||||
/** Sets whether this icon menu item is enabled or not. */
|
||||
async setEnabled(enabled: boolean): Promise<void> {
|
||||
return invoke('plugin:menu|set_enabled', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
enabled
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the accelerator for this icon menu item, or removes it when given `null`.
|
||||
*
|
||||
* See {@linkcode MenuItemOptions.accelerator} for the accepted format.
|
||||
*/
|
||||
async setAccelerator(accelerator: string | null): Promise<void> {
|
||||
return invoke('plugin:menu|set_accelerator', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
accelerator
|
||||
})
|
||||
}
|
||||
|
||||
/** Sets an icon for this icon menu item */
|
||||
async setIcon(icon: MenuIcon | null): Promise<void> {
|
||||
return invoke('plugin:menu|set_icon', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
icon: transformImage(icon)
|
||||
})
|
||||
}
|
||||
}
|
||||
349
packages/api/src/menu/menu.ts
Normal file
349
packages/api/src/menu/menu.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import {
|
||||
MenuItemOptions,
|
||||
SubmenuOptions,
|
||||
IconMenuItemOptions,
|
||||
PredefinedMenuItemOptions,
|
||||
CheckMenuItemOptions
|
||||
} from '../menu'
|
||||
import { MenuItem } from './menuItem'
|
||||
import { CheckMenuItem } from './checkMenuItem'
|
||||
import { IconMenuItem } from './iconMenuItem'
|
||||
import { PredefinedMenuItem } from './predefinedMenuItem'
|
||||
import { itemFromKind, Submenu } from './submenu'
|
||||
import { type LogicalPosition, PhysicalPosition, Position } from '../dpi'
|
||||
import { type Window } from '../window'
|
||||
import { invoke } from '../core'
|
||||
import { type ItemKind, MenuItemBase, newMenu } from './base'
|
||||
|
||||
/** Options for creating a new menu. */
|
||||
export interface MenuOptions {
|
||||
/** Specify an id to use for the new menu. */
|
||||
id?: string
|
||||
/** List of items to add to the new menu. */
|
||||
items?: Array<
|
||||
| Submenu
|
||||
| MenuItem
|
||||
| PredefinedMenuItem
|
||||
| CheckMenuItem
|
||||
| IconMenuItem
|
||||
| MenuItemOptions
|
||||
| SubmenuOptions
|
||||
| IconMenuItemOptions
|
||||
| PredefinedMenuItemOptions
|
||||
| CheckMenuItemOptions
|
||||
>
|
||||
}
|
||||
|
||||
/** A type that is either a menu bar on the window
|
||||
* on Windows and Linux or as a global menu in the menubar on macOS.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **macOS**: if using {@linkcode Menu} for the global menubar, it can only contain {@linkcode Submenu}s.
|
||||
*/
|
||||
export class Menu extends MenuItemBase {
|
||||
/** @ignore */
|
||||
protected constructor(rid: number, id: string) {
|
||||
super(rid, id, 'Menu')
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new menu.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Menu, Submenu } from '@tauri-apps/api/menu';
|
||||
*
|
||||
* const menu = await Menu.new({
|
||||
* items: [
|
||||
* await Submenu.new({
|
||||
* text: 'File',
|
||||
* items: [
|
||||
* { id: 'open', text: 'Open', accelerator: 'CmdOrCtrl+O', action: () => console.log('open') },
|
||||
* { item: 'Separator' },
|
||||
* { item: 'Quit' }
|
||||
* ]
|
||||
* })
|
||||
* ]
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
static async new(opts?: MenuOptions): Promise<Menu> {
|
||||
return newMenu('Menu', opts).then(([rid, id]) => new Menu(rid, id))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the default application menu, the same one Tauri installs when no menu
|
||||
* is configured.
|
||||
*
|
||||
* It contains an `Edit` submenu (undo, redo, cut, copy, paste, select all), a
|
||||
* `Window` submenu (minimize, maximize, close window) and a `Help` submenu, plus:
|
||||
*
|
||||
* - **macOS:** an application submenu named after your app (about, services,
|
||||
* hide, hide others, quit) and a `View` submenu with the fullscreen item.
|
||||
* - **Windows:** a `File` submenu with close window and quit; the about item
|
||||
* lives in `Help`.
|
||||
* - **Linux:** no `File` submenu; the about item lives in `Help`.
|
||||
*
|
||||
* Useful as a starting point you then extend with {@linkcode Menu.append},
|
||||
* {@linkcode Menu.insert} or {@linkcode Menu.prepend}.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Menu, Submenu } from '@tauri-apps/api/menu';
|
||||
*
|
||||
* const menu = await Menu.default();
|
||||
* await menu.append(await Submenu.new({ text: 'Tools', items: [{ id: 'fmt', text: 'Format' }] }));
|
||||
* await menu.setAsAppMenu();
|
||||
* ```
|
||||
*/
|
||||
static async default(): Promise<Menu> {
|
||||
return invoke<[number, string]>('plugin:menu|create_default').then(
|
||||
([rid, id]) => new Menu(rid, id)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a menu item to the end of this menu.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}.
|
||||
*/
|
||||
async append<
|
||||
T extends
|
||||
| Submenu
|
||||
| MenuItem
|
||||
| PredefinedMenuItem
|
||||
| CheckMenuItem
|
||||
| IconMenuItem
|
||||
| MenuItemOptions
|
||||
| SubmenuOptions
|
||||
| IconMenuItemOptions
|
||||
| PredefinedMenuItemOptions
|
||||
| CheckMenuItemOptions
|
||||
>(items: T | T[]): Promise<void> {
|
||||
return invoke('plugin:menu|append', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
items: (Array.isArray(items) ? items : [items]).map((i) =>
|
||||
'rid' in i ? [i.rid, i.kind] : i
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a menu item to the beginning of this menu.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}.
|
||||
*/
|
||||
async prepend<
|
||||
T extends
|
||||
| Submenu
|
||||
| MenuItem
|
||||
| PredefinedMenuItem
|
||||
| CheckMenuItem
|
||||
| IconMenuItem
|
||||
| MenuItemOptions
|
||||
| SubmenuOptions
|
||||
| IconMenuItemOptions
|
||||
| PredefinedMenuItemOptions
|
||||
| CheckMenuItemOptions
|
||||
>(items: T | T[]): Promise<void> {
|
||||
return invoke('plugin:menu|prepend', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
items: (Array.isArray(items) ? items : [items]).map((i) =>
|
||||
'rid' in i ? [i.rid, i.kind] : i
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a menu item to the specified position in this menu.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}.
|
||||
*/
|
||||
async insert<
|
||||
T extends
|
||||
| Submenu
|
||||
| MenuItem
|
||||
| PredefinedMenuItem
|
||||
| CheckMenuItem
|
||||
| IconMenuItem
|
||||
| MenuItemOptions
|
||||
| SubmenuOptions
|
||||
| IconMenuItemOptions
|
||||
| PredefinedMenuItemOptions
|
||||
| CheckMenuItemOptions
|
||||
>(items: T | T[], position: number): Promise<void> {
|
||||
return invoke('plugin:menu|insert', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
items: (Array.isArray(items) ? items : [items]).map((i) =>
|
||||
'rid' in i ? [i.rid, i.kind] : i
|
||||
),
|
||||
position
|
||||
})
|
||||
}
|
||||
|
||||
/** Remove a menu item from this menu. */
|
||||
async remove(
|
||||
item: Submenu | MenuItem | PredefinedMenuItem | CheckMenuItem | IconMenuItem
|
||||
): Promise<void> {
|
||||
return invoke('plugin:menu|remove', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
item: [item.rid, item.kind]
|
||||
})
|
||||
}
|
||||
|
||||
/** Remove a menu item from this menu at the specified position. */
|
||||
async removeAt(
|
||||
position: number
|
||||
): Promise<
|
||||
| Submenu
|
||||
| MenuItem
|
||||
| PredefinedMenuItem
|
||||
| CheckMenuItem
|
||||
| IconMenuItem
|
||||
| null
|
||||
> {
|
||||
return invoke<[number, string, ItemKind]>('plugin:menu|remove_at', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
position
|
||||
}).then(itemFromKind)
|
||||
}
|
||||
|
||||
/** Returns a list of menu items that has been added to this menu. */
|
||||
async items(): Promise<
|
||||
Array<
|
||||
Submenu | MenuItem | PredefinedMenuItem | CheckMenuItem | IconMenuItem
|
||||
>
|
||||
> {
|
||||
return invoke<Array<[number, string, ItemKind]>>('plugin:menu|items', {
|
||||
rid: this.rid,
|
||||
kind: this.kind
|
||||
}).then((i) => i.map(itemFromKind))
|
||||
}
|
||||
|
||||
/** Retrieves the menu item matching the given identifier. */
|
||||
async get(
|
||||
id: string
|
||||
): Promise<
|
||||
| Submenu
|
||||
| MenuItem
|
||||
| PredefinedMenuItem
|
||||
| CheckMenuItem
|
||||
| IconMenuItem
|
||||
| null
|
||||
> {
|
||||
return invoke<[number, string, ItemKind] | null>('plugin:menu|get', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
id
|
||||
}).then((r) => (r ? itemFromKind(r) : null))
|
||||
}
|
||||
|
||||
/**
|
||||
* Popup this menu as a context menu on the specified window.
|
||||
*
|
||||
* Call it from a `contextmenu` DOM listener (and `preventDefault()` on the event)
|
||||
* to replace the webview context menu with a native one. The promise resolves as
|
||||
* soon as the menu is shown, not when an item is picked: use each item's `action`
|
||||
* handler for that.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Menu } from '@tauri-apps/api/menu';
|
||||
* import { LogicalPosition } from '@tauri-apps/api/dpi';
|
||||
*
|
||||
* const menu = await Menu.new({
|
||||
* items: [{ id: 'copy', text: 'Copy', action: () => console.log('copy') }]
|
||||
* });
|
||||
*
|
||||
* document.addEventListener('contextmenu', (event) => {
|
||||
* event.preventDefault();
|
||||
* void menu.popup(new LogicalPosition(event.clientX, event.clientY));
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param at If a position is provided, it is relative to the window's top-left corner.
|
||||
* If there isn't one provided, the menu will pop up at the current location of the mouse.
|
||||
* @param window The window to show the menu on. Defaults to the current window.
|
||||
*/
|
||||
async popup(
|
||||
at?: PhysicalPosition | LogicalPosition | Position,
|
||||
window?: Window
|
||||
): Promise<void> {
|
||||
return invoke('plugin:menu|popup', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
window: window?.label ?? null,
|
||||
at: at instanceof Position ? at : at ? new Position(at) : null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the app-wide menu and returns the previous one.
|
||||
*
|
||||
* If a window was not created with an explicit menu or had one set explicitly,
|
||||
* this menu will be assigned to it.
|
||||
*
|
||||
* This is the menu bar shown at the top of the screen on macOS. On Windows and
|
||||
* Linux, where menus belong to a window, use {@linkcode Menu.setAsWindowMenu}
|
||||
* to target one window instead.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Menu } from '@tauri-apps/api/menu';
|
||||
*
|
||||
* const menu = await Menu.default();
|
||||
* const previous = await menu.setAsAppMenu();
|
||||
* await previous?.close();
|
||||
* ```
|
||||
*
|
||||
* @returns The menu that was set before this call, or `null` if there was none.
|
||||
*/
|
||||
async setAsAppMenu(): Promise<Menu | null> {
|
||||
return invoke<[number, string] | null>('plugin:menu|set_as_app_menu', {
|
||||
rid: this.rid
|
||||
}).then((r) => (r ? new Menu(r[0], r[1]) : null))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the window menu and returns the previous one.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one
|
||||
* window, if you need to set it, use {@linkcode Menu.setAsAppMenu} instead.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Menu } from '@tauri-apps/api/menu';
|
||||
* import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
*
|
||||
* const menu = await Menu.default();
|
||||
* await menu.setAsWindowMenu(getCurrentWindow());
|
||||
* ```
|
||||
*
|
||||
* @param window The window to set the menu on. Defaults to the current window.
|
||||
* @returns The menu that was set on that window before this call, or `null` if there was none.
|
||||
*/
|
||||
async setAsWindowMenu(window?: Window): Promise<Menu | null> {
|
||||
return invoke<[number, string] | null>('plugin:menu|set_as_window_menu', {
|
||||
rid: this.rid,
|
||||
window: window?.label ?? null
|
||||
}).then((r) => (r ? new Menu(r[0], r[1]) : null))
|
||||
}
|
||||
}
|
||||
126
packages/api/src/menu/menuItem.ts
Normal file
126
packages/api/src/menu/menuItem.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { MenuItemBase, newMenu } from './base'
|
||||
import { invoke } from '../core'
|
||||
|
||||
/** Options for creating a new menu item. */
|
||||
export interface MenuItemOptions {
|
||||
/** Specify an id to use for the new menu item. */
|
||||
id?: string
|
||||
/** The text of the new menu item. */
|
||||
text: string
|
||||
/** Whether the new menu item is enabled or not. */
|
||||
enabled?: boolean
|
||||
/**
|
||||
* Specify an accelerator (keyboard shortcut) for the new menu item, for example
|
||||
* `'CmdOrCtrl+Shift+K'`.
|
||||
*
|
||||
* The string is a list of zero or more modifiers followed by exactly one key,
|
||||
* joined with `+`. Matching is case insensitive and spaces around each token are
|
||||
* ignored, so `'CmdOrCtrl+Shift+K'` and `'cmdorctrl + shift + k'` are equivalent.
|
||||
* All modifiers must come before the key.
|
||||
*
|
||||
* Accepted modifiers:
|
||||
*
|
||||
* - `Shift`
|
||||
* - `Control`, `Ctrl`
|
||||
* - `Alt`, `Option`
|
||||
* - `Command`, `Cmd`, `Super`
|
||||
* - `CmdOrCtrl`, `CmdOrControl`, `CommandOrCtrl`, `CommandOrControl` — `Command`
|
||||
* on macOS and `Control` everywhere else, which is what you usually want for
|
||||
* application shortcuts.
|
||||
*
|
||||
* Accepted keys:
|
||||
*
|
||||
* - Letters `A`-`Z` (also written `KeyA`-`KeyZ`) and digits `0`-`9` (also `Digit0`-`Digit9`).
|
||||
* - Punctuation, either as the character or by name: `` ` ``/`Backquote`, `\`/`Backslash`,
|
||||
* `[`/`BracketLeft`, `]`/`BracketRight`, `,`/`Comma`, `=`/`Equal`, `-`/`Minus`,
|
||||
* `.`/`Period`, `'`/`Quote`, `;`/`Semicolon`, `/`/`Slash`.
|
||||
* - `Backspace`, `CapsLock`, `Enter`, `Space`, `Tab`, `Delete`, `End`, `Home`,
|
||||
* `Insert`, `PageDown`, `PageUp`, `PrintScreen`, `ScrollLock`, `NumLock`,
|
||||
* `Escape` (also `Esc`).
|
||||
* - Arrows: `ArrowUp`, `ArrowDown`, `ArrowLeft`, `ArrowRight` (also `Up`, `Down`, `Left`, `Right`).
|
||||
* - Function keys `F1` through `F24`.
|
||||
* - Numpad keys: `Numpad0`-`Numpad9` (also `Num0`-`Num9`), `NumpadAdd`, `NumpadSubtract`,
|
||||
* `NumpadMultiply`, `NumpadDivide`, `NumpadDecimal`, `NumpadEnter`, `NumpadEqual`
|
||||
* (each also accepted with the `Num` prefix, e.g. `NumAdd`).
|
||||
* - Media keys: `AudioVolumeUp`, `AudioVolumeDown`, `AudioVolumeMute` (also `VolumeUp`,
|
||||
* `VolumeDown`, `VolumeMute`).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { MenuItem } from '@tauri-apps/api/menu';
|
||||
*
|
||||
* await MenuItem.new({ text: 'Find', accelerator: 'CmdOrCtrl+F' });
|
||||
* await MenuItem.new({ text: 'Command palette', accelerator: 'CmdOrCtrl+Shift+P' });
|
||||
* await MenuItem.new({ text: 'Refresh', accelerator: 'F5' });
|
||||
* ```
|
||||
*
|
||||
* An accelerator that cannot be parsed is ignored and the item is created
|
||||
* without a shortcut, so double-check the spelling of the modifiers and key.
|
||||
*/
|
||||
accelerator?: string
|
||||
/** Specify a handler to be called when this menu item is activated. */
|
||||
action?: (id: string) => void
|
||||
}
|
||||
|
||||
/** A menu item inside a {@linkcode Menu} or {@linkcode Submenu} and contains only text. */
|
||||
export class MenuItem extends MenuItemBase {
|
||||
/** @ignore */
|
||||
protected constructor(rid: number, id: string) {
|
||||
super(rid, id, 'MenuItem')
|
||||
}
|
||||
|
||||
/** Create a new menu item. */
|
||||
static async new(opts: MenuItemOptions): Promise<MenuItem> {
|
||||
return newMenu('MenuItem', opts).then(([rid, id]) => new MenuItem(rid, id))
|
||||
}
|
||||
|
||||
/** Returns the text of this menu item. */
|
||||
async text(): Promise<string> {
|
||||
return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind })
|
||||
}
|
||||
|
||||
/** Sets the text for this menu item. */
|
||||
async setText(text: string): Promise<void> {
|
||||
return invoke('plugin:menu|set_text', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
text
|
||||
})
|
||||
}
|
||||
|
||||
/** Returns whether this menu item is enabled or not. */
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind })
|
||||
}
|
||||
|
||||
/** Sets whether this menu item is enabled or not. */
|
||||
async setEnabled(enabled: boolean): Promise<void> {
|
||||
return invoke('plugin:menu|set_enabled', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
enabled
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the accelerator for this menu item, or removes it when given `null`.
|
||||
*
|
||||
* See {@linkcode MenuItemOptions.accelerator} for the accepted format.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await item.setAccelerator('CmdOrCtrl+Shift+K');
|
||||
* ```
|
||||
*/
|
||||
async setAccelerator(accelerator: string | null): Promise<void> {
|
||||
return invoke('plugin:menu|set_accelerator', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
accelerator
|
||||
})
|
||||
}
|
||||
}
|
||||
203
packages/api/src/menu/predefinedMenuItem.ts
Normal file
203
packages/api/src/menu/predefinedMenuItem.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { MenuItemBase, newMenu } from './base'
|
||||
import { invoke } from '../core'
|
||||
import type { JsImage } from '../image'
|
||||
|
||||
/** A metadata for the about predefined menu item. */
|
||||
export interface AboutMetadata {
|
||||
/** Sets the application name. */
|
||||
name?: string
|
||||
/** The application version. */
|
||||
version?: string
|
||||
/**
|
||||
* The short version, e.g. "1.0".
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Windows / Linux:** Appended to the end of `version` in parentheses.
|
||||
*/
|
||||
shortVersion?: string
|
||||
/**
|
||||
* The authors of the application.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **macOS:** Unsupported.
|
||||
*/
|
||||
authors?: string[]
|
||||
/**
|
||||
* Application comments.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **macOS:** Unsupported.
|
||||
*/
|
||||
comments?: string
|
||||
/** The copyright of the application. */
|
||||
copyright?: string
|
||||
/**
|
||||
* The license of the application.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **macOS:** Unsupported.
|
||||
*/
|
||||
license?: string
|
||||
/**
|
||||
* The application website.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **macOS:** Unsupported.
|
||||
*/
|
||||
website?: string
|
||||
/**
|
||||
* The website label.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **macOS:** Unsupported.
|
||||
*/
|
||||
websiteLabel?: string
|
||||
/**
|
||||
* The credits.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Windows / Linux:** Unsupported.
|
||||
*/
|
||||
credits?: string
|
||||
/**
|
||||
* The application icon.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Windows:** Unsupported.
|
||||
*/
|
||||
icon?: JsImage
|
||||
}
|
||||
|
||||
/** Options for creating a new predefined menu item. */
|
||||
export interface PredefinedMenuItemOptions {
|
||||
/**
|
||||
* The text of the new predefined menu item.
|
||||
*
|
||||
* Defaults to the platform's own label for the chosen item, so only set it to
|
||||
* override or localize that label.
|
||||
*/
|
||||
text?: string
|
||||
/**
|
||||
* The predefined item type.
|
||||
*
|
||||
* Each variant maps to a native menu item whose behavior is implemented by the
|
||||
* operating system, so there is no `action` to handle. Variants that are
|
||||
* unsupported on a platform still create an item, but it does nothing there.
|
||||
*
|
||||
* - `Separator`: a horizontal separator line. All platforms.
|
||||
* - `Copy`, `Cut`, `Paste`, `SelectAll`: the standard clipboard and selection
|
||||
* commands, applied to the focused text input. All platforms.
|
||||
* - `Undo`, `Redo`: undo/redo in the focused text input. **macOS only**
|
||||
* (*Windows / Linux:* unsupported).
|
||||
* - `Minimize`: minimizes the focused window. *Linux:* unsupported.
|
||||
* - `Maximize`: maximizes the focused window. *Linux:* unsupported.
|
||||
* - `Fullscreen`: toggles fullscreen for the focused window. **macOS only**
|
||||
* (*Windows / Linux:* unsupported).
|
||||
* - `Hide`: hides the application. *Linux:* unsupported.
|
||||
* - `HideOthers`: hides every other application. *Linux:* unsupported.
|
||||
* - `ShowAll`: shows all hidden applications. **macOS only**
|
||||
* (*Windows / Linux:* unsupported).
|
||||
* - `CloseWindow`: closes the focused window. *Linux:* unsupported.
|
||||
* - `Quit`: quits the application. *Linux:* unsupported.
|
||||
* - `Services`: the macOS *Services* submenu. **macOS only**
|
||||
* (*Windows / Linux:* unsupported).
|
||||
* - `BringAllToFront`: brings all of the app's windows to the front. **macOS only**
|
||||
* (*Windows / Linux:* unsupported).
|
||||
* - `{ About: AboutMetadata | null }`: opens an about dialog. All platforms; pass
|
||||
* `null` to use the values from your `tauri.conf.json`, or see
|
||||
* {@linkcode AboutMetadata} for the per-field platform support.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Menu, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu';
|
||||
*
|
||||
* const edit = await Submenu.new({
|
||||
* text: 'Edit',
|
||||
* items: [
|
||||
* { item: 'Undo' },
|
||||
* { item: 'Redo' },
|
||||
* { item: 'Separator' },
|
||||
* { item: 'Cut' },
|
||||
* { item: 'Copy' },
|
||||
* { item: 'Paste' },
|
||||
* { item: 'SelectAll' }
|
||||
* ]
|
||||
* });
|
||||
*
|
||||
* const quit = await PredefinedMenuItem.new({ item: 'Quit', text: 'Quit my app' });
|
||||
* const menu = await Menu.new({ items: [edit, quit] });
|
||||
* ```
|
||||
*/
|
||||
item:
|
||||
| 'Separator'
|
||||
| 'Copy'
|
||||
| 'Cut'
|
||||
| 'Paste'
|
||||
| 'SelectAll'
|
||||
| 'Undo'
|
||||
| 'Redo'
|
||||
| 'Minimize'
|
||||
| 'Maximize'
|
||||
| 'Fullscreen'
|
||||
| 'Hide'
|
||||
| 'HideOthers'
|
||||
| 'ShowAll'
|
||||
| 'CloseWindow'
|
||||
| 'Quit'
|
||||
| 'Services'
|
||||
| 'BringAllToFront'
|
||||
| {
|
||||
About: AboutMetadata | null
|
||||
}
|
||||
}
|
||||
|
||||
/** A predefined (native) menu item which has a predefined behavior by the OS or by tauri. */
|
||||
export class PredefinedMenuItem extends MenuItemBase {
|
||||
/** @ignore */
|
||||
protected constructor(rid: number, id: string) {
|
||||
super(rid, id, 'Predefined')
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new predefined menu item.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { PredefinedMenuItem } from '@tauri-apps/api/menu';
|
||||
* const separator = await PredefinedMenuItem.new({ item: 'Separator' });
|
||||
* ```
|
||||
*/
|
||||
static async new(
|
||||
opts?: PredefinedMenuItemOptions
|
||||
): Promise<PredefinedMenuItem> {
|
||||
return newMenu('Predefined', opts).then(
|
||||
([rid, id]) => new PredefinedMenuItem(rid, id)
|
||||
)
|
||||
}
|
||||
|
||||
/** Returns the text of this predefined menu item. */
|
||||
async text(): Promise<string> {
|
||||
return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind })
|
||||
}
|
||||
|
||||
/** Sets the text for this predefined menu item. */
|
||||
async setText(text: string): Promise<void> {
|
||||
return invoke('plugin:menu|set_text', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
text
|
||||
})
|
||||
}
|
||||
}
|
||||
305
packages/api/src/menu/submenu.ts
Normal file
305
packages/api/src/menu/submenu.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import {
|
||||
IconMenuItemOptions,
|
||||
PredefinedMenuItemOptions,
|
||||
CheckMenuItemOptions
|
||||
} from '../menu'
|
||||
import { MenuItem, type MenuItemOptions } from './menuItem'
|
||||
import { CheckMenuItem } from './checkMenuItem'
|
||||
import { IconMenuItem } from './iconMenuItem'
|
||||
import { PredefinedMenuItem } from './predefinedMenuItem'
|
||||
import { invoke } from '../core'
|
||||
import { type LogicalPosition, PhysicalPosition, type Window } from '../window'
|
||||
import { type ItemKind, MenuItemBase, newMenu } from './base'
|
||||
import { type MenuOptions } from './menu'
|
||||
import { Position } from '../dpi'
|
||||
import { transformImage, MenuIcon } from '../image'
|
||||
|
||||
/** @ignore */
|
||||
export function itemFromKind([rid, id, kind]: [number, string, ItemKind]):
|
||||
| Submenu
|
||||
| MenuItem
|
||||
| PredefinedMenuItem
|
||||
| CheckMenuItem
|
||||
| IconMenuItem {
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
switch (kind) {
|
||||
case 'Submenu':
|
||||
// @ts-expect-error constructor is protected for external usage only, safe for us to use
|
||||
return new Submenu(rid, id)
|
||||
case 'Predefined':
|
||||
// @ts-expect-error constructor is protected for external usage only, safe for us to use
|
||||
return new PredefinedMenuItem(rid, id)
|
||||
case 'Check':
|
||||
// @ts-expect-error constructor is protected for external usage only, safe for us to use
|
||||
return new CheckMenuItem(rid, id)
|
||||
case 'Icon':
|
||||
// @ts-expect-error constructor is protected for external usage only, safe for us to use
|
||||
return new IconMenuItem(rid, id)
|
||||
case 'MenuItem':
|
||||
default:
|
||||
// @ts-expect-error constructor is protected for external usage only, safe for us to use
|
||||
return new MenuItem(rid, id)
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-return */
|
||||
}
|
||||
|
||||
export type SubmenuOptions = (Omit<MenuItemOptions, 'accelerator' | 'action'>
|
||||
& MenuOptions) & {
|
||||
/**
|
||||
* Icon to be used for the submenu.
|
||||
* Note: you may need the `image-ico` or `image-png` Cargo features to use this API.
|
||||
*/
|
||||
icon?: MenuIcon
|
||||
}
|
||||
|
||||
/** A type that is a submenu inside a {@linkcode Menu} or {@linkcode Submenu}. */
|
||||
export class Submenu extends MenuItemBase {
|
||||
/** @ignore */
|
||||
protected constructor(rid: number, id: string) {
|
||||
super(rid, id, 'Submenu')
|
||||
}
|
||||
|
||||
/** Create a new submenu. */
|
||||
static async new(opts: SubmenuOptions): Promise<Submenu> {
|
||||
return newMenu('Submenu', opts).then(([rid, id]) => new Submenu(rid, id))
|
||||
}
|
||||
|
||||
/** Returns the text of this submenu. */
|
||||
async text(): Promise<string> {
|
||||
return invoke('plugin:menu|text', { rid: this.rid, kind: this.kind })
|
||||
}
|
||||
|
||||
/** Sets the text for this submenu. */
|
||||
async setText(text: string): Promise<void> {
|
||||
return invoke('plugin:menu|set_text', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
text
|
||||
})
|
||||
}
|
||||
|
||||
/** Returns whether this submenu is enabled or not. */
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return invoke('plugin:menu|is_enabled', { rid: this.rid, kind: this.kind })
|
||||
}
|
||||
|
||||
/** Sets whether this submenu is enabled or not. */
|
||||
async setEnabled(enabled: boolean): Promise<void> {
|
||||
return invoke('plugin:menu|set_enabled', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
enabled
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a menu item to the end of this submenu.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}.
|
||||
*/
|
||||
async append<
|
||||
T extends
|
||||
| Submenu
|
||||
| MenuItem
|
||||
| PredefinedMenuItem
|
||||
| CheckMenuItem
|
||||
| IconMenuItem
|
||||
| MenuItemOptions
|
||||
| SubmenuOptions
|
||||
| IconMenuItemOptions
|
||||
| PredefinedMenuItemOptions
|
||||
| CheckMenuItemOptions
|
||||
>(items: T | T[]): Promise<void> {
|
||||
return invoke('plugin:menu|append', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
items: (Array.isArray(items) ? items : [items]).map((i) =>
|
||||
'rid' in i ? [i.rid, i.kind] : i
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a menu item to the beginning of this submenu.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}.
|
||||
*/
|
||||
async prepend<
|
||||
T extends
|
||||
| Submenu
|
||||
| MenuItem
|
||||
| PredefinedMenuItem
|
||||
| CheckMenuItem
|
||||
| IconMenuItem
|
||||
| MenuItemOptions
|
||||
| SubmenuOptions
|
||||
| IconMenuItemOptions
|
||||
| PredefinedMenuItemOptions
|
||||
| CheckMenuItemOptions
|
||||
>(items: T | T[]): Promise<void> {
|
||||
return invoke('plugin:menu|prepend', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
items: (Array.isArray(items) ? items : [items]).map((i) =>
|
||||
'rid' in i ? [i.rid, i.kind] : i
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a menu item to the specified position in this submenu.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **macOS:** Only {@linkcode Submenu}s can be added to a {@linkcode Menu}.
|
||||
*/
|
||||
async insert<
|
||||
T extends
|
||||
| Submenu
|
||||
| MenuItem
|
||||
| PredefinedMenuItem
|
||||
| CheckMenuItem
|
||||
| IconMenuItem
|
||||
| MenuItemOptions
|
||||
| SubmenuOptions
|
||||
| IconMenuItemOptions
|
||||
| PredefinedMenuItemOptions
|
||||
| CheckMenuItemOptions
|
||||
>(items: T | T[], position: number): Promise<void> {
|
||||
return invoke('plugin:menu|insert', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
items: (Array.isArray(items) ? items : [items]).map((i) =>
|
||||
'rid' in i ? [i.rid, i.kind] : i
|
||||
),
|
||||
position
|
||||
})
|
||||
}
|
||||
|
||||
/** Remove a menu item from this submenu. */
|
||||
async remove(
|
||||
item: Submenu | MenuItem | PredefinedMenuItem | CheckMenuItem | IconMenuItem
|
||||
): Promise<void> {
|
||||
return invoke('plugin:menu|remove', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
item: [item.rid, item.kind]
|
||||
})
|
||||
}
|
||||
|
||||
/** Remove a menu item from this submenu at the specified position. */
|
||||
async removeAt(
|
||||
position: number
|
||||
): Promise<
|
||||
| Submenu
|
||||
| MenuItem
|
||||
| PredefinedMenuItem
|
||||
| CheckMenuItem
|
||||
| IconMenuItem
|
||||
| null
|
||||
> {
|
||||
return invoke<[number, string, ItemKind]>('plugin:menu|remove_at', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
position
|
||||
}).then(itemFromKind)
|
||||
}
|
||||
|
||||
/** Returns a list of menu items that has been added to this submenu. */
|
||||
async items(): Promise<
|
||||
Array<
|
||||
Submenu | MenuItem | PredefinedMenuItem | CheckMenuItem | IconMenuItem
|
||||
>
|
||||
> {
|
||||
return invoke<Array<[number, string, ItemKind]>>('plugin:menu|items', {
|
||||
rid: this.rid,
|
||||
kind: this.kind
|
||||
}).then((i) => i.map(itemFromKind))
|
||||
}
|
||||
|
||||
/** Retrieves the menu item matching the given identifier. */
|
||||
async get(
|
||||
id: string
|
||||
): Promise<
|
||||
| Submenu
|
||||
| MenuItem
|
||||
| PredefinedMenuItem
|
||||
| CheckMenuItem
|
||||
| IconMenuItem
|
||||
| null
|
||||
> {
|
||||
return invoke<[number, string, ItemKind] | null>('plugin:menu|get', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
id
|
||||
}).then((r) => (r ? itemFromKind(r) : null))
|
||||
}
|
||||
|
||||
/**
|
||||
* Popup this submenu as a context menu on the specified window.
|
||||
*
|
||||
* If the position, is provided, it is relative to the window's top-left corner.
|
||||
*/
|
||||
async popup(
|
||||
at?: PhysicalPosition | LogicalPosition,
|
||||
window?: Window
|
||||
): Promise<void> {
|
||||
return invoke('plugin:menu|popup', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
window: window?.label ?? null,
|
||||
at: at instanceof Position ? at : at ? new Position(at) : null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this submenu as the Window menu for the application on macOS.
|
||||
*
|
||||
* This will cause macOS to automatically add window-switching items and
|
||||
* certain other items to the menu.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **Windows / Linux**: Unsupported.
|
||||
*/
|
||||
async setAsWindowsMenuForNSApp(): Promise<void> {
|
||||
return invoke('plugin:menu|set_as_windows_menu_for_nsapp', {
|
||||
rid: this.rid
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this submenu as the Help menu for the application on macOS.
|
||||
*
|
||||
* This will cause macOS to automatically add a search box to the menu.
|
||||
*
|
||||
* If no menu is set as the Help menu, macOS will automatically use any menu
|
||||
* which has a title matching the localized word "Help".
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **Windows / Linux**: Unsupported.
|
||||
*/
|
||||
async setAsHelpMenuForNSApp(): Promise<void> {
|
||||
return invoke('plugin:menu|set_as_help_menu_for_nsapp', {
|
||||
rid: this.rid
|
||||
})
|
||||
}
|
||||
|
||||
/** Sets an icon for this submenu */
|
||||
async setIcon(icon: MenuIcon | null): Promise<void> {
|
||||
return invoke('plugin:menu|set_icon', {
|
||||
rid: this.rid,
|
||||
kind: this.kind,
|
||||
icon: transformImage(icon)
|
||||
})
|
||||
}
|
||||
}
|
||||
380
packages/api/src/mocks.ts
Normal file
380
packages/api/src/mocks.ts
Normal file
@@ -0,0 +1,380 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* Test helpers that stand in for the Tauri backend.
|
||||
*
|
||||
* Importing any other module of this package in a plain Node/browser test
|
||||
* environment fails, because there is no `window.__TAURI_INTERNALS__` to talk to.
|
||||
* The functions here install a fake one: {@linkcode mockIPC} intercepts `invoke`
|
||||
* calls, {@linkcode mockWindows} makes the window/webview APIs believe a set of
|
||||
* windows exists, and {@linkcode mockConvertFileSrc} makes `convertFileSrc` return
|
||||
* the URL shape of a given platform.
|
||||
*
|
||||
* Call {@linkcode clearMocks} after each test when your runner reuses the same
|
||||
* `window` object between tests.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { beforeEach, afterEach, expect, test } from 'vitest';
|
||||
* import { mockIPC, clearMocks } from '@tauri-apps/api/mocks';
|
||||
* import { invoke } from '@tauri-apps/api/core';
|
||||
*
|
||||
* afterEach(() => {
|
||||
* clearMocks();
|
||||
* });
|
||||
*
|
||||
* test('greet', async () => {
|
||||
* mockIPC((cmd, args) => {
|
||||
* if (cmd === 'greet') {
|
||||
* return `Hello, ${(args as { name: string }).name}!`;
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* await expect(invoke('greet', { name: 'tauri' })).resolves.toBe('Hello, tauri!');
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* This package is also accessible with `window.__TAURI__.mocks` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { InvokeArgs, InvokeOptions } from './core'
|
||||
import { EventName } from './event'
|
||||
|
||||
function mockInternals() {
|
||||
window.__TAURI_INTERNALS__ = window.__TAURI_INTERNALS__ ?? {}
|
||||
window.__TAURI_EVENT_PLUGIN_INTERNALS__ =
|
||||
window.__TAURI_EVENT_PLUGIN_INTERNALS__ ?? {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for `mockIPC`.
|
||||
*
|
||||
* # Options
|
||||
* `shouldMockEvents`: If true, the `listen` and `emit` functions will be mocked, allowing you to test event handling without a real backend.
|
||||
* **This will consume any events emitted with the `plugin:event` prefix.**
|
||||
*
|
||||
* @since 2.7.0
|
||||
*/
|
||||
export interface MockIPCOptions {
|
||||
shouldMockEvents?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts all IPC requests with the given mock handler.
|
||||
*
|
||||
* This function can be used when testing tauri frontend applications or when running the frontend in a Node.js context during static site generation.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* Testing setup using Vitest:
|
||||
* ```ts
|
||||
* import { mockIPC, clearMocks } from "@tauri-apps/api/mocks"
|
||||
* import { invoke } from "@tauri-apps/api/core"
|
||||
*
|
||||
* afterEach(() => {
|
||||
* clearMocks()
|
||||
* })
|
||||
*
|
||||
* test("mocked command", () => {
|
||||
* mockIPC((cmd, payload) => {
|
||||
* switch (cmd) {
|
||||
* case "add":
|
||||
* return (payload.a as number) + (payload.b as number);
|
||||
* default:
|
||||
* break;
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* expect(invoke('add', { a: 12, b: 15 })).resolves.toBe(27);
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* The callback function can also return a Promise:
|
||||
* ```js
|
||||
* import { mockIPC, clearMocks } from "@tauri-apps/api/mocks"
|
||||
* import { invoke } from "@tauri-apps/api/core"
|
||||
*
|
||||
* afterEach(() => {
|
||||
* clearMocks()
|
||||
* })
|
||||
*
|
||||
* test("mocked command", () => {
|
||||
* mockIPC((cmd, payload) => {
|
||||
* if(cmd === "get_data") {
|
||||
* return fetch("https://example.com/data.json")
|
||||
* .then((response) => response.json())
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* expect(invoke('get_data')).resolves.toBe({ foo: 'bar' });
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* `listen` can also be mocked with direct calls to the `emit` function. This functionality is opt-in via the `shouldMockEvents` option:
|
||||
* ```js
|
||||
* import { mockIPC, clearMocks } from "@tauri-apps/api/mocks"
|
||||
* import { emit, listen } from "@tauri-apps/api/event"
|
||||
*
|
||||
* afterEach(() => {
|
||||
* clearMocks()
|
||||
* })
|
||||
*
|
||||
* test("mocked event", () => {
|
||||
* mockIPC(() => {}, { shouldMockEvents: true }); // enable event mocking
|
||||
*
|
||||
* const eventHandler = vi.fn();
|
||||
* listen('test-event', eventHandler); // typically in component setup or similar
|
||||
*
|
||||
* emit('test-event', { foo: 'bar' });
|
||||
* expect(eventHandler).toHaveBeenCalledWith({
|
||||
* event: 'test-event',
|
||||
* payload: { foo: 'bar' }
|
||||
* });
|
||||
* })
|
||||
* ```
|
||||
* `emitTo` is currently **not** supported by this mock implementation.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
export function mockIPC(
|
||||
cb: (cmd: string, payload?: InvokeArgs) => unknown,
|
||||
options?: MockIPCOptions
|
||||
): void {
|
||||
mockInternals()
|
||||
|
||||
function isEventPluginInvoke(cmd: string): boolean {
|
||||
return cmd.startsWith('plugin:event|')
|
||||
}
|
||||
|
||||
function handleEventPlugin(cmd: string, args?: InvokeArgs): unknown {
|
||||
switch (cmd) {
|
||||
case 'plugin:event|listen':
|
||||
return handleListen(args as { event: EventName; handler: number })
|
||||
case 'plugin:event|emit':
|
||||
return handleEmit(args as { event: EventName; payload?: unknown })
|
||||
case 'plugin:event|unlisten':
|
||||
return handleRemoveListener(args as { event: EventName; id: number })
|
||||
}
|
||||
}
|
||||
|
||||
const listeners = new Map<string, number[]>()
|
||||
function handleListen(args: { event: EventName; handler: number }) {
|
||||
if (!listeners.has(args.event)) {
|
||||
listeners.set(args.event, [])
|
||||
}
|
||||
listeners.get(args.event)!.push(args.handler)
|
||||
return args.handler
|
||||
}
|
||||
|
||||
function handleEmit(args: { event: EventName; payload?: unknown }) {
|
||||
const eventListeners = listeners.get(args.event) || []
|
||||
for (const handler of eventListeners) {
|
||||
runCallback(handler, args)
|
||||
}
|
||||
return null
|
||||
}
|
||||
function handleRemoveListener(args: { event: EventName; id: number }) {
|
||||
const eventListeners = listeners.get(args.event)
|
||||
if (eventListeners) {
|
||||
const index = eventListeners.indexOf(args.id)
|
||||
if (index !== -1) {
|
||||
eventListeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async function invoke<T>(
|
||||
cmd: string,
|
||||
args?: InvokeArgs,
|
||||
_options?: InvokeOptions
|
||||
): Promise<T> {
|
||||
if (options?.shouldMockEvents && isEventPluginInvoke(cmd)) {
|
||||
return handleEventPlugin(cmd, args) as T
|
||||
}
|
||||
|
||||
return cb(cmd, args) as T
|
||||
}
|
||||
|
||||
const callbacks = new Map<number, (data: unknown) => void>()
|
||||
|
||||
function registerCallback<T = unknown>(
|
||||
callback?: (response: T) => void,
|
||||
once = false
|
||||
) {
|
||||
const identifier = window.crypto.getRandomValues(new Uint32Array(1))[0]
|
||||
callbacks.set(identifier, (data) => {
|
||||
if (once) {
|
||||
unregisterCallback(identifier)
|
||||
}
|
||||
return callback && callback(data as T)
|
||||
})
|
||||
return identifier
|
||||
}
|
||||
|
||||
function unregisterCallback(id: number) {
|
||||
callbacks.delete(id)
|
||||
}
|
||||
|
||||
function runCallback(id: number, data: unknown) {
|
||||
const callback = callbacks.get(id)
|
||||
if (callback) {
|
||||
callback(data)
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`[TAURI] Couldn't find callback id ${id}. This might happen when the app is reloaded while Rust is running an asynchronous operation.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function unregisterListener(event: EventName, id: number) {
|
||||
unregisterCallback(id)
|
||||
}
|
||||
|
||||
window.__TAURI_INTERNALS__.invoke = invoke
|
||||
window.__TAURI_INTERNALS__.transformCallback = registerCallback
|
||||
window.__TAURI_INTERNALS__.unregisterCallback = unregisterCallback
|
||||
window.__TAURI_INTERNALS__.runCallback = runCallback
|
||||
window.__TAURI_INTERNALS__.callbacks = callbacks
|
||||
window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener =
|
||||
unregisterListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks one or many window labels.
|
||||
* In non-tauri context it is required to call this function *before* using the `@tauri-apps/api/window` module.
|
||||
*
|
||||
* This function only mocks the *presence* of windows,
|
||||
* window properties (e.g. width and height) can be mocked like regular IPC calls using the `mockIPC` function.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```js
|
||||
* import { mockWindows } from "@tauri-apps/api/mocks";
|
||||
* import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
*
|
||||
* mockWindows("main", "second", "third");
|
||||
*
|
||||
* const win = getCurrentWindow();
|
||||
*
|
||||
* win.label // "main"
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* import { mockWindows } from "@tauri-apps/api/mocks";
|
||||
*
|
||||
* mockWindows("main", "second", "third");
|
||||
*
|
||||
* mockIPC((cmd, args) => {
|
||||
* if (cmd === "plugin:event|emit") {
|
||||
* console.log('emit event', args?.event, args?.payload);
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* const { emit } = await import("@tauri-apps/api/event");
|
||||
* await emit('loaded'); // this will cause the mocked IPC handler to log to the console.
|
||||
* ```
|
||||
*
|
||||
* @param current Label of window this JavaScript context is running in.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
export function mockWindows(
|
||||
current: string,
|
||||
..._additionalWindows: string[]
|
||||
): void {
|
||||
mockInternals()
|
||||
window.__TAURI_INTERNALS__.metadata = {
|
||||
currentWindow: { label: current },
|
||||
currentWebview: { windowLabel: current, label: current }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock `convertFileSrc` function
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* import { mockConvertFileSrc } from "@tauri-apps/api/mocks";
|
||||
* import { convertFileSrc } from "@tauri-apps/api/core";
|
||||
*
|
||||
* mockConvertFileSrc("windows")
|
||||
*
|
||||
* const url = convertFileSrc("C:\\Users\\user\\file.txt")
|
||||
* ```
|
||||
*
|
||||
* @param osName The operating system to mock, can be one of linux, macos, or windows
|
||||
*
|
||||
* @since 1.6.0
|
||||
*/
|
||||
export function mockConvertFileSrc(osName: string): void {
|
||||
mockInternals()
|
||||
window.__TAURI_INTERNALS__.convertFileSrc = function (
|
||||
filePath,
|
||||
protocol = 'asset'
|
||||
) {
|
||||
const path = encodeURIComponent(filePath)
|
||||
return osName === 'windows'
|
||||
? `http://${protocol}.localhost/${path}`
|
||||
: `${protocol}://localhost/${path}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears mocked functions/data injected by the other functions in this module.
|
||||
* When using a test runner that doesn't provide a fresh window object for each test, calling this function will reset tauri specific properties.
|
||||
*
|
||||
* # Example
|
||||
*
|
||||
* ```js
|
||||
* import { mockWindows, clearMocks } from "@tauri-apps/api/mocks"
|
||||
*
|
||||
* afterEach(() => {
|
||||
* clearMocks()
|
||||
* })
|
||||
*
|
||||
* test("mocked windows", () => {
|
||||
* mockWindows("main", "second", "third");
|
||||
*
|
||||
* expect(window.__TAURI_INTERNALS__).toHaveProperty("metadata")
|
||||
* })
|
||||
*
|
||||
* test("no mocked windows", () => {
|
||||
* expect(window.__TAURI_INTERNALS__).not.toHaveProperty("metadata")
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
export function clearMocks(): void {
|
||||
if (typeof window.__TAURI_INTERNALS__ !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
// @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case
|
||||
delete window.__TAURI_INTERNALS__.invoke
|
||||
// @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case
|
||||
delete window.__TAURI_INTERNALS__.transformCallback
|
||||
// @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case
|
||||
delete window.__TAURI_INTERNALS__.unregisterCallback
|
||||
// @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case
|
||||
delete window.__TAURI_INTERNALS__.runCallback
|
||||
// @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case
|
||||
delete window.__TAURI_INTERNALS__.callbacks
|
||||
// @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case
|
||||
delete window.__TAURI_INTERNALS__.convertFileSrc
|
||||
// @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case
|
||||
delete window.__TAURI_INTERNALS__.metadata
|
||||
|
||||
if (typeof window.__TAURI_EVENT_PLUGIN_INTERNALS__ !== 'object') {
|
||||
return
|
||||
}
|
||||
// @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case
|
||||
delete window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener
|
||||
}
|
||||
853
packages/api/src/path.ts
Normal file
853
packages/api/src/path.ts
Normal file
@@ -0,0 +1,853 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* The path module provides utilities for working with file and directory paths.
|
||||
*
|
||||
* This package is also accessible with `window.__TAURI__.path` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`.
|
||||
*
|
||||
* It is recommended to allowlist only the APIs you use for optimal bundle size and security.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** The user directories are sandboxed per app. `desktopDir`,
|
||||
* `executableDir`, `fontDir`, `runtimeDir` and `templateDir` — and the matching
|
||||
* {@link BaseDirectory} variants — do not exist on Android at all. The remaining
|
||||
* ones resolve inside the app's own storage, so they are not shared with other
|
||||
* apps, and `videoDir` resolves to the app-specific Movies directory.
|
||||
* - **iOS:** Every directory resolves inside the app sandbox, so paths are private
|
||||
* to your app and change between installs. Resolve them at runtime instead of
|
||||
* persisting absolute paths.
|
||||
*
|
||||
* @remarks All commands used by this module are part of the `core:path:default`
|
||||
* permission set, which is enabled by default, so no extra capability
|
||||
* configuration is needed.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { invoke } from './core'
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
enum BaseDirectory {
|
||||
/**
|
||||
* @see {@link audioDir} for more information.
|
||||
*/
|
||||
Audio = 1,
|
||||
/**
|
||||
* @see {@link cacheDir} for more information.
|
||||
*/
|
||||
Cache = 2,
|
||||
/**
|
||||
* @see {@link configDir} for more information.
|
||||
*/
|
||||
Config = 3,
|
||||
/**
|
||||
* @see {@link dataDir} for more information.
|
||||
*/
|
||||
Data = 4,
|
||||
/**
|
||||
* @see {@link localDataDir} for more information.
|
||||
*/
|
||||
LocalData = 5,
|
||||
/**
|
||||
* @see {@link documentDir} for more information.
|
||||
*/
|
||||
Document = 6,
|
||||
/**
|
||||
* @see {@link downloadDir} for more information.
|
||||
*/
|
||||
Download = 7,
|
||||
/**
|
||||
* @see {@link pictureDir} for more information.
|
||||
*/
|
||||
Picture = 8,
|
||||
/**
|
||||
* @see {@link publicDir} for more information.
|
||||
*/
|
||||
Public = 9,
|
||||
/**
|
||||
* @see {@link videoDir} for more information.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** Resolves to the app-specific Movies directory since 2.12.0,
|
||||
* see {@link videoDir}.
|
||||
*/
|
||||
Video = 10,
|
||||
/**
|
||||
* @see {@link resourceDir} for more information.
|
||||
*/
|
||||
Resource = 11,
|
||||
/**
|
||||
* @see {@link tempDir} for more information.
|
||||
*/
|
||||
Temp = 12,
|
||||
/**
|
||||
* @see {@link appConfigDir} for more information.
|
||||
*/
|
||||
AppConfig = 13,
|
||||
/**
|
||||
* @see {@link appDataDir} for more information.
|
||||
*/
|
||||
AppData = 14,
|
||||
/**
|
||||
* @see {@link appLocalDataDir} for more information.
|
||||
*/
|
||||
AppLocalData = 15,
|
||||
/**
|
||||
* @see {@link appCacheDir} for more information.
|
||||
*/
|
||||
AppCache = 16,
|
||||
/**
|
||||
* @see {@link appLogDir} for more information.
|
||||
*/
|
||||
AppLog = 17,
|
||||
/**
|
||||
* @see {@link desktopDir} for more information.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** Unsupported, this variant does not exist on Android.
|
||||
*/
|
||||
Desktop = 18,
|
||||
/**
|
||||
* @see {@link executableDir} for more information.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** Unsupported, this variant does not exist on Android.
|
||||
*/
|
||||
Executable = 19,
|
||||
/**
|
||||
* @see {@link fontDir} for more information.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** Unsupported, this variant does not exist on Android.
|
||||
*/
|
||||
Font = 20,
|
||||
/**
|
||||
* @see {@link homeDir} for more information.
|
||||
*/
|
||||
Home = 21,
|
||||
/**
|
||||
* @see {@link runtimeDir} for more information.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** Unsupported, this variant does not exist on Android.
|
||||
*/
|
||||
Runtime = 22,
|
||||
/**
|
||||
* @see {@link templateDir} for more information.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** Unsupported, this variant does not exist on Android.
|
||||
*/
|
||||
Template = 23
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the suggested directory for your app's config files.
|
||||
* Resolves to `${configDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { appConfigDir } from '@tauri-apps/api/path';
|
||||
* const appConfigDirPath = await appConfigDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.2.0
|
||||
*/
|
||||
async function appConfigDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.AppConfig
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the suggested directory for your app's data files.
|
||||
* Resolves to `${dataDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { appDataDir } from '@tauri-apps/api/path';
|
||||
* const appDataDirPath = await appDataDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.2.0
|
||||
*/
|
||||
async function appDataDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.AppData
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the suggested directory for your app's local data files.
|
||||
* Resolves to `${localDataDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { appLocalDataDir } from '@tauri-apps/api/path';
|
||||
* const appLocalDataDirPath = await appLocalDataDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.2.0
|
||||
*/
|
||||
async function appLocalDataDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.AppLocalData
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the suggested directory for your app's cache files.
|
||||
* Resolves to `${cacheDir}/${bundleIdentifier}`, where `bundleIdentifier` is the [`identifier`](https://v2.tauri.app/reference/config/#identifier) value configured in `tauri.conf.json`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { appCacheDir } from '@tauri-apps/api/path';
|
||||
* const appCacheDirPath = await appCacheDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.2.0
|
||||
*/
|
||||
async function appCacheDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.AppCache
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's audio directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_MUSIC_DIR`.
|
||||
* - **macOS:** Resolves to `$HOME/Music`.
|
||||
* - **Windows:** Resolves to `{FOLDERID_Music}`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { audioDir } from '@tauri-apps/api/path';
|
||||
* const audioDirPath = await audioDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function audioDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Audio
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's cache directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Linux:** Resolves to `$XDG_CACHE_HOME` or `$HOME/.cache`.
|
||||
* - **macOS:** Resolves to `$HOME/Library/Caches`.
|
||||
* - **Windows:** Resolves to `{FOLDERID_LocalAppData}`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { cacheDir } from '@tauri-apps/api/path';
|
||||
* const cacheDirPath = await cacheDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function cacheDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Cache
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's config directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Linux:** Resolves to `$XDG_CONFIG_HOME` or `$HOME/.config`.
|
||||
* - **macOS:** Resolves to `$HOME/Library/Application Support`.
|
||||
* - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { configDir } from '@tauri-apps/api/path';
|
||||
* const configDirPath = await configDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function configDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Config
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's data directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`.
|
||||
* - **macOS:** Resolves to `$HOME/Library/Application Support`.
|
||||
* - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { dataDir } from '@tauri-apps/api/path';
|
||||
* const dataDirPath = await dataDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function dataDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's desktop directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** Unsupported. Android has no desktop directory, the
|
||||
* {@link BaseDirectory.Desktop} variant does not exist there and the call rejects.
|
||||
*
|
||||
* - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DESKTOP_DIR`.
|
||||
* - **macOS:** Resolves to `$HOME/Desktop`.
|
||||
* - **Windows:** Resolves to `{FOLDERID_Desktop}`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { desktopDir } from '@tauri-apps/api/path';
|
||||
* const desktopPath = await desktopDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function desktopDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Desktop
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's document directory.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { documentDir } from '@tauri-apps/api/path';
|
||||
* const documentDirPath = await documentDir();
|
||||
* ```
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOCUMENTS_DIR`.
|
||||
* - **macOS:** Resolves to `$HOME/Documents`.
|
||||
* - **Windows:** Resolves to `{FOLDERID_Documents}`.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function documentDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Document
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's download directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Linux**: Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOWNLOAD_DIR`.
|
||||
* - **macOS**: Resolves to `$HOME/Downloads`.
|
||||
* - **Windows**: Resolves to `{FOLDERID_Downloads}`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { downloadDir } from '@tauri-apps/api/path';
|
||||
* const downloadDirPath = await downloadDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function downloadDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Download
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's executable directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** Unsupported. The {@link BaseDirectory.Executable} variant does not
|
||||
* exist there and the call rejects.
|
||||
*
|
||||
* - **Linux:** Resolves to `$XDG_BIN_HOME/../bin` or `$XDG_DATA_HOME/../bin` or `$HOME/.local/bin`.
|
||||
* - **macOS:** Not supported.
|
||||
* - **Windows:** Not supported.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { executableDir } from '@tauri-apps/api/path';
|
||||
* const executableDirPath = await executableDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function executableDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Executable
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's font directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** Unsupported. The {@link BaseDirectory.Font} variant does not exist
|
||||
* there and the call rejects.
|
||||
*
|
||||
* - **Linux:** Resolves to `$XDG_DATA_HOME/fonts` or `$HOME/.local/share/fonts`.
|
||||
* - **macOS:** Resolves to `$HOME/Library/Fonts`.
|
||||
* - **Windows:** Not supported.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { fontDir } from '@tauri-apps/api/path';
|
||||
* const fontDirPath = await fontDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function fontDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Font
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's home directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Linux:** Resolves to `$HOME`.
|
||||
* - **macOS:** Resolves to `$HOME`.
|
||||
* - **Windows:** Resolves to `{FOLDERID_Profile}`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { homeDir } from '@tauri-apps/api/path';
|
||||
* const homeDirPath = await homeDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function homeDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Home
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's local data directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`.
|
||||
* - **macOS:** Resolves to `$HOME/Library/Application Support`.
|
||||
* - **Windows:** Resolves to `{FOLDERID_LocalAppData}`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { localDataDir } from '@tauri-apps/api/path';
|
||||
* const localDataDirPath = await localDataDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function localDataDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.LocalData
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's picture directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PICTURES_DIR`.
|
||||
* - **macOS:** Resolves to `$HOME/Pictures`.
|
||||
* - **Windows:** Resolves to `{FOLDERID_Pictures}`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { pictureDir } from '@tauri-apps/api/path';
|
||||
* const pictureDirPath = await pictureDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function pictureDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Picture
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's public directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PUBLICSHARE_DIR`.
|
||||
* - **macOS:** Resolves to `$HOME/Public`.
|
||||
* - **Windows:** Resolves to `{FOLDERID_Public}`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { publicDir } from '@tauri-apps/api/path';
|
||||
* const publicDirPath = await publicDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function publicDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Public
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the application's resource directory.
|
||||
* To resolve a resource path, see {@linkcode resolveResource}.
|
||||
*
|
||||
* ## Platform-specific
|
||||
*
|
||||
* Although we provide the exact path where this function resolves to,
|
||||
* this is not a contract and things might change in the future
|
||||
*
|
||||
* - **Windows:** Resolves to the directory that contains the main executable.
|
||||
* - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to
|
||||
* the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`.
|
||||
* If not running in an AppImage, the path is `/usr/lib/${exe_name}`.
|
||||
* When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`.
|
||||
* - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app).
|
||||
* - **iOS:** Resolves to `${exe_dir}/assets`.
|
||||
* - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path,
|
||||
* we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/),
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { resourceDir } from '@tauri-apps/api/path';
|
||||
* const resourceDirPath = await resourceDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function resourceDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Resource
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the path to a resource file.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { resolveResource } from '@tauri-apps/api/path';
|
||||
* const resourcePath = await resolveResource('script.sh');
|
||||
* ```
|
||||
*
|
||||
* @param resourcePath The path to the resource.
|
||||
* Must follow the same syntax as defined in `tauri.conf.json > bundle > resources`, i.e. keeping subfolders and parent dir components (`../`).
|
||||
* @returns The full path to the resource.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function resolveResource(resourcePath: string): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Resource,
|
||||
path: resourcePath
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's runtime directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** Unsupported. The {@link BaseDirectory.Runtime} variant does not
|
||||
* exist there and the call rejects.
|
||||
*
|
||||
* - **Linux:** Resolves to `$XDG_RUNTIME_DIR`.
|
||||
* - **macOS:** Not supported.
|
||||
* - **Windows:** Not supported.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { runtimeDir } from '@tauri-apps/api/path';
|
||||
* const runtimeDirPath = await runtimeDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function runtimeDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Runtime
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's template directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Android:** Unsupported. The {@link BaseDirectory.Template} variant does not
|
||||
* exist there and the call rejects.
|
||||
*
|
||||
* - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_TEMPLATES_DIR`.
|
||||
* - **macOS:** Not supported.
|
||||
* - **Windows:** Resolves to `{FOLDERID_Templates}`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { templateDir } from '@tauri-apps/api/path';
|
||||
* const templateDirPath = await templateDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function templateDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Template
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the user's video directory.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_VIDEOS_DIR`.
|
||||
* - **macOS:** Resolves to `$HOME/Movies`.
|
||||
* - **Windows:** Resolves to `{FOLDERID_Videos}`.
|
||||
* - **Android:** Resolves to the app-specific Movies directory
|
||||
* (`getExternalFilesDir(DIRECTORY_MOVIES)`), typically `.../files/Movies`. It is
|
||||
* private to your app and removed when the app is uninstalled.
|
||||
* - **iOS:** Resolves inside the app sandbox.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { videoDir } from '@tauri-apps/api/path';
|
||||
* const videoDirPath = await videoDir();
|
||||
* ```
|
||||
*
|
||||
* @remarks **Breaking change on Android in `tauri` 2.12.0:** `videoDir()` and the
|
||||
* `$VIDEO` path variable used to resolve to external *cache* storage
|
||||
* (`.../cache`) and now resolve to the app-specific Movies directory
|
||||
* (`.../files/Movies`). Files written to the old location are not found at the new
|
||||
* one, so migrate them or update your path assumptions.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function videoDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Video
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the suggested directory for your app's log files.
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Linux:** Resolves to `${configDir}/${bundleIdentifier}/logs`.
|
||||
* - **macOS:** Resolves to `${homeDir}/Library/Logs/{bundleIdentifier}`
|
||||
* - **Windows:** Resolves to `${configDir}/${bundleIdentifier}/logs`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { appLogDir } from '@tauri-apps/api/path';
|
||||
* const appLogDirPath = await appLogDir();
|
||||
* ```
|
||||
*
|
||||
* @since 1.2.0
|
||||
*/
|
||||
async function appLogDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.AppLog
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a temporary directory.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { tempDir } from '@tauri-apps/api/path';
|
||||
* const temp = await tempDir();
|
||||
* ```
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function tempDir(): Promise<string> {
|
||||
return invoke('plugin:path|resolve_directory', {
|
||||
directory: BaseDirectory.Temp
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the platform-specific path segment separator:
|
||||
* - `\` on Windows
|
||||
* - `/` on POSIX
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
function sep(): string {
|
||||
return window.__TAURI_INTERNALS__.plugins.path.sep
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the platform-specific path segment delimiter:
|
||||
* - `;` on Windows
|
||||
* - `:` on POSIX
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
function delimiter(): string {
|
||||
return window.__TAURI_INTERNALS__.plugins.path.delimiter
|
||||
}
|
||||
/**
|
||||
* Resolves a sequence of `paths` or `path` segments into an absolute path.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { resolve, appDataDir } from '@tauri-apps/api/path';
|
||||
* const appDataDirPath = await appDataDir();
|
||||
* const path = await resolve(appDataDirPath, '..', 'users', 'tauri', 'avatar.png');
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function resolve(...paths: string[]): Promise<string> {
|
||||
return invoke('plugin:path|resolve', { paths })
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the given `path`, resolving `'..'` and `'.'` segments and resolve symbolic links.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { normalize, appDataDir } from '@tauri-apps/api/path';
|
||||
* const appDataDirPath = await appDataDir();
|
||||
* const path = await normalize(`${appDataDirPath}/../users/tauri/avatar.png`);
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function normalize(path: string): Promise<string> {
|
||||
return invoke('plugin:path|normalize', { path })
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins all given `path` segments together using the platform-specific separator as a delimiter, then normalizes the resulting path.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { join, appDataDir } from '@tauri-apps/api/path';
|
||||
* const appDataDirPath = await appDataDir();
|
||||
* const path = await join(appDataDirPath, 'users', 'tauri', 'avatar.png');
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function join(...paths: string[]): Promise<string> {
|
||||
return invoke('plugin:path|join', { paths })
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent directory of a given `path`. Trailing directory separators are ignored.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { dirname } from '@tauri-apps/api/path';
|
||||
* const dir = await dirname('/path/to/somedir/');
|
||||
* assert(dir === '/path/to');
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function dirname(path: string): Promise<string> {
|
||||
return invoke('plugin:path|dirname', { path })
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extension of the `path`.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { extname } from '@tauri-apps/api/path';
|
||||
* const ext = await extname('/path/to/file.html');
|
||||
* assert(ext === 'html');
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function extname(path: string): Promise<string> {
|
||||
return invoke('plugin:path|extname', { path })
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last portion of a `path`. Trailing directory separators are ignored.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { basename } from '@tauri-apps/api/path';
|
||||
* const base = await basename('path/to/app.conf');
|
||||
* assert(base === 'app.conf');
|
||||
* ```
|
||||
* @param ext An optional file extension to be removed from the returned path.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function basename(path: string, ext?: string): Promise<string> {
|
||||
return invoke('plugin:path|basename', { path, ext })
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the path is absolute or not.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { isAbsolute } from '@tauri-apps/api/path';
|
||||
* assert(await isAbsolute('/home/tauri'));
|
||||
* ```
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
async function isAbsolute(path: string): Promise<boolean> {
|
||||
return invoke('plugin:path|is_absolute', { path })
|
||||
}
|
||||
|
||||
export {
|
||||
BaseDirectory,
|
||||
appConfigDir,
|
||||
appDataDir,
|
||||
appLocalDataDir,
|
||||
appCacheDir,
|
||||
appLogDir,
|
||||
audioDir,
|
||||
cacheDir,
|
||||
configDir,
|
||||
dataDir,
|
||||
desktopDir,
|
||||
documentDir,
|
||||
downloadDir,
|
||||
executableDir,
|
||||
fontDir,
|
||||
homeDir,
|
||||
localDataDir,
|
||||
pictureDir,
|
||||
publicDir,
|
||||
resourceDir,
|
||||
resolveResource,
|
||||
runtimeDir,
|
||||
templateDir,
|
||||
videoDir,
|
||||
sep,
|
||||
delimiter,
|
||||
resolve,
|
||||
normalize,
|
||||
join,
|
||||
dirname,
|
||||
extname,
|
||||
basename,
|
||||
isAbsolute,
|
||||
tempDir
|
||||
}
|
||||
502
packages/api/src/tray.ts
Normal file
502
packages/api/src/tray.ts
Normal file
@@ -0,0 +1,502 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* Create and manipulate system tray (menu bar / notification area) icons.
|
||||
*
|
||||
* Tray icons are created with {@linkcode TrayIcon.new} and live on the Rust side,
|
||||
* the frontend only holds a handle to them.
|
||||
*
|
||||
* This package is also accessible with `window.__TAURI__.tray` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`.
|
||||
*
|
||||
* @remarks All commands used by this module are part of the `core:tray:default`
|
||||
* permission set, which is enabled by default, so no extra capability
|
||||
* configuration is needed. Loading an icon from a path or from bytes additionally
|
||||
* requires the `image-png` / `image-ico` Cargo features of the `tauri` crate.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { Menu, Submenu } from './menu'
|
||||
import { Channel, invoke, Resource } from './core'
|
||||
import { type JsImage, transformImage } from './image'
|
||||
import { PhysicalPosition, PhysicalSize } from './dpi'
|
||||
|
||||
/** Whether the mouse button was pressed (`Down`) or released (`Up`). */
|
||||
export type MouseButtonState = 'Up' | 'Down'
|
||||
|
||||
/** The mouse button that triggered a tray icon event. */
|
||||
export type MouseButton = 'Left' | 'Right' | 'Middle'
|
||||
|
||||
/**
|
||||
* The kind of a {@linkcode TrayIconEvent}.
|
||||
*
|
||||
* - `Click`: a mouse button was pressed or released over the icon.
|
||||
* - `DoubleClick`: the icon was double clicked.
|
||||
* - `Enter`: the cursor entered the icon area.
|
||||
* - `Move`: the cursor moved inside the icon area.
|
||||
* - `Leave`: the cursor left the icon area.
|
||||
*/
|
||||
export type TrayIconEventType =
|
||||
| 'Click'
|
||||
| 'DoubleClick'
|
||||
| 'Enter'
|
||||
| 'Move'
|
||||
| 'Leave'
|
||||
|
||||
/** The fields shared by every {@linkcode TrayIconEvent}. */
|
||||
export type TrayIconEventBase<T extends TrayIconEventType> = {
|
||||
/** The tray icon event type */
|
||||
type: T
|
||||
/** Id of the tray icon which triggered this event. */
|
||||
id: string
|
||||
/** Physical position of the click the triggered this event. */
|
||||
position: PhysicalPosition
|
||||
/** Position and size of the tray icon. */
|
||||
rect: {
|
||||
position: PhysicalPosition
|
||||
size: PhysicalSize
|
||||
}
|
||||
}
|
||||
|
||||
/** The extra fields of the `Click` and `DoubleClick` {@linkcode TrayIconEvent} variants. */
|
||||
export type TrayIconClickEvent = {
|
||||
/** Mouse button that triggered this event. */
|
||||
button: MouseButton
|
||||
/** Mouse button state when this event was triggered. */
|
||||
buttonState: MouseButtonState
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a tray icon event.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **Linux**: Unsupported. The event is not emitted even though the icon is shown,
|
||||
* the icon will still show a context menu on right click.
|
||||
*/
|
||||
export type TrayIconEvent =
|
||||
| (TrayIconEventBase<'Click'> & TrayIconClickEvent)
|
||||
| (TrayIconEventBase<'DoubleClick'> & Omit<TrayIconClickEvent, 'buttonState'>)
|
||||
| TrayIconEventBase<'Enter'>
|
||||
| TrayIconEventBase<'Move'>
|
||||
| TrayIconEventBase<'Leave'>
|
||||
|
||||
type RustTrayIconEvent = Omit<TrayIconEvent, 'rect'> & {
|
||||
rect: {
|
||||
position: {
|
||||
Physical: { x: number; y: number }
|
||||
}
|
||||
size: {
|
||||
Physical: { width: number; height: number }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** {@link TrayIcon.new|`TrayIcon`} creation options */
|
||||
export interface TrayIconOptions {
|
||||
/** The tray icon id. If undefined, a random one will be assigned */
|
||||
id?: string
|
||||
/** The tray icon menu */
|
||||
menu?: Menu | Submenu
|
||||
/**
|
||||
* The tray icon which could be icon bytes or path to the icon file.
|
||||
*
|
||||
* Note that you may need the `image-ico` or `image-png` Cargo features to use this API.
|
||||
* To enable it, change your Cargo.toml file:
|
||||
* ```toml
|
||||
* [dependencies]
|
||||
* tauri = { version = "...", features = ["...", "image-png"] }
|
||||
* ```
|
||||
*/
|
||||
icon?: JsImage
|
||||
/** The tray icon tooltip */
|
||||
tooltip?: string
|
||||
/**
|
||||
* The tray title
|
||||
*
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Linux:** The title will not be shown unless there is an icon
|
||||
* as well. The title is useful for numerical and other frequently
|
||||
* updated information. In general, it shouldn't be shown unless a
|
||||
* user requests it as it can take up a significant amount of space
|
||||
* on the user's panel. This may not be shown in all visualizations.
|
||||
* - **Windows:** Unsupported.
|
||||
*/
|
||||
title?: string
|
||||
/**
|
||||
* The tray icon temp dir path. **Linux only**.
|
||||
*
|
||||
* On Linux, we need to write the icon to the disk and usually it will
|
||||
* be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
|
||||
*/
|
||||
tempDirPath?: string
|
||||
/**
|
||||
* Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
|
||||
*/
|
||||
iconAsTemplate?: boolean
|
||||
/**
|
||||
* Whether to show the tray menu on left click or not, default is `true`.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **Linux**: Unsupported.
|
||||
*
|
||||
* @deprecated use {@linkcode TrayIconOptions.showMenuOnLeftClick} instead.
|
||||
*/
|
||||
menuOnLeftClick?: boolean
|
||||
/**
|
||||
* Whether to show the tray menu on left click or not, default is `true`.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **Linux**: Unsupported.
|
||||
*
|
||||
* @since 2.2.0
|
||||
*/
|
||||
showMenuOnLeftClick?: boolean
|
||||
/** A handler for an event on the tray icon. */
|
||||
action?: (event: TrayIconEvent) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Tray icon class and associated methods. This type constructor is private,
|
||||
* instead, you should use the static method {@linkcode TrayIcon.new}.
|
||||
*
|
||||
* #### Warning
|
||||
*
|
||||
* Unlike Rust, javascript does not have any way to run cleanup code
|
||||
* when an object is being removed by garbage collection, but this tray icon
|
||||
* will be cleaned up when the tauri app exists, however if you want to cleanup
|
||||
* this object early, you need to call {@linkcode TrayIcon.close}.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { TrayIcon } from '@tauri-apps/api/tray';
|
||||
* const tray = await TrayIcon.new({ tooltip: 'awesome tray tooltip' });
|
||||
* await tray.setTooltip('new tooltip');
|
||||
* ```
|
||||
*/
|
||||
export class TrayIcon extends Resource {
|
||||
/** The id associated with this tray icon. */
|
||||
public id: string
|
||||
|
||||
private constructor(rid: number, id: string) {
|
||||
super(rid)
|
||||
this.id = id
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a tray icon using the provided id.
|
||||
*
|
||||
* Use it to get a handle from the frontend to a tray icon that was created on
|
||||
* the Rust side with an explicit id.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { TrayIcon } from '@tauri-apps/api/tray';
|
||||
* const tray = await TrayIcon.getById('main-tray');
|
||||
* await tray?.setTooltip('still here');
|
||||
* ```
|
||||
*
|
||||
* @returns The tray icon, or `null` if no tray icon with that id exists.
|
||||
*/
|
||||
static async getById(id: string): Promise<TrayIcon | null> {
|
||||
return invoke<number>('plugin:tray|get_by_id', { id }).then((rid) =>
|
||||
rid ? new TrayIcon(rid, id) : null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tray icon using the provided id from tauri's internal state.
|
||||
*
|
||||
* Note that this may cause the tray icon to disappear
|
||||
* if it wasn't cloned somewhere else or referenced by JS.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { TrayIcon } from '@tauri-apps/api/tray';
|
||||
* await TrayIcon.removeById('main-tray');
|
||||
* ```
|
||||
*/
|
||||
static async removeById(id: string): Promise<void> {
|
||||
return invoke('plugin:tray|remove_by_id', { id })
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@linkcode TrayIcon}
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **Linux:** Sometimes the icon won't be visible unless a menu is set.
|
||||
* Setting an empty {@linkcode Menu} is enough.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { TrayIcon } from '@tauri-apps/api/tray';
|
||||
* import { Menu } from '@tauri-apps/api/menu';
|
||||
* import { defaultWindowIcon } from '@tauri-apps/api/app';
|
||||
*
|
||||
* const menu = await Menu.new({
|
||||
* items: [{ id: 'quit', text: 'Quit', action: () => console.log('quit') }]
|
||||
* });
|
||||
*
|
||||
* const tray = await TrayIcon.new({
|
||||
* id: 'main-tray',
|
||||
* icon: (await defaultWindowIcon()) ?? undefined,
|
||||
* menu,
|
||||
* tooltip: 'My app',
|
||||
* action: (event) => {
|
||||
* if (event.type === 'Click') {
|
||||
* console.log('tray clicked with', event.button);
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
static async new(options?: TrayIconOptions): Promise<TrayIcon> {
|
||||
if (options?.menu) {
|
||||
// @ts-expect-error we only need the rid and kind
|
||||
options.menu = [options.menu.rid, options.menu.kind]
|
||||
}
|
||||
if (options?.icon) {
|
||||
options.icon = transformImage(options.icon)
|
||||
}
|
||||
|
||||
const handler = new Channel<RustTrayIconEvent>()
|
||||
if (options?.action) {
|
||||
const action = options.action
|
||||
handler.onmessage = (e) => action(mapEvent(e))
|
||||
delete options.action
|
||||
}
|
||||
|
||||
return invoke<[number, string]>('plugin:tray|new', {
|
||||
options: options ?? {},
|
||||
handler
|
||||
}).then(([rid, id]) => new TrayIcon(rid, id))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new tray icon. If `null` is provided, it will remove the icon.
|
||||
*
|
||||
* Note that you may need the `image-ico` or `image-png` Cargo features to use this API.
|
||||
* To enable it, change your Cargo.toml file:
|
||||
* ```toml
|
||||
* [dependencies]
|
||||
* tauri = { version = "...", features = ["...", "image-png"] }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Image } from '@tauri-apps/api/image';
|
||||
* await tray.setIcon(await Image.fromPath('icons/active.png'));
|
||||
* // remove the icon
|
||||
* await tray.setIcon(null);
|
||||
* ```
|
||||
*/
|
||||
async setIcon(icon: JsImage | null): Promise<void> {
|
||||
let trayIcon = null
|
||||
if (icon) {
|
||||
trayIcon = transformImage(icon)
|
||||
}
|
||||
return invoke('plugin:tray|set_icon', { rid: this.rid, icon: trayIcon })
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new tray menu.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **Linux**: once a menu is set it cannot be removed so `null` has no effect
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Menu } from '@tauri-apps/api/menu';
|
||||
* const menu = await Menu.new({ items: [{ id: 'quit', text: 'Quit' }] });
|
||||
* await tray.setMenu(menu);
|
||||
* ```
|
||||
*/
|
||||
async setMenu(menu: Menu | Submenu | null): Promise<void> {
|
||||
if (menu) {
|
||||
// @ts-expect-error we only need the rid and kind
|
||||
menu = [menu.rid, menu.kind]
|
||||
}
|
||||
return invoke('plugin:tray|set_menu', { rid: this.rid, menu })
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tooltip for this tray icon, shown when the cursor hovers it.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **Linux:** Unsupported
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await tray.setTooltip('3 unread messages');
|
||||
* ```
|
||||
*/
|
||||
async setTooltip(tooltip: string | null): Promise<void> {
|
||||
return invoke('plugin:tray|set_tooltip', { rid: this.rid, tooltip })
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title shown next to this tray icon.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await tray.setTitle('42');
|
||||
* ```
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **Linux:** The title will not be shown unless there is an icon
|
||||
* as well. The title is useful for numerical and other frequently
|
||||
* updated information. In general, it shouldn't be shown unless a
|
||||
* user requests it as it can take up a significant amount of space
|
||||
* on the user's panel. This may not be shown in all visualizations.
|
||||
* - **Windows:** Unsupported
|
||||
*/
|
||||
async setTitle(title: string | null): Promise<void> {
|
||||
return invoke('plugin:tray|set_title', { rid: this.rid, title })
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide this tray icon.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await tray.setVisible(false);
|
||||
* ```
|
||||
*/
|
||||
async setVisible(visible: boolean): Promise<void> {
|
||||
return invoke('plugin:tray|set_visible', { rid: this.rid, visible })
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tray icon temp dir path. **Linux only**.
|
||||
*
|
||||
* On Linux, we need to write the icon to the disk and usually it will
|
||||
* be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { appCacheDir } from '@tauri-apps/api/path';
|
||||
* await tray.setTempDirPath(await appCacheDir());
|
||||
* ```
|
||||
*/
|
||||
async setTempDirPath(path: string | null): Promise<void> {
|
||||
return invoke('plugin:tray|set_temp_dir_path', { rid: this.rid, path })
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**
|
||||
*
|
||||
* A template image is recolored by the system so it matches the menu bar
|
||||
* appearance in light and dark mode.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await tray.setIconAsTemplate(true);
|
||||
* ```
|
||||
*/
|
||||
async setIconAsTemplate(asTemplate: boolean): Promise<void> {
|
||||
return invoke('plugin:tray|set_icon_as_template', {
|
||||
rid: this.rid,
|
||||
asTemplate
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new tray icon and template status atomically. **macOS only**.
|
||||
*
|
||||
* Note that you may need the `image-ico` or `image-png` Cargo features to use this API.
|
||||
* To enable it, change your Cargo.toml file:
|
||||
* ```toml
|
||||
* [dependencies]
|
||||
* tauri = { version = "...", features = ["...", "image-png"] }
|
||||
* ```
|
||||
*
|
||||
* Prefer this over calling {@linkcode TrayIcon.setIcon} and
|
||||
* {@linkcode TrayIcon.setIconAsTemplate} in sequence, which can briefly show the
|
||||
* new icon with the previous template setting.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Image } from '@tauri-apps/api/image';
|
||||
* await tray.setIconWithAsTemplate(await Image.fromPath('icons/active.png'), true);
|
||||
* ```
|
||||
*/
|
||||
async setIconWithAsTemplate(
|
||||
icon: JsImage | null,
|
||||
asTemplate: boolean
|
||||
): Promise<void> {
|
||||
let trayIcon = null
|
||||
if (icon) {
|
||||
trayIcon = transformImage(icon)
|
||||
}
|
||||
return invoke('plugin:tray|set_icon_with_as_template', {
|
||||
rid: this.rid,
|
||||
icon: trayIcon,
|
||||
asTemplate
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable or enable showing the tray menu on left click.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **Linux**: Unsupported.
|
||||
*
|
||||
* @deprecated use {@linkcode TrayIcon.setShowMenuOnLeftClick} instead.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await tray.setShowMenuOnLeftClick(false);
|
||||
* ```
|
||||
*/
|
||||
async setMenuOnLeftClick(onLeft: boolean): Promise<void> {
|
||||
return invoke('plugin:tray|set_show_menu_on_left_click', {
|
||||
rid: this.rid,
|
||||
onLeft
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable or enable showing the tray menu on left click.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **Linux**: Unsupported.
|
||||
*
|
||||
* Disable it to handle left clicks yourself through the
|
||||
* {@linkcode TrayIconOptions.action} handler while still showing the menu on
|
||||
* right click.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await tray.setShowMenuOnLeftClick(false);
|
||||
* ```
|
||||
*
|
||||
* @since 2.2.0
|
||||
*/
|
||||
async setShowMenuOnLeftClick(onLeft: boolean): Promise<void> {
|
||||
return invoke('plugin:tray|set_show_menu_on_left_click', {
|
||||
rid: this.rid,
|
||||
onLeft
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function mapEvent(e: RustTrayIconEvent): TrayIconEvent {
|
||||
const out = e as unknown as TrayIconEvent
|
||||
|
||||
out.position = new PhysicalPosition(e.position)
|
||||
out.rect.position = new PhysicalPosition(e.rect.position)
|
||||
out.rect.size = new PhysicalSize(e.rect.size)
|
||||
|
||||
return out
|
||||
}
|
||||
1051
packages/api/src/webview.ts
Normal file
1051
packages/api/src/webview.ts
Normal file
File diff suppressed because it is too large
Load Diff
324
packages/api/src/webviewWindow.ts
Normal file
324
packages/api/src/webviewWindow.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/**
|
||||
* Create and manipulate windows that host a single webview.
|
||||
*
|
||||
* {@linkcode WebviewWindow} is the type you want for ordinary multi-window apps: it
|
||||
* mixes together the {@link Window} and {@link Webview} APIs, so one object exposes
|
||||
* both the window methods (`setTitle`, `maximize`, `close`, ...) and the webview
|
||||
* ones (`setZoom`, `clearAllBrowsingData`, ...).
|
||||
*
|
||||
* ```typescript
|
||||
* import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
*
|
||||
* const settings = new WebviewWindow('settings', {
|
||||
* url: '/settings',
|
||||
* title: 'Settings'
|
||||
* });
|
||||
* settings.once('tauri://created', () => console.log('window created'));
|
||||
* settings.once('tauri://error', (e) => console.error(e));
|
||||
* ```
|
||||
*
|
||||
* Unlike {@link Webview | child webviews}, this does **not** require the `unstable`
|
||||
* Cargo feature.
|
||||
*
|
||||
* This package is also accessible with `window.__TAURI__.webviewWindow` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`.
|
||||
*
|
||||
* @remarks Creating a webview window requires the
|
||||
* `core:webview:allow-create-webview-window` permission, which is not included in
|
||||
* `core:webview:default`. The methods inherited from {@link Window} and
|
||||
* {@link Webview} each document their own permission.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
getCurrentWebview,
|
||||
Webview,
|
||||
WebviewLabel,
|
||||
WebviewOptions
|
||||
} from './webview'
|
||||
import type { WindowOptions } from './window'
|
||||
import { Window } from './window'
|
||||
import { listen, once } from './event'
|
||||
import type { EventName, EventCallback, UnlistenFn } from './event'
|
||||
import { invoke } from './core'
|
||||
import type { Color, DragDropEvent } from './webview'
|
||||
|
||||
/**
|
||||
* Get an instance of `Webview` for the current webview window.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
function getCurrentWebviewWindow(): WebviewWindow {
|
||||
const webview = getCurrentWebview()
|
||||
// @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor
|
||||
return new WebviewWindow(webview.label, { skip: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of instances of `Webview` for all available webview windows.
|
||||
*
|
||||
* @remarks Uses the `core:window:allow-get-all-windows` permission, which is part
|
||||
* of `core:window:default`.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function getAllWebviewWindows(): Promise<WebviewWindow[]> {
|
||||
return invoke<string[]>('plugin:window|get_all_windows').then((windows) =>
|
||||
windows.map(
|
||||
(w) =>
|
||||
new WebviewWindow(w, {
|
||||
// @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor
|
||||
skip: true
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||
interface WebviewWindow extends Webview, Window {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
||||
class WebviewWindow {
|
||||
label: string
|
||||
/** Local event listeners. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
listeners: Record<string, Array<EventCallback<any>>>
|
||||
|
||||
/**
|
||||
* Creates a new {@link Window} hosting a {@link Webview}.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
* const webview = new WebviewWindow('my-label', {
|
||||
* url: 'https://github.com/tauri-apps/tauri'
|
||||
* });
|
||||
* webview.once('tauri://created', function () {
|
||||
* // webview successfully created
|
||||
* });
|
||||
* webview.once('tauri://error', function (e) {
|
||||
* // an error happened creating the webview
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param label The unique webview label. Must be alphanumeric: `a-zA-Z-/:_`.
|
||||
* @param options The window and webview configuration, see {@link WindowOptions}
|
||||
* and {@link WebviewOptions}.
|
||||
* @returns The {@link WebviewWindow} instance to communicate with the window and webview.
|
||||
*
|
||||
* @remarks Requires the `core:webview:allow-create-webview-window` permission
|
||||
* (not included in `core:webview:default`).
|
||||
*/
|
||||
constructor(
|
||||
label: WebviewLabel,
|
||||
options: Omit<WebviewOptions, 'x' | 'y' | 'width' | 'height'>
|
||||
& WindowOptions = {}
|
||||
) {
|
||||
this.label = label
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
this.listeners = Object.create(null)
|
||||
|
||||
// @ts-expect-error `skip` is not a public API so it is not defined in WebviewOptions
|
||||
if (!options?.skip) {
|
||||
invoke('plugin:webview|create_webview_window', {
|
||||
options: {
|
||||
...options,
|
||||
parent:
|
||||
typeof options.parent === 'string'
|
||||
? options.parent
|
||||
: options.parent?.label,
|
||||
label
|
||||
}
|
||||
})
|
||||
.then(async () => this.emit('tauri://created'))
|
||||
.catch(async (e: string) => this.emit('tauri://error', e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Webview for the webview associated with the given label.
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
* const mainWebview = WebviewWindow.getByLabel('main');
|
||||
* ```
|
||||
*
|
||||
* @param label The webview label.
|
||||
* @returns The Webview instance to communicate with the webview or null if the webview doesn't exist.
|
||||
*/
|
||||
static async getByLabel(label: string): Promise<WebviewWindow | null> {
|
||||
const webview =
|
||||
(await getAllWebviewWindows()).find((w) => w.label === label) ?? null
|
||||
if (webview) {
|
||||
// @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor
|
||||
return new WebviewWindow(webview.label, { skip: true })
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of `Webview` for the current webview.
|
||||
*/
|
||||
static getCurrent(): WebviewWindow {
|
||||
return getCurrentWebviewWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of instances of `Webview` for all available webviews.
|
||||
*/
|
||||
static async getAll(): Promise<WebviewWindow[]> {
|
||||
return getAllWebviewWindows()
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to an emitted event on this webview window.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
* const unlisten = await WebviewWindow.getCurrent().listen<string>('state-changed', (event) => {
|
||||
* console.log(`Got error: ${payload}`);
|
||||
* });
|
||||
*
|
||||
* // call unlisten when your handler goes out of scope e.g. the component is unmounted
|
||||
* unlisten();
|
||||
* ```
|
||||
*
|
||||
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
|
||||
* @param handler Event handler.
|
||||
* @returns A promise resolving to a function to unlisten to the event.
|
||||
*
|
||||
* @remarks The listener is removed automatically when this webview window is
|
||||
* destroyed, so you do not need to unlisten just because the window is closing.
|
||||
* Do call the returned function when the listener's own scope ends, e.g. on page
|
||||
* navigation or when a component unmounts.
|
||||
*/
|
||||
async listen<T>(
|
||||
event: EventName,
|
||||
handler: EventCallback<T>
|
||||
): Promise<UnlistenFn> {
|
||||
if (this._handleTauriEvent(event, handler)) {
|
||||
return () => {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
const listeners = this.listeners[event]
|
||||
listeners.splice(listeners.indexOf(handler), 1)
|
||||
}
|
||||
}
|
||||
return listen(event, handler, {
|
||||
target: { kind: 'WebviewWindow', label: this.label }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to an emitted event on this webview window only once.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
* const unlisten = await WebviewWindow.getCurrent().once<null>('initialized', (event) => {
|
||||
* console.log(`Webview initialized!`);
|
||||
* });
|
||||
*
|
||||
* // call unlisten when your handler goes out of scope e.g. the component is unmounted
|
||||
* unlisten();
|
||||
* ```
|
||||
*
|
||||
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
|
||||
* @param handler Event handler.
|
||||
* @returns A promise resolving to a function to unlisten to the event.
|
||||
*
|
||||
* @remarks The listener removes itself after the first event and is also removed
|
||||
* automatically when this webview window is destroyed. Do call the returned
|
||||
* function if the listener's own scope ends before the event arrives.
|
||||
*/
|
||||
async once<T>(
|
||||
event: EventName,
|
||||
handler: EventCallback<T>
|
||||
): Promise<UnlistenFn> {
|
||||
if (this._handleTauriEvent(event, handler)) {
|
||||
return () => {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
const listeners = this.listeners[event]
|
||||
listeners.splice(listeners.indexOf(handler), 1)
|
||||
}
|
||||
}
|
||||
return once(event, handler, {
|
||||
target: { kind: 'WebviewWindow', label: this.label }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the window and webview background color.
|
||||
*
|
||||
* #### Platform-specific:
|
||||
*
|
||||
* - **Android / iOS:** Unsupported for the window layer.
|
||||
* - **macOS / iOS**: Not implemented for the webview layer.
|
||||
* - **Windows**:
|
||||
* - alpha channel is ignored for the window layer.
|
||||
* - On Windows 7, alpha channel is ignored for the webview layer.
|
||||
* - On Windows 8 and newer, if alpha channel is not `0`, it will be ignored.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
* await getCurrentWebviewWindow().setBackgroundColor('#2f2f2f');
|
||||
* ```
|
||||
*
|
||||
* @param color The new background color.
|
||||
* @returns A promise indicating the success or failure of the operation.
|
||||
*
|
||||
* @remarks Requires both the `core:window:allow-set-background-color` and
|
||||
* `core:webview:allow-set-webview-background-color` permissions, neither of which
|
||||
* is included in the respective default permission set.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
async setBackgroundColor(color: Color): Promise<void> {
|
||||
return invoke('plugin:window|set_background_color', {
|
||||
label: this.label,
|
||||
value: color
|
||||
}).then(() => {
|
||||
return invoke('plugin:webview|set_webview_background_color', {
|
||||
label: this.label,
|
||||
value: color
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Order matters, we use window APIs by default
|
||||
applyMixins(WebviewWindow, [Window, Webview])
|
||||
|
||||
/** Extends a base class by other specified classes, without overriding existing properties */
|
||||
function applyMixins(
|
||||
baseClass: { prototype: unknown },
|
||||
extendedClasses: unknown
|
||||
): void {
|
||||
;(Array.isArray(extendedClasses)
|
||||
? extendedClasses
|
||||
: [extendedClasses]
|
||||
).forEach((extendedClass: { prototype: unknown }) => {
|
||||
Object.getOwnPropertyNames(extendedClass.prototype).forEach((name) => {
|
||||
if (
|
||||
typeof baseClass.prototype === 'object'
|
||||
&& baseClass.prototype
|
||||
&& name in baseClass.prototype
|
||||
)
|
||||
return
|
||||
Object.defineProperty(
|
||||
baseClass.prototype,
|
||||
name,
|
||||
// eslint-disable-next-line
|
||||
Object.getOwnPropertyDescriptor(extendedClass.prototype, name)
|
||||
?? Object.create(null)
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export { WebviewWindow, getCurrentWebviewWindow, getAllWebviewWindows }
|
||||
export type { DragDropEvent, Color }
|
||||
3215
packages/api/src/window.ts
Normal file
3215
packages/api/src/window.ts
Normal file
File diff suppressed because it is too large
Load Diff
13
packages/api/tsconfig.json
Normal file
13
packages/api/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2019",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitAny": true,
|
||||
"noEmit": true,
|
||||
"lib": ["es2022", "esnext.disposable", "dom"]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user