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:
196
packages/api-e2e/README.md
Normal file
196
packages/api-e2e/README.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# `@tauri-apps/api` end-to-end tests
|
||||
|
||||
WebdriverIO suite that exercises every [`@tauri-apps/api`](../api) module against a
|
||||
**real** Tauri app — the [`examples/api`](../../examples/api) validation app — rather than
|
||||
a mocked backend, on desktop (Linux, macOS, Windows) and mobile (Android, iOS). Each module
|
||||
has its own spec file, shared by every platform, and adding coverage for a new API is
|
||||
normally just dropping in one more spec.
|
||||
|
||||
## How it works
|
||||
|
||||
- The example app is built with `withGlobalTauri: true`, so the entire API surface is
|
||||
reachable on `window.__TAURI__` inside the webview.
|
||||
- On desktop, WebdriverIO drives the app through [`@crabnebula/tauri-driver`](https://www.npmjs.com/package/@crabnebula/tauri-driver),
|
||||
which bridges the WebDriver protocol to each platform's webview:
|
||||
- **macOS** — the CrabNebula Webdriver, which needs [`tauri-plugin-automation`](https://crates.io/crates/tauri-plugin-automation)
|
||||
(registered in `examples/api` behind its off-by-default `automation` Cargo feature, which
|
||||
the suite's build enables) and a locally-running `@crabnebula/test-runner-backend`,
|
||||
authenticated with `CN_API_KEY`.
|
||||
- **Linux** — `webkit2gtk-driver` (`WebKitWebDriver` on `PATH`).
|
||||
- **Windows** — `msedgedriver.exe` on `PATH`. It hands the app the `--remote-debugging-port`
|
||||
it attaches to through `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS`, which WebView2 ignores in an
|
||||
elevated process ([wry#1782](https://github.com/tauri-apps/wry/issues/1782)), so the suite has
|
||||
to run unelevated.
|
||||
- On mobile, WebdriverIO drives the app through [Appium](https://appium.io) (started by
|
||||
`@wdio/appium-service`; the drivers are plain devDependencies of this package, which Appium
|
||||
picks up on its own):
|
||||
- **Android** — the UiAutomator2 driver. The suite switches to the app's `WEBVIEW_*`
|
||||
context, which chromedriver reaches through the WebView's debugging socket. Debug builds
|
||||
turn that on (`setWebContentsDebuggingEnabled`), so the suite builds a debug APK. A
|
||||
chromedriver matching the device's WebView is downloaded on demand (see `E2E_CHROMEDRIVER`).
|
||||
- **iOS** — the XCUITest driver on a simulator, attaching to the WKWebView through the
|
||||
WebKit remote inspector. Debug builds mark the webview `isInspectable`, so the suite
|
||||
builds an unsigned debug simulator app. The inspector identifies an app by the
|
||||
`application-identifier` entitlement that Xcode embeds when it code signs a simulator
|
||||
build; an unsigned one has none and is listed as `process-<executable name>` instead of
|
||||
its bundle identifier, so the config has the driver match that name too
|
||||
(`appium:additionalWebviewBundleIds`). The driver also starts with a script timeout of
|
||||
0, which the config raises to the 30s the other drivers default to, or every
|
||||
`executeAsync` would time out at once.
|
||||
- Specs never `eval` in the page. They pass a function to the [`tauri()`](test/helpers/index.ts)
|
||||
helper, which serializes it and runs it via the driver's own (CSP-exempt) script injection,
|
||||
handing it `window.__TAURI__` as the first argument and returning its JSON result.
|
||||
- Each spec file gets its own session — a fresh `tauri-driver` (and therefore a fresh app
|
||||
instance) on desktop, a fresh Appium session (which relaunches the app) on mobile — so
|
||||
each module's suite runs in isolation.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```sh
|
||||
# from the repo root
|
||||
pnpm install
|
||||
pnpm build:api # examples/api resolves @tauri-apps/api from packages/api/dist
|
||||
pnpm build:cli # examples/api's `tauri` script uses the local native CLI
|
||||
```
|
||||
|
||||
Platform driver dependencies:
|
||||
|
||||
| Platform | Requirement |
|
||||
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| macOS | `CN_API_KEY` env var (CrabNebula Cloud). The automation plugin and test-runner-backend are wired up already. |
|
||||
| Linux | `webkit2gtk-driver` package (provides `WebKitWebDriver`). |
|
||||
| Windows | `msedgedriver.exe` matching your Edge version, on `PATH`. Run the suite unelevated. |
|
||||
| Android | The usual Tauri Android setup (`ANDROID_HOME`, `NDK_HOME`, a JDK), plus a running emulator or a connected device with USB debugging. Network access the first time, for the chromedriver download. |
|
||||
| iOS | macOS with Xcode and an iOS simulator runtime. `tauri ios init` installs [xcodegen](https://github.com/yonaskolb/XcodeGen) through Homebrew if missing. The first session compiles WebDriverAgent (a few minutes). |
|
||||
|
||||
## Running
|
||||
|
||||
```sh
|
||||
# from the repo root
|
||||
pnpm test:api-e2e
|
||||
|
||||
# or from this package
|
||||
pnpm e2e
|
||||
|
||||
# iterate without rebuilding the app every run
|
||||
E2E_SKIP_BUILD=1 pnpm e2e
|
||||
|
||||
# run a single module's spec
|
||||
pnpm exec wdio run ./wdio.conf.ts --spec test/specs/window.spec.ts
|
||||
|
||||
# mobile (from the repo root; or `pnpm e2e:android` / `pnpm e2e:ios` from this package)
|
||||
pnpm test:api-e2e:android
|
||||
pnpm test:api-e2e:ios
|
||||
```
|
||||
|
||||
The first run builds the app with [`tauri.e2e.conf.json`](tauri.e2e.conf.json) as a config
|
||||
override, which enables the example's `automation` feature; afterwards use `E2E_SKIP_BUILD=1`
|
||||
to reuse the existing binary. A binary supplied through `E2E_SKIP_BUILD` or `E2E_APP_PATH`
|
||||
must have been built with that feature whenever the CrabNebula Webdriver is in use (always
|
||||
on macOS).
|
||||
|
||||
The mobile configs ([`wdio.android.conf.ts`](wdio.android.conf.ts), [`wdio.ios.conf.ts`](wdio.ios.conf.ts),
|
||||
sharing [`wdio.mobile.ts`](wdio.mobile.ts)) initialize the example's Android/Xcode project if
|
||||
`src-tauri/gen` is missing, then run `tauri android build --debug --apk` /
|
||||
`tauri ios build --debug --target aarch64-sim --no-sign`, compiling only the Rust target the
|
||||
device runs (the Android one is read from the connected device through `adb`). The app must be
|
||||
a **debug** build — release builds have webview debugging off, and Appium cannot see the page.
|
||||
`E2E_SKIP_BUILD` and `E2E_APP_PATH` (an `.apk` / simulator `.app`) work as on desktop.
|
||||
|
||||
On iOS, the XCUITest driver compiles WebDriverAgent with xcodebuild in the first session, which
|
||||
takes minutes on a CI runner. Appium's own release build of it for the simulator skips that:
|
||||
`APPIUM_HOME=$PWD pnpm exec appium driver run xcuitest download-wda -- --outdir <dir> --platform iOS --kind sim`
|
||||
(from this package), then `E2E_IOS_WDA=<dir>/WebDriverAgentRunner-Runner.app`, and the driver
|
||||
installs and launches it as is (`appium:usePreinstalledWDA`).
|
||||
|
||||
The generated Gradle and Xcode projects call back into the CLI with `pnpm tauri …` from
|
||||
`src-tauri` / `gen/apple`, which pnpm 12.0–12.3 could not resolve to the package's scripts
|
||||
([pnpm/pnpm#14645](https://github.com/pnpm/pnpm/pull/14645)); the repo's `packageManager` pins a
|
||||
fixed version, so run the suite through that pnpm (corepack) rather than an older global one.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Purpose |
|
||||
| ------------------- | --------------------------------------------------------------------------------- |
|
||||
| `CN_API_KEY` | CrabNebula Cloud key. Required on macOS (and whenever `E2E_CN_WEBDRIVER=1`). |
|
||||
| `E2E_SKIP_BUILD` | Skip the `tauri build` step and reuse the existing binary. |
|
||||
| `E2E_APP_PATH` | Absolute path to a prebuilt app/binary to test (also implies skip-build). |
|
||||
| `E2E_SKIP` | Comma-separated module names to skip, e.g. `E2E_SKIP=tray,menu`. |
|
||||
| `E2E_SKIP_WM` | Skip window-manager-dependent tests (minimize/maximize/fullscreen/position/hide). |
|
||||
| `E2E_SPEC_RETRIES` | Retry count for flaky spec files (default `0`). |
|
||||
| `E2E_CN_WEBDRIVER` | Use the CrabNebula Webdriver on Linux/Windows too (instead of the native driver). |
|
||||
| `E2E_NATIVE_DRIVER` | Path passed to `tauri-driver --native-driver` (e.g. a specific chromedriver). |
|
||||
| `CARGO_TARGET_DIR` | Override the target dir the app binary is looked up in. |
|
||||
|
||||
Mobile only:
|
||||
|
||||
| Variable | Purpose |
|
||||
| -------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `E2E_ANDROID_TARGET` | Rust target for the APK (`aarch64`, `armv7`, `i686`, `x86_64`); default: the connected device's ABI. |
|
||||
| `E2E_ANDROID_DEVICE` | `adb` serial of the device/emulator to use (`appium:udid`); default: the first connected one. |
|
||||
| `E2E_ANDROID_AVD` | Name of an AVD for Appium to boot (`appium:avd`) instead of using an already-running emulator. |
|
||||
| `E2E_CHROMEDRIVER` | chromedriver binary matching the device's WebView, instead of letting Appium download one. |
|
||||
| `E2E_IOS_TARGET` | Rust target for the simulator app (`aarch64-sim` or `x86_64`); default: the host architecture. |
|
||||
| `E2E_IOS_DEVICE` | Simulator UDID or name (as in `xcrun simctl list`); default: a booted iPhone, else the newest one. |
|
||||
| `E2E_IOS_WDA` | Prebuilt `WebDriverAgentRunner-Runner.app` for the simulator, used instead of compiling WDA. |
|
||||
| `E2E_PLATFORM` | Set by the mobile configs for the spec workers (`android`/`ios`) — see `platform` in the helpers. |
|
||||
|
||||
Appium's own log is written to `logs/wdio-appium.log` in this package.
|
||||
|
||||
## Adding tests for a new API
|
||||
|
||||
1. **Add a spec.** Create `test/specs/<module>.spec.ts` and use `describeApi('<module>', …)`
|
||||
with the `tauri()` helper. It is picked up automatically by the `test/specs/**/*.spec.ts`
|
||||
glob. Minimal example:
|
||||
|
||||
```ts
|
||||
import { expect } from '@wdio/globals'
|
||||
import { tauri, describeApi } from '../helpers/index.js'
|
||||
|
||||
describeApi('app', () => {
|
||||
it('reports the product name', async () => {
|
||||
expect(await tauri((api) => api.app.getName())).toBe('Tauri API')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
2. **Grant permissions if needed.** If the API calls a command that is not allowed by
|
||||
default, add the permission to
|
||||
[`examples/api/src-tauri/capabilities/run-app.json`](../../examples/api/src-tauri/capabilities/run-app.json).
|
||||
The default permission sets are documented under
|
||||
`crates/tauri/permissions/*/autogenerated/reference.md`.
|
||||
|
||||
3. **Add a Rust command if needed.** If the API needs a custom backend command, add it to
|
||||
[`examples/api/src-tauri/src/cmd.rs`](../../examples/api/src-tauri/src/cmd.rs), register it
|
||||
in the `generate_handler!` list in
|
||||
[`src/lib.rs`](../../examples/api/src-tauri/src/lib.rs), add its permission, and grant it
|
||||
in `run-app.json`.
|
||||
|
||||
4. **Handle environment-sensitive cases.** Use `itWm` (instead of `it`) for assertions that
|
||||
depend on a real window manager, and `eventually()` to poll for state a WM applies
|
||||
asynchronously. Branch on `platform` from the helpers (never `process.platform`, which is
|
||||
the host running the emulator/simulator on mobile) for platform-specific behavior.
|
||||
|
||||
5. **Gate what mobile does not have.** The same specs run on Android and iOS. Wrap tests of
|
||||
desktop-only commands (`#[cfg(desktop)]` in the core plugins, or no-ops on mobile such as
|
||||
the window title and size) in `itDesktop`, use `itOn('android', …)` / `itOn('ios', …)` for
|
||||
platform-specific APIs, and pass `{ desktopOnly: true }` to `describeApi` for modules whose
|
||||
plugin is not registered on mobile at all (`menu`, `tray`). Skipped tests show up as pending
|
||||
rather than silently disappearing.
|
||||
|
||||
### Rules for `tauri()` page functions
|
||||
|
||||
The function you pass to `tauri()` runs **inside the webview**, serialized as a string:
|
||||
|
||||
- It **cannot** close over anything from the spec module — pass every value it needs through
|
||||
the trailing `tauri(fn, ...args)` arguments.
|
||||
- It may only reference `api` (the `window.__TAURI__` object), those args, and browser
|
||||
globals (`window`, `document`, `setTimeout`, `Promise`, …).
|
||||
- Its return value must be JSON-serializable — return plain objects/primitives, not class
|
||||
instances (call methods and return their results instead).
|
||||
- Restore any app state you mutate (title, size, theme, …); tests within a spec file share
|
||||
the same app instance.
|
||||
- For in-page waiting, wrap logic in a `Promise` with an explicit `setTimeout` rejection so a
|
||||
failure surfaces as a message rather than an opaque driver timeout.
|
||||
|
||||
Use `tauriError(fn, ...args)` to assert that a call rejects; it returns the rejection message.
|
||||
33
packages/api-e2e/package.json
Normal file
33
packages/api-e2e/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@tauri-apps/api-e2e",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"description": "End-to-end test suite for @tauri-apps/api, driven against the examples/api app via WebdriverIO.",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"e2e": "wdio run ./wdio.conf.ts",
|
||||
"e2e:skip-build": "cross-env E2E_SKIP_BUILD=true wdio run ./wdio.conf.ts",
|
||||
"e2e:android": "wdio run ./wdio.android.conf.ts",
|
||||
"e2e:ios": "wdio run ./wdio.ios.conf.ts",
|
||||
"ts:check": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@crabnebula/tauri-driver": "^2.0.9",
|
||||
"@crabnebula/test-runner-backend": "^0.3.0",
|
||||
"@tauri-apps/api": "../api/dist",
|
||||
"@types/node": "^22.10.0",
|
||||
"@wdio/appium-service": "^9.31.9",
|
||||
"@wdio/cli": "^9.31.9",
|
||||
"@wdio/globals": "^9.31.3",
|
||||
"@wdio/local-runner": "^9.31.9",
|
||||
"@wdio/mocha-framework": "^9.31.9",
|
||||
"@wdio/spec-reporter": "^9.31.2",
|
||||
"appium": "^3.7.0",
|
||||
"appium-uiautomator2-driver": "^8.7.0",
|
||||
"appium-xcuitest-driver": "^12.12.4",
|
||||
"cross-env": "^10.1.0",
|
||||
"typescript": "^6.0.3",
|
||||
"webdriverio": "^9.31.9"
|
||||
}
|
||||
}
|
||||
16
packages/api-e2e/tauri.e2e.conf.json
Normal file
16
packages/api-e2e/tauri.e2e.conf.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "../../crates/tauri-schema-generator/schemas/config.schema.json",
|
||||
"build": {
|
||||
"removeUnusedCommands": false,
|
||||
"features": ["automation"]
|
||||
},
|
||||
"bundle": {
|
||||
"icon": [
|
||||
"../../.icons/32x32.png",
|
||||
"../../.icons/128x128.png",
|
||||
"../../.icons/128x128@2x.png",
|
||||
"../../.icons/icon.icns",
|
||||
"../../.icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
243
packages/api-e2e/test/helpers/index.ts
Normal file
243
packages/api-e2e/test/helpers/index.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
|
||||
|
||||
import { browser } from '@wdio/globals'
|
||||
import type * as TauriApi from '@tauri-apps/api'
|
||||
|
||||
/** The full `@tauri-apps/api` surface, as exposed on `window.__TAURI__`. */
|
||||
export type Api = typeof TauriApi
|
||||
|
||||
/** OS the app under test runs on. */
|
||||
export type Platform = NodeJS.Platform | 'android' | 'ios'
|
||||
|
||||
/**
|
||||
* The platform of the app under test. The desktop suite drives an app on the
|
||||
* host, so it is `process.platform`; the mobile configs (`wdio.android.conf.ts`,
|
||||
* `wdio.ios.conf.ts`) drive an emulator/simulator and set `E2E_PLATFORM` for
|
||||
* the spec workers instead.
|
||||
*/
|
||||
export const platform: Platform =
|
||||
(process.env.E2E_PLATFORM as Platform | undefined) ?? process.platform
|
||||
|
||||
export const isMobile = platform === 'android' || platform === 'ios'
|
||||
|
||||
type PageOutcome<T> =
|
||||
| { ok: true; value: T }
|
||||
| { ok: false; error: string; stack?: string }
|
||||
|
||||
/** Thrown when the function passed to {@link tauri} rejects inside the webview. */
|
||||
export class TauriPageError extends Error {
|
||||
pageStack?: string
|
||||
constructor(message: string, pageStack?: string) {
|
||||
super(message)
|
||||
this.name = 'TauriPageError'
|
||||
this.pageStack = pageStack
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `fn` inside the app's webview with `window.__TAURI__` as its first argument
|
||||
* and resolves with its (JSON-serializable) return value.
|
||||
*
|
||||
* `fn` is serialized with `Function.prototype.toString`, so it **cannot close over
|
||||
* anything in the spec module** — every value it needs must be passed through `args`,
|
||||
* and it may only reference `api`, those args, and browser globals (`window`,
|
||||
* `document`, `setTimeout`, `Promise`, ...).
|
||||
*
|
||||
* @example
|
||||
* const name = await tauri((api) => api.app.getName())
|
||||
* const sum = await tauri((api, a, b) => a + b, 2, 3)
|
||||
*/
|
||||
export async function tauri<R, A extends unknown[]>(
|
||||
fn: (api: Api, ...args: A) => R,
|
||||
...args: A
|
||||
): Promise<Awaited<R>> {
|
||||
// A string body (rather than passing `fn` directly) keeps this working across
|
||||
// both the classic and bidi WebDriver protocols and avoids any in-page eval of
|
||||
// our own — the driver injects this script itself, which is exempt from the
|
||||
// app's CSP. `executeAsync` is used because promise support in `execute` is not
|
||||
// uniform across the platform drivers tauri-driver proxies to.
|
||||
//
|
||||
// The outcome crosses the driver as a JSON string rather than an object so no
|
||||
// driver gets to interpret its shape: the Selenium atoms that Appium runs
|
||||
// scripts through on iOS turn any object with a numeric `length` property
|
||||
// into an array.
|
||||
const script = `
|
||||
var done = arguments[arguments.length - 1];
|
||||
var args = Array.prototype.slice.call(arguments, 0, arguments.length - 1);
|
||||
var fn = (${fn.toString()});
|
||||
Promise.resolve()
|
||||
.then(function () { return fn.apply(null, [window.__TAURI__].concat(args)); })
|
||||
.then(
|
||||
function (value) { return { ok: true, value: value === undefined ? null : value }; },
|
||||
function (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined
|
||||
};
|
||||
}
|
||||
)
|
||||
.then(function (outcome) {
|
||||
try {
|
||||
done(JSON.stringify(outcome));
|
||||
} catch (error) {
|
||||
done(JSON.stringify({ ok: false, error: 'result is not JSON-serializable: ' + error }));
|
||||
}
|
||||
});
|
||||
`
|
||||
const raw = await browser.executeAsync(script, ...args)
|
||||
const outcome = (
|
||||
typeof raw === 'string' ? JSON.parse(raw) : raw
|
||||
) as PageOutcome<Awaited<R>> | null
|
||||
if (!outcome || typeof outcome !== 'object' || !('ok' in outcome)) {
|
||||
throw new Error(
|
||||
`tauri() bridge returned an unexpected value: ${JSON.stringify(outcome)}`
|
||||
)
|
||||
}
|
||||
if (!outcome.ok) {
|
||||
throw new TauriPageError(outcome.error, outcome.stack)
|
||||
}
|
||||
return outcome.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the page-side call rejects and returns the rejection message,
|
||||
* so specs can assert on it. Throws if the call unexpectedly resolves.
|
||||
*/
|
||||
export async function tauriError<A extends unknown[]>(
|
||||
fn: (api: Api, ...args: A) => unknown,
|
||||
...args: A
|
||||
): Promise<string> {
|
||||
try {
|
||||
await tauri(fn, ...args)
|
||||
} catch (error) {
|
||||
if (error instanceof TauriPageError) {
|
||||
return error.message
|
||||
}
|
||||
// Some platform drivers (notably the Linux WebKitWebDriver) surface a
|
||||
// page-side `invoke` rejection as a WebDriver-level error on the
|
||||
// `execute/async` command instead of letting the in-page bridge report it
|
||||
// as an `{ ok: false }` outcome. Fall back to that error's message so the
|
||||
// backend rejection is still assertable. This is safe for error-path specs:
|
||||
// they match the message against an expected pattern, so a genuine driver
|
||||
// failure (whose message won't match) still fails the test.
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
throw error
|
||||
}
|
||||
throw new Error('expected the API call to reject, but it resolved')
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls `check` until it returns without throwing or `timeout` elapses.
|
||||
* Use for state that a window manager applies asynchronously (focus, minimize, ...).
|
||||
*/
|
||||
export async function eventually<T>(
|
||||
check: () => T | Promise<T>,
|
||||
{
|
||||
timeout = 10_000,
|
||||
interval = 250
|
||||
}: { timeout?: number; interval?: number } = {}
|
||||
): Promise<T> {
|
||||
const deadline = Date.now() + timeout
|
||||
let lastError: unknown
|
||||
for (;;) {
|
||||
try {
|
||||
return await check()
|
||||
} catch (error) {
|
||||
lastError = error
|
||||
}
|
||||
if (Date.now() > deadline) {
|
||||
throw lastError instanceof Error
|
||||
? lastError
|
||||
: new Error(String(lastError))
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, interval))
|
||||
}
|
||||
}
|
||||
|
||||
const skippedModules = (process.env.E2E_SKIP ?? '')
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
export interface DescribeApiOptions {
|
||||
/**
|
||||
* The module's plugin is not registered at all on mobile (`menu`, `tray`),
|
||||
* so the whole suite is skipped there.
|
||||
*/
|
||||
desktopOnly?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* `describe` wrapper keyed by API module name. Any module listed in the
|
||||
* comma-separated `E2E_SKIP` env var (e.g. `E2E_SKIP=tray,menu`) is skipped.
|
||||
*/
|
||||
export function describeApi(module: string, fn: () => void): void
|
||||
export function describeApi(
|
||||
module: string,
|
||||
options: DescribeApiOptions,
|
||||
fn: () => void
|
||||
): void
|
||||
export function describeApi(
|
||||
module: string,
|
||||
optionsOrFn: DescribeApiOptions | (() => void),
|
||||
maybeFn?: () => void
|
||||
): void {
|
||||
const [options, fn] =
|
||||
typeof optionsOrFn === 'function'
|
||||
? [{} as DescribeApiOptions, optionsOrFn]
|
||||
: [optionsOrFn, maybeFn!]
|
||||
const title = `@tauri-apps/api/${module}`
|
||||
if (skippedModules.includes(module) || (options.desktopOnly && isMobile)) {
|
||||
describe.skip(title, fn)
|
||||
} else {
|
||||
describe(title, fn)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `it` restricted to the given platform(s); skipped (as pending) elsewhere.
|
||||
* Use for behavior that only exists on one OS, e.g. `activityName()` on Android.
|
||||
*/
|
||||
export function itOn(
|
||||
platforms: Platform | Platform[],
|
||||
title: string,
|
||||
fn: () => void | Promise<void>
|
||||
): void {
|
||||
const list = Array.isArray(platforms) ? platforms : [platforms]
|
||||
if (list.includes(platform)) {
|
||||
it(title, fn)
|
||||
} else {
|
||||
it.skip(title, fn)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `it` for desktop-only APIs. The mobile builds do not register the commands
|
||||
* behind them (`#[cfg(desktop)]` in the core plugins) or implement them as
|
||||
* no-ops (window title, size, resizability), so they are skipped on mobile.
|
||||
*/
|
||||
export function itDesktop(title: string, fn: () => void | Promise<void>): void {
|
||||
if (isMobile) {
|
||||
it.skip(title, fn)
|
||||
} else {
|
||||
it(title, fn)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `it` for assertions that depend on a real window manager (minimize, maximize,
|
||||
* focus, ...). Skipped entirely when `E2E_SKIP_WM` is set (e.g. bare headless CI),
|
||||
* and on mobile, which has no window manager (nor those commands).
|
||||
*/
|
||||
export function itWm(title: string, fn: () => void | Promise<void>): void {
|
||||
if (process.env.E2E_SKIP_WM || isMobile) {
|
||||
it.skip(title, fn)
|
||||
} else {
|
||||
it(title, fn)
|
||||
}
|
||||
}
|
||||
64
packages/api-e2e/test/specs/app.spec.ts
Normal file
64
packages/api-e2e/test/specs/app.spec.ts
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
|
||||
|
||||
import { expect } from '@wdio/globals'
|
||||
import {
|
||||
tauri,
|
||||
eventually,
|
||||
describeApi,
|
||||
platform,
|
||||
isMobile
|
||||
} from '../helpers/index.js'
|
||||
|
||||
describeApi('app', () => {
|
||||
it('getName returns the configured product name', async () => {
|
||||
expect(await tauri((api) => api.app.getName())).toBe('Tauri API')
|
||||
})
|
||||
|
||||
it('getVersion returns the configured version', async () => {
|
||||
expect(await tauri((api) => api.app.getVersion())).toBe('1.0.0')
|
||||
})
|
||||
|
||||
it('getTauriVersion returns a semver string', async () => {
|
||||
expect(await tauri((api) => api.app.getTauriVersion())).toMatch(
|
||||
/^\d+\.\d+\.\d+/
|
||||
)
|
||||
})
|
||||
|
||||
it('getIdentifier returns the bundle identifier', async () => {
|
||||
expect(await tauri((api) => api.app.getIdentifier())).toBe('com.tauri.api')
|
||||
})
|
||||
|
||||
it('defaultWindowIcon returns an image with a positive size', async () => {
|
||||
const size = await tauri(async (api) => {
|
||||
const icon = await api.app.defaultWindowIcon()
|
||||
return icon ? await icon.size() : null
|
||||
})
|
||||
expect(size).not.toBeNull()
|
||||
expect(size!.width).toBeGreaterThan(0)
|
||||
expect(size!.height).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('setTheme applies a theme and can be reset to the system default', async () => {
|
||||
await tauri((api) => api.app.setTheme('dark'))
|
||||
// On Linux the app-level theme is not observable through `window.theme()`:
|
||||
// tao's event-loop `set_theme` only flips the GTK `prefer-dark` setting and
|
||||
// never updates the window's stored theme, so `theme()` keeps reporting the
|
||||
// portal/system value (and blocks on a 5s dbus timeout when no portal runs,
|
||||
// as in headless CI). On mobile `set_theme` is a no-op and `theme()` always
|
||||
// reports `light`. Elsewhere the change is reflected on the window.
|
||||
if (platform !== 'linux' && !isMobile) {
|
||||
await eventually(async () => {
|
||||
const theme = await tauri((api) =>
|
||||
api.window.getCurrentWindow().theme()
|
||||
)
|
||||
if (theme !== 'dark') {
|
||||
throw new Error(`expected theme "dark", got "${theme}"`)
|
||||
}
|
||||
})
|
||||
}
|
||||
// restore
|
||||
await tauri((api) => api.app.setTheme(null))
|
||||
})
|
||||
})
|
||||
109
packages/api-e2e/test/specs/core.spec.ts
Normal file
109
packages/api-e2e/test/specs/core.spec.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { expect } from '@wdio/globals'
|
||||
import { tauri, tauriError, describeApi, itOn } from '../helpers/index.js'
|
||||
|
||||
describeApi('core', () => {
|
||||
it('isTauri reports true inside the app', async () => {
|
||||
expect(await tauri((api) => api.core.isTauri())).toBe(true)
|
||||
})
|
||||
|
||||
it('invoke round-trips a raw request through the echo command', async () => {
|
||||
const echoed = await tauri((api) =>
|
||||
api.core.invoke('echo', { some: 'payload', n: 42 })
|
||||
)
|
||||
expect(echoed).toEqual({ some: 'payload', n: 42 })
|
||||
})
|
||||
|
||||
it('invoke rejects for a command that is not registered', async () => {
|
||||
// `removeUnusedCommands` strips ungranted commands from the app entirely.
|
||||
const message = await tauriError((api) =>
|
||||
api.core.invoke('this_command_does_not_exist')
|
||||
)
|
||||
expect(message).toMatch(/not found|not allowed|denied/i)
|
||||
})
|
||||
|
||||
it('command scopes gate log_operation by event', async () => {
|
||||
// The run-app capability scopes `log_operation` to the `tauri-click` event.
|
||||
await tauri((api) =>
|
||||
api.core.invoke('log_operation', {
|
||||
event: 'tauri-click',
|
||||
payload: 'from e2e'
|
||||
})
|
||||
)
|
||||
const message = await tauriError((api) =>
|
||||
api.core.invoke('log_operation', { event: 'out-of-scope-event' })
|
||||
)
|
||||
expect(message).toMatch(/not allowed|denied/i)
|
||||
})
|
||||
|
||||
it('Channel receives every message streamed from the backend in order', async () => {
|
||||
// `spam` sends 1..=1000 over the channel.
|
||||
const result = await tauri(
|
||||
(api, count) =>
|
||||
new Promise<{ length: number; ordered: boolean; last: number }>(
|
||||
(resolve, reject) => {
|
||||
const received: number[] = []
|
||||
const channel = new api.core.Channel<number>()
|
||||
channel.onmessage = (value: number) => {
|
||||
received.push(value)
|
||||
if (received.length === count) {
|
||||
let ordered = true
|
||||
for (let i = 0; i < received.length; i++) {
|
||||
if (received[i] !== i + 1) {
|
||||
ordered = false
|
||||
break
|
||||
}
|
||||
}
|
||||
resolve({
|
||||
length: received.length,
|
||||
ordered,
|
||||
last: received[received.length - 1]
|
||||
})
|
||||
}
|
||||
}
|
||||
api.core.invoke('spam', { channel }).catch(reject)
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error(
|
||||
`only received ${received.length} of ${count} messages`
|
||||
)
|
||||
),
|
||||
15000
|
||||
)
|
||||
}
|
||||
),
|
||||
1000
|
||||
)
|
||||
expect(result.length).toBe(1000)
|
||||
expect(result.ordered).toBe(true)
|
||||
expect(result.last).toBe(1000)
|
||||
})
|
||||
|
||||
itOn(
|
||||
['android', 'ios'],
|
||||
'a mobile plugin resolves JSArray and JSObject values as JSON arrays and objects',
|
||||
async () => {
|
||||
// On Android, the sample plugin's `jsValues` goes through
|
||||
// `Invoke.resolveObject`, i.e. the plugin JSON mapper rather than
|
||||
// org.json's own `toString`.
|
||||
const values = await tauri((api) =>
|
||||
api.core.invoke('plugin:sample|js_values')
|
||||
)
|
||||
expect(values).toEqual({
|
||||
array: ['a', 1, true],
|
||||
obj: { kind: 'object', nested: [1, 2] }
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('convertFileSrc builds a platform-appropriate asset URL', async () => {
|
||||
const url = await tauri((api) =>
|
||||
api.core.convertFileSrc('/path/to/file.png')
|
||||
)
|
||||
expect(url).toMatch(/(asset:\/\/localhost\/|https?:\/\/asset\.localhost\/)/)
|
||||
})
|
||||
})
|
||||
54
packages/api-e2e/test/specs/dpi.spec.ts
Normal file
54
packages/api-e2e/test/specs/dpi.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { expect } from '@wdio/globals'
|
||||
import { tauri, describeApi } from '../helpers/index.js'
|
||||
|
||||
describeApi('dpi', () => {
|
||||
it('LogicalSize.toPhysical scales up by the factor', async () => {
|
||||
const physical = await tauri((api) => {
|
||||
const size = new api.dpi.LogicalSize(100, 50).toPhysical(2)
|
||||
return { width: size.width, height: size.height }
|
||||
})
|
||||
expect(physical).toEqual({ width: 200, height: 100 })
|
||||
})
|
||||
|
||||
it('PhysicalSize.toLogical scales down by the factor', async () => {
|
||||
const logical = await tauri((api) => {
|
||||
const size = new api.dpi.PhysicalSize(200, 100).toLogical(2)
|
||||
return { width: size.width, height: size.height }
|
||||
})
|
||||
expect(logical).toEqual({ width: 100, height: 50 })
|
||||
})
|
||||
|
||||
it('LogicalPosition.toPhysical and PhysicalPosition.toLogical round-trip', async () => {
|
||||
const result = await tauri((api) => {
|
||||
const physical = new api.dpi.LogicalPosition(10, 20).toPhysical(2)
|
||||
const logical = new api.dpi.PhysicalPosition(10, 20).toLogical(2)
|
||||
return {
|
||||
physical: { x: physical.x, y: physical.y },
|
||||
logical: { x: logical.x, y: logical.y }
|
||||
}
|
||||
})
|
||||
expect(result.physical).toEqual({ x: 20, y: 40 })
|
||||
expect(result.logical).toEqual({ x: 5, y: 10 })
|
||||
})
|
||||
|
||||
it('Size and Position wrappers delegate conversions', async () => {
|
||||
const result = await tauri((api) => {
|
||||
const size = new api.dpi.Size(
|
||||
new api.dpi.LogicalSize(100, 50)
|
||||
).toPhysical(2)
|
||||
const position = new api.dpi.Position(
|
||||
new api.dpi.PhysicalPosition(20, 40)
|
||||
).toLogical(2)
|
||||
return {
|
||||
size: { width: size.width, height: size.height },
|
||||
position: { x: position.x, y: position.y }
|
||||
}
|
||||
})
|
||||
expect(result.size).toEqual({ width: 200, height: 100 })
|
||||
expect(result.position).toEqual({ x: 10, y: 20 })
|
||||
})
|
||||
})
|
||||
125
packages/api-e2e/test/specs/event.spec.ts
Normal file
125
packages/api-e2e/test/specs/event.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { expect } from '@wdio/globals'
|
||||
import { tauri, describeApi } from '../helpers/index.js'
|
||||
|
||||
describeApi('event', () => {
|
||||
it('listen receives an emitted payload', async () => {
|
||||
const payload = await tauri(
|
||||
(api) =>
|
||||
new Promise<{ hello: string }>((resolve, reject) => {
|
||||
api.event
|
||||
.listen<{ hello: string }>('e2e://ping', (event) =>
|
||||
resolve(event.payload)
|
||||
)
|
||||
.then((unlisten) => {
|
||||
api.event.emit('e2e://ping', { hello: 'world' }).catch(reject)
|
||||
setTimeout(() => {
|
||||
unlisten()
|
||||
reject(new Error('event was not received'))
|
||||
}, 5000)
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
)
|
||||
expect(payload).toEqual({ hello: 'world' })
|
||||
})
|
||||
|
||||
it('once fires exactly once even if the event is emitted twice', async () => {
|
||||
const count = await tauri(
|
||||
(api) =>
|
||||
new Promise<number>((resolve, reject) => {
|
||||
let calls = 0
|
||||
api.event
|
||||
.once('e2e://once', () => {
|
||||
calls++
|
||||
})
|
||||
.then(async () => {
|
||||
await api.event.emit('e2e://once')
|
||||
await api.event.emit('e2e://once')
|
||||
setTimeout(() => resolve(calls), 1000)
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
)
|
||||
expect(count).toBe(1)
|
||||
})
|
||||
|
||||
it('unlisten stops further delivery', async () => {
|
||||
const count = await tauri(
|
||||
(api) =>
|
||||
new Promise<number>((resolve, reject) => {
|
||||
let calls = 0
|
||||
api.event
|
||||
.listen('e2e://off', () => {
|
||||
calls++
|
||||
})
|
||||
.then(async (unlisten) => {
|
||||
await api.event.emit('e2e://off')
|
||||
setTimeout(async () => {
|
||||
await unlisten()
|
||||
await api.event.emit('e2e://off')
|
||||
setTimeout(() => resolve(calls), 500)
|
||||
}, 500)
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
)
|
||||
expect(count).toBe(1)
|
||||
})
|
||||
|
||||
it('emitTo delivers only to the targeted label', async () => {
|
||||
const result = await tauri(
|
||||
(api) =>
|
||||
new Promise<{ mainCount: number }>((resolve, reject) => {
|
||||
let mainCount = 0
|
||||
api.event
|
||||
// The listener must be scoped to the `main` label: a default
|
||||
// `listen()` registers the `Any` target, a catch-all that receives
|
||||
// every emit regardless of the `emitTo` label (so it would also see
|
||||
// the `does-not-exist` emit).
|
||||
.listen(
|
||||
'e2e://target',
|
||||
() => {
|
||||
mainCount++
|
||||
},
|
||||
{ target: 'main' }
|
||||
)
|
||||
.then(async (unlisten) => {
|
||||
await api.event.emitTo('main', 'e2e://target') // delivered
|
||||
await api.event.emitTo('does-not-exist', 'e2e://target') // dropped
|
||||
setTimeout(async () => {
|
||||
await unlisten()
|
||||
resolve({ mainCount })
|
||||
}, 800)
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
)
|
||||
expect(result.mainCount).toBe(1)
|
||||
})
|
||||
|
||||
it('bridges events to and from the Rust backend', async () => {
|
||||
// examples/api listens for `js-event` and replies with `rust-event { data }`.
|
||||
const reply = await tauri(
|
||||
(api) =>
|
||||
new Promise<{ data: string }>((resolve, reject) => {
|
||||
api.event
|
||||
.listen<{ data: string }>('rust-event', (event) =>
|
||||
resolve(event.payload)
|
||||
)
|
||||
.then((unlisten) => {
|
||||
api.event.emit('js-event', 'hello from e2e').catch(reject)
|
||||
setTimeout(() => {
|
||||
unlisten()
|
||||
reject(new Error('no rust-event reply received'))
|
||||
}, 5000)
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
)
|
||||
expect(reply).toEqual({ data: 'something else' })
|
||||
})
|
||||
})
|
||||
31
packages/api-e2e/test/specs/image.spec.ts
Normal file
31
packages/api-e2e/test/specs/image.spec.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
|
||||
|
||||
import { expect } from '@wdio/globals'
|
||||
import { tauri, describeApi } from '../helpers/index.js'
|
||||
|
||||
describeApi('image', () => {
|
||||
it('Image.new round-trips raw rgba pixels', async () => {
|
||||
// 2x1 image: red pixel, green pixel.
|
||||
const rgba = [255, 0, 0, 255, 0, 255, 0, 255]
|
||||
const result = await tauri(
|
||||
async (api, pixels, width, height) => {
|
||||
const image = await api.image.Image.new(pixels, width, height)
|
||||
const size = await image.size()
|
||||
const bytes = await image.rgba()
|
||||
return {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
byteLength: bytes.length
|
||||
}
|
||||
},
|
||||
rgba,
|
||||
2,
|
||||
1
|
||||
)
|
||||
expect(result.width).toBe(2)
|
||||
expect(result.height).toBe(1)
|
||||
expect(result.byteLength).toBe(rgba.length)
|
||||
})
|
||||
})
|
||||
88
packages/api-e2e/test/specs/menu.spec.ts
Normal file
88
packages/api-e2e/test/specs/menu.spec.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { expect } from '@wdio/globals'
|
||||
import { tauri, describeApi } from '../helpers/index.js'
|
||||
|
||||
// The menu commands are all allowed by default. These tests only build and
|
||||
// inspect menu resources; they deliberately do not attach the menu to the app
|
||||
// or window, to avoid disturbing the live example app. The menu plugin is
|
||||
// desktop-only, so the whole module is skipped on mobile.
|
||||
describeApi('menu', { desktopOnly: true }, () => {
|
||||
it('Menu.new builds a menu with the given items', async () => {
|
||||
const result = await tauri(async (api) => {
|
||||
const item = await api.menu.MenuItem.new({ text: 'Item A' })
|
||||
const check = await api.menu.CheckMenuItem.new({
|
||||
text: 'Item B',
|
||||
checked: true
|
||||
})
|
||||
const separator = await api.menu.PredefinedMenuItem.new({
|
||||
item: 'Separator'
|
||||
})
|
||||
const submenu = await api.menu.Submenu.new({
|
||||
text: 'More',
|
||||
items: [await api.menu.MenuItem.new({ text: 'Nested' })]
|
||||
})
|
||||
const menu = await api.menu.Menu.new({
|
||||
items: [item, check, separator, submenu]
|
||||
})
|
||||
const items = await menu.items()
|
||||
return { count: items.length }
|
||||
})
|
||||
expect(result.count).toBe(4)
|
||||
})
|
||||
|
||||
it('MenuItem text and enabled state can be read and updated', async () => {
|
||||
const result = await tauri(async (api) => {
|
||||
const item = await api.menu.MenuItem.new({
|
||||
text: 'Original',
|
||||
enabled: true
|
||||
})
|
||||
const before = await item.text()
|
||||
await item.setText('Updated')
|
||||
const after = await item.text()
|
||||
const enabledBefore = await item.isEnabled()
|
||||
await item.setEnabled(false)
|
||||
const enabledAfter = await item.isEnabled()
|
||||
return { before, after, enabledBefore, enabledAfter }
|
||||
})
|
||||
expect(result.before).toBe('Original')
|
||||
expect(result.after).toBe('Updated')
|
||||
expect(result.enabledBefore).toBe(true)
|
||||
expect(result.enabledAfter).toBe(false)
|
||||
})
|
||||
|
||||
it('CheckMenuItem tracks its checked state', async () => {
|
||||
const result = await tauri(async (api) => {
|
||||
const item = await api.menu.CheckMenuItem.new({
|
||||
text: 'Toggle',
|
||||
checked: false
|
||||
})
|
||||
// On Windows the checked state lives in the native menu the item belongs
|
||||
// to, so it must be attached to a menu to be readable.
|
||||
await api.menu.Menu.new({ items: [item] })
|
||||
const before = await item.isChecked()
|
||||
await item.setChecked(true)
|
||||
const after = await item.isChecked()
|
||||
return { before, after }
|
||||
})
|
||||
expect(result.before).toBe(false)
|
||||
expect(result.after).toBe(true)
|
||||
})
|
||||
|
||||
it('append and removeAt mutate the item list', async () => {
|
||||
const result = await tauri(async (api) => {
|
||||
const menu = await api.menu.Menu.new({
|
||||
items: [await api.menu.MenuItem.new({ text: 'First' })]
|
||||
})
|
||||
await menu.append(await api.menu.MenuItem.new({ text: 'Second' }))
|
||||
const afterAppend = (await menu.items()).length
|
||||
await menu.removeAt(0)
|
||||
const afterRemove = (await menu.items()).length
|
||||
return { afterAppend, afterRemove }
|
||||
})
|
||||
expect(result.afterAppend).toBe(2)
|
||||
expect(result.afterRemove).toBe(1)
|
||||
})
|
||||
})
|
||||
62
packages/api-e2e/test/specs/path.spec.ts
Normal file
62
packages/api-e2e/test/specs/path.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import nodePath from 'node:path'
|
||||
import { expect } from '@wdio/globals'
|
||||
import { tauri, describeApi, platform } from '../helpers/index.js'
|
||||
|
||||
// Node's path module for the platform the *app* runs on. That is the host for
|
||||
// the desktop suite, but the mobile suites drive an Android/iOS app (POSIX
|
||||
// paths) from whatever OS hosts the emulator/simulator.
|
||||
const path = platform === 'win32' ? nodePath.win32 : nodePath.posix
|
||||
|
||||
describeApi('path', () => {
|
||||
it('sep and delimiter match the target platform', async () => {
|
||||
const separators = await tauri((api) => ({
|
||||
sep: api.path.sep(),
|
||||
delimiter: api.path.delimiter()
|
||||
}))
|
||||
expect(separators.sep).toBe(path.sep)
|
||||
expect(separators.delimiter).toBe(path.delimiter)
|
||||
})
|
||||
|
||||
it('join, dirname, basename and extname behave like the platform', async () => {
|
||||
const result = await tauri(async (api) => ({
|
||||
joined: await api.path.join('a', 'b', 'c.txt'),
|
||||
dir: await api.path.dirname(await api.path.join('a', 'b', 'c.txt')),
|
||||
base: await api.path.basename(await api.path.join('a', 'b', 'c.txt')),
|
||||
ext: await api.path.extname(await api.path.join('a', 'b', 'c.txt'))
|
||||
}))
|
||||
expect(result.joined).toBe(path.join('a', 'b', 'c.txt'))
|
||||
expect(result.base).toBe('c.txt')
|
||||
expect(result.ext).toBe('txt')
|
||||
expect(result.dir.endsWith(path.join('a', 'b'))).toBe(true)
|
||||
})
|
||||
|
||||
it('normalize and isAbsolute work', async () => {
|
||||
const result = await tauri(async (api) => ({
|
||||
normalized: await api.path.normalize('a/b/../c'),
|
||||
absoluteIsAbsolute: await api.path.isAbsolute(
|
||||
await api.path.resolve('x')
|
||||
),
|
||||
relativeIsAbsolute: await api.path.isAbsolute('a/b')
|
||||
}))
|
||||
expect(result.normalized).toContain('c')
|
||||
expect(result.absoluteIsAbsolute).toBe(true)
|
||||
expect(result.relativeIsAbsolute).toBe(false)
|
||||
})
|
||||
|
||||
it('app directories are absolute and scoped to the identifier', async () => {
|
||||
const dirs = await tauri(async (api) => ({
|
||||
appData: await api.path.appDataDir(),
|
||||
appConfig: await api.path.appConfigDir(),
|
||||
home: await api.path.homeDir(),
|
||||
temp: await api.path.tempDir()
|
||||
}))
|
||||
expect(dirs.appData).toContain('com.tauri.api')
|
||||
expect(dirs.appConfig.length).toBeGreaterThan(0)
|
||||
expect(dirs.home.length).toBeGreaterThan(0)
|
||||
expect(dirs.temp.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
43
packages/api-e2e/test/specs/tray.spec.ts
Normal file
43
packages/api-e2e/test/specs/tray.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { expect } from '@wdio/globals'
|
||||
import { tauri, describeApi } from '../helpers/index.js'
|
||||
|
||||
// Tray commands are allowed by default, but a tray host is not always available
|
||||
// in headless environments — set `E2E_SKIP=tray` there. The tray plugin is
|
||||
// desktop-only, so the whole module is skipped on mobile.
|
||||
describeApi('tray', { desktopOnly: true }, () => {
|
||||
it('creates a tray icon, looks it up and removes it', async () => {
|
||||
const created = await tauri(async (api) => {
|
||||
const icon = await api.app.defaultWindowIcon()
|
||||
const tray = await api.tray.TrayIcon.new({
|
||||
id: 'e2e-tray',
|
||||
icon: icon ?? undefined,
|
||||
tooltip: 'e2e tray'
|
||||
})
|
||||
return tray.id
|
||||
})
|
||||
expect(created).toBe('e2e-tray')
|
||||
|
||||
const foundById = await tauri(async (api) => {
|
||||
const tray = await api.tray.TrayIcon.getById('e2e-tray')
|
||||
return tray !== null
|
||||
})
|
||||
expect(foundById).toBe(true)
|
||||
|
||||
await tauri(async (api) => {
|
||||
const tray = await api.tray.TrayIcon.getById('e2e-tray')
|
||||
await tray?.setVisible(true)
|
||||
return null
|
||||
})
|
||||
|
||||
const removed = await tauri(async (api) => {
|
||||
await api.tray.TrayIcon.removeById('e2e-tray')
|
||||
const tray = await api.tray.TrayIcon.getById('e2e-tray')
|
||||
return tray === null
|
||||
})
|
||||
expect(removed).toBe(true)
|
||||
})
|
||||
})
|
||||
44
packages/api-e2e/test/specs/webview.spec.ts
Normal file
44
packages/api-e2e/test/specs/webview.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { expect } from '@wdio/globals'
|
||||
import { tauri, describeApi, itDesktop } from '../helpers/index.js'
|
||||
|
||||
describeApi('webview', () => {
|
||||
it('getCurrentWebview reports the main label', async () => {
|
||||
expect(await tauri((api) => api.webview.getCurrentWebview().label)).toBe(
|
||||
'main'
|
||||
)
|
||||
})
|
||||
|
||||
it('getAllWebviews includes the main webview', async () => {
|
||||
const labels = await tauri(async (api) =>
|
||||
(await api.webview.getAllWebviews()).map((w) => w.label)
|
||||
)
|
||||
expect(labels).toContain('main')
|
||||
})
|
||||
|
||||
// `webview_position`/`webview_size` are desktop-only commands: on mobile the
|
||||
// webview always fills the window.
|
||||
itDesktop('exposes a physical position and size', async () => {
|
||||
const rect = await tauri(async (api) => {
|
||||
const wv = api.webview.getCurrentWebview()
|
||||
const pos = await wv.position()
|
||||
const size = await wv.size()
|
||||
return { x: pos.x, y: pos.y, width: size.width, height: size.height }
|
||||
})
|
||||
expect(rect.width).toBeGreaterThan(0)
|
||||
expect(rect.height).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
itDesktop('setZoom adjusts and restores the zoom level', async () => {
|
||||
// Requires `core:webview:allow-set-webview-zoom` in the run-app capability.
|
||||
await tauri(async (api) => {
|
||||
const wv = api.webview.getCurrentWebview()
|
||||
await wv.setZoom(1.5)
|
||||
await wv.setZoom(1)
|
||||
return null
|
||||
})
|
||||
})
|
||||
})
|
||||
64
packages/api-e2e/test/specs/webviewWindow.spec.ts
Normal file
64
packages/api-e2e/test/specs/webviewWindow.spec.ts
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
|
||||
|
||||
import { expect } from '@wdio/globals'
|
||||
import { tauri, eventually, describeApi, itDesktop } from '../helpers/index.js'
|
||||
|
||||
describeApi('webviewWindow', () => {
|
||||
it('getCurrentWebviewWindow reports the main label', async () => {
|
||||
expect(
|
||||
await tauri((api) => api.webviewWindow.getCurrentWebviewWindow().label)
|
||||
).toBe('main')
|
||||
})
|
||||
|
||||
// On mobile a new window is a new Android activity / iOS scene that takes
|
||||
// over the foreground (and the automation session's webview context), so
|
||||
// this is only exercised on desktop.
|
||||
itDesktop('creates, finds and closes a new webview window', async () => {
|
||||
// The label must match `main-*` to inherit the run-app capability.
|
||||
const created = await tauri(
|
||||
(api) =>
|
||||
new Promise<boolean>((resolve, reject) => {
|
||||
const w = new api.webviewWindow.WebviewWindow('main-e2e', {
|
||||
width: 400,
|
||||
height: 300
|
||||
})
|
||||
w.once('tauri://created', () => resolve(true)).catch(reject)
|
||||
w.once('tauri://error', (event) =>
|
||||
reject(
|
||||
new Error(`window creation failed: ${String(event.payload)}`)
|
||||
)
|
||||
).catch(reject)
|
||||
setTimeout(() => reject(new Error('window creation timed out')), 8000)
|
||||
})
|
||||
)
|
||||
expect(created).toBe(true)
|
||||
|
||||
const found = await tauri(async (api) => {
|
||||
const byLabel =
|
||||
await api.webviewWindow.WebviewWindow.getByLabel('main-e2e')
|
||||
const all = (await api.webviewWindow.getAllWebviewWindows()).map(
|
||||
(w) => w.label
|
||||
)
|
||||
return { exists: byLabel !== null, all }
|
||||
})
|
||||
expect(found.exists).toBe(true)
|
||||
expect(found.all).toContain('main-e2e')
|
||||
|
||||
await tauri(async (api) => {
|
||||
const w = await api.webviewWindow.WebviewWindow.getByLabel('main-e2e')
|
||||
await w?.close()
|
||||
return null
|
||||
})
|
||||
|
||||
await eventually(async () => {
|
||||
const labels = await tauri(async (api) =>
|
||||
(await api.webviewWindow.getAllWebviewWindows()).map((w) => w.label)
|
||||
)
|
||||
if (labels.includes('main-e2e')) {
|
||||
throw new Error('window is still present after close')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
295
packages/api-e2e/test/specs/window.spec.ts
Normal file
295
packages/api-e2e/test/specs/window.spec.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { expect } from '@wdio/globals'
|
||||
import {
|
||||
tauri,
|
||||
eventually,
|
||||
describeApi,
|
||||
itWm,
|
||||
itDesktop,
|
||||
itOn
|
||||
} from '../helpers/index.js'
|
||||
|
||||
describeApi('window', () => {
|
||||
it('getCurrentWindow reports the main label', async () => {
|
||||
expect(await tauri((api) => api.window.getCurrentWindow().label)).toBe(
|
||||
'main'
|
||||
)
|
||||
})
|
||||
|
||||
it('getAllWindows includes the main window', async () => {
|
||||
const labels = await tauri(async (api) =>
|
||||
(await api.window.getAllWindows()).map((w) => w.label)
|
||||
)
|
||||
expect(labels).toContain('main')
|
||||
})
|
||||
|
||||
// Mobile windows have no title bar: `title()` is always empty and
|
||||
// `setTitle` a no-op there.
|
||||
itDesktop('title can be read and set', async () => {
|
||||
const original = await tauri((api) => api.window.getCurrentWindow().title())
|
||||
await tauri((api) => api.window.getCurrentWindow().setTitle('e2e title'))
|
||||
expect(await tauri((api) => api.window.getCurrentWindow().title())).toBe(
|
||||
'e2e title'
|
||||
)
|
||||
// restore
|
||||
await tauri(
|
||||
(api, title) => api.window.getCurrentWindow().setTitle(title),
|
||||
original
|
||||
)
|
||||
})
|
||||
|
||||
it('reports a positive scale factor and sizes', async () => {
|
||||
const metrics = await tauri(async (api) => {
|
||||
const w = api.window.getCurrentWindow()
|
||||
const scale = await w.scaleFactor()
|
||||
const inner = await w.innerSize()
|
||||
const outer = await w.outerSize()
|
||||
return {
|
||||
scale,
|
||||
inner: { width: inner.width, height: inner.height },
|
||||
outer: { width: outer.width, height: outer.height }
|
||||
}
|
||||
})
|
||||
expect(metrics.scale).toBeGreaterThan(0)
|
||||
expect(metrics.inner.width).toBeGreaterThan(0)
|
||||
expect(metrics.inner.height).toBeGreaterThan(0)
|
||||
expect(metrics.outer.width).toBeGreaterThanOrEqual(metrics.inner.width)
|
||||
})
|
||||
|
||||
itOn(
|
||||
'android',
|
||||
'activityName reports the activity hosting the window',
|
||||
async () => {
|
||||
const name = await tauri((api) =>
|
||||
api.window.getCurrentWindow().activityName()
|
||||
)
|
||||
expect(name).toMatch(/MainActivity/)
|
||||
}
|
||||
)
|
||||
|
||||
// The window is the whole screen on mobile: `set_size` is a no-op.
|
||||
itDesktop('setSize resizes the window', async () => {
|
||||
const scale = await tauri((api) =>
|
||||
api.window.getCurrentWindow().scaleFactor()
|
||||
)
|
||||
const original = await tauri(async (api) => {
|
||||
const size = await api.window.getCurrentWindow().innerSize()
|
||||
return { width: size.width, height: size.height }
|
||||
})
|
||||
|
||||
await tauri(
|
||||
(api, width, height) =>
|
||||
api.window
|
||||
.getCurrentWindow()
|
||||
.setSize(new api.dpi.LogicalSize(width, height)),
|
||||
600,
|
||||
400
|
||||
)
|
||||
|
||||
await eventually(async () => {
|
||||
const inner = await tauri(async (api) => {
|
||||
const size = await api.window.getCurrentWindow().innerSize()
|
||||
return { width: size.width, height: size.height }
|
||||
})
|
||||
const tolerance = Math.ceil(scale) * 4
|
||||
if (Math.abs(inner.width - 600 * scale) > tolerance) {
|
||||
throw new Error(`width ${inner.width} not near ${600 * scale}`)
|
||||
}
|
||||
if (Math.abs(inner.height - 400 * scale) > tolerance) {
|
||||
throw new Error(`height ${inner.height} not near ${400 * scale}`)
|
||||
}
|
||||
})
|
||||
|
||||
// restore the original physical size
|
||||
await tauri(
|
||||
(api, width, height) =>
|
||||
api.window
|
||||
.getCurrentWindow()
|
||||
.setSize(new api.dpi.PhysicalSize(width, height)),
|
||||
original.width,
|
||||
original.height
|
||||
)
|
||||
})
|
||||
|
||||
// Mobile windows are never resizable (`is_resizable` is always `false`).
|
||||
itDesktop('setResizable toggles resizability', async () => {
|
||||
await tauri((api) => api.window.getCurrentWindow().setResizable(false))
|
||||
expect(
|
||||
await tauri((api) => api.window.getCurrentWindow().isResizable())
|
||||
).toBe(false)
|
||||
await tauri((api) => api.window.getCurrentWindow().setResizable(true))
|
||||
expect(
|
||||
await tauri((api) => api.window.getCurrentWindow().isResizable())
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
// Desktop-only commands (`#[cfg(desktop)]` in the window plugin).
|
||||
itDesktop('assorted setters resolve without throwing', async () => {
|
||||
await tauri(async (api) => {
|
||||
const w = api.window.getCurrentWindow()
|
||||
await w.setAlwaysOnTop(true)
|
||||
await w.setAlwaysOnTop(false)
|
||||
await w.setShadow(true)
|
||||
await w.setContentProtected(false)
|
||||
await w.setCursorVisible(true)
|
||||
await w.requestUserAttention(null)
|
||||
return null
|
||||
})
|
||||
})
|
||||
|
||||
itWm('setPosition moves the window', async () => {
|
||||
const original = await tauri(async (api) => {
|
||||
const pos = await api.window.getCurrentWindow().outerPosition()
|
||||
return { x: pos.x, y: pos.y }
|
||||
})
|
||||
|
||||
await tauri(
|
||||
(api, x, y) =>
|
||||
api.window
|
||||
.getCurrentWindow()
|
||||
.setPosition(new api.dpi.PhysicalPosition(x, y)),
|
||||
original.x + 40,
|
||||
original.y + 40
|
||||
)
|
||||
|
||||
await eventually(async () => {
|
||||
const pos = await tauri(async (api) => {
|
||||
const p = await api.window.getCurrentWindow().outerPosition()
|
||||
return { x: p.x, y: p.y }
|
||||
})
|
||||
// Window managers can nudge the final position; allow a generous tolerance.
|
||||
if (
|
||||
Math.abs(pos.x - (original.x + 40)) > 30
|
||||
|| Math.abs(pos.y - (original.y + 40)) > 30
|
||||
) {
|
||||
throw new Error(
|
||||
`position ${pos.x},${pos.y} not near ${original.x + 40},${original.y + 40}`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
await tauri(
|
||||
(api, x, y) =>
|
||||
api.window
|
||||
.getCurrentWindow()
|
||||
.setPosition(new api.dpi.PhysicalPosition(x, y)),
|
||||
original.x,
|
||||
original.y
|
||||
)
|
||||
})
|
||||
|
||||
itWm('minimize and unminimize toggle the minimized state', async () => {
|
||||
await tauri((api) => api.window.getCurrentWindow().minimize())
|
||||
await eventually(async () => {
|
||||
if (
|
||||
!(await tauri((api) => api.window.getCurrentWindow().isMinimized()))
|
||||
) {
|
||||
throw new Error('window is not minimized')
|
||||
}
|
||||
})
|
||||
await tauri((api) => api.window.getCurrentWindow().unminimize())
|
||||
await eventually(async () => {
|
||||
if (await tauri((api) => api.window.getCurrentWindow().isMinimized())) {
|
||||
throw new Error('window is still minimized')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
itWm('maximize and unmaximize toggle the maximized state', async () => {
|
||||
await tauri((api) => api.window.getCurrentWindow().maximize())
|
||||
await eventually(async () => {
|
||||
if (
|
||||
!(await tauri((api) => api.window.getCurrentWindow().isMaximized()))
|
||||
) {
|
||||
throw new Error('window is not maximized')
|
||||
}
|
||||
})
|
||||
await tauri((api) => api.window.getCurrentWindow().unmaximize())
|
||||
await eventually(async () => {
|
||||
if (await tauri((api) => api.window.getCurrentWindow().isMaximized())) {
|
||||
throw new Error('window is still maximized')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
itWm('setFullscreen toggles fullscreen', async () => {
|
||||
await tauri((api) => api.window.getCurrentWindow().setFullscreen(true))
|
||||
await eventually(async () => {
|
||||
if (
|
||||
!(await tauri((api) => api.window.getCurrentWindow().isFullscreen()))
|
||||
) {
|
||||
throw new Error('window is not fullscreen')
|
||||
}
|
||||
})
|
||||
await tauri((api) => api.window.getCurrentWindow().setFullscreen(false))
|
||||
await eventually(async () => {
|
||||
if (await tauri((api) => api.window.getCurrentWindow().isFullscreen())) {
|
||||
throw new Error('window is still fullscreen')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
itWm('setFullscreenOnMonitor goes fullscreen on that monitor', async () => {
|
||||
// Use the monitor the window is on, so the assertion holds on single-monitor CI.
|
||||
const origin = await tauri(async (api) => {
|
||||
const monitor = await api.window.currentMonitor()
|
||||
return monitor ? { x: monitor.position.x, y: monitor.position.y } : null
|
||||
})
|
||||
expect(origin).not.toBeNull()
|
||||
|
||||
await tauri(
|
||||
(api, origin) =>
|
||||
api.window
|
||||
.getCurrentWindow()
|
||||
.setFullscreenOnMonitor(
|
||||
new api.dpi.PhysicalPosition(origin.x, origin.y)
|
||||
),
|
||||
origin!
|
||||
)
|
||||
await eventually(async () => {
|
||||
const state = await tauri(async (api) => {
|
||||
const w = api.window.getCurrentWindow()
|
||||
const position = await w.outerPosition()
|
||||
return {
|
||||
fullscreen: await w.isFullscreen(),
|
||||
x: position.x,
|
||||
y: position.y
|
||||
}
|
||||
})
|
||||
if (!state.fullscreen) {
|
||||
throw new Error('window is not fullscreen')
|
||||
}
|
||||
// a fullscreen window sits at its monitor's origin
|
||||
if (state.x !== origin!.x || state.y !== origin!.y) {
|
||||
throw new Error(
|
||||
`window is at (${state.x}, ${state.y}), expected (${origin!.x}, ${origin!.y})`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
await tauri((api) => api.window.getCurrentWindow().setFullscreen(false))
|
||||
await eventually(async () => {
|
||||
if (await tauri((api) => api.window.getCurrentWindow().isFullscreen())) {
|
||||
throw new Error('window is still fullscreen')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
itWm('hide and show toggle visibility', async () => {
|
||||
await tauri((api) => api.window.getCurrentWindow().hide())
|
||||
await eventually(async () => {
|
||||
if (await tauri((api) => api.window.getCurrentWindow().isVisible())) {
|
||||
throw new Error('window is still visible')
|
||||
}
|
||||
})
|
||||
await tauri((api) => api.window.getCurrentWindow().show())
|
||||
await eventually(async () => {
|
||||
if (!(await tauri((api) => api.window.getCurrentWindow().isVisible()))) {
|
||||
throw new Error('window is not visible')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
17
packages/api-e2e/test/types/global.d.ts
vendored
Normal file
17
packages/api-e2e/test/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// The examples/api app is built with `withGlobalTauri: true`, so the whole
|
||||
// `@tauri-apps/api` surface is available on `window.__TAURI__` inside the webview.
|
||||
// This mirrors that for the functions we serialize and run in the page.
|
||||
|
||||
import type * as TauriApi from '@tauri-apps/api'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__TAURI__: typeof TauriApi
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
20
packages/api-e2e/tsconfig.json
Normal file
20
packages/api-e2e/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2023",
|
||||
"module": "ESNext",
|
||||
// The specs run through tsx (esbuild, pulled in by @wdio/cli), and
|
||||
// @tauri-apps/api's published type definitions use extensionless relative
|
||||
// imports, so bundler-style resolution is what matches the runtime and
|
||||
// resolves the api types.
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["wdio.*.ts", "test/**/*.ts"]
|
||||
}
|
||||
10
packages/api-e2e/wdio.android.conf.ts
Normal file
10
packages/api-e2e/wdio.android.conf.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// `@tauri-apps/api` e2e suite against the examples/api app on an Android
|
||||
// device or emulator, through Appium's UiAutomator2 driver. See wdio.mobile.ts.
|
||||
|
||||
import { mobileConfig } from './wdio.mobile.js'
|
||||
|
||||
export const config = mobileConfig('android')
|
||||
296
packages/api-e2e/wdio.conf.ts
Normal file
296
packages/api-e2e/wdio.conf.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import { spawn, spawnSync, type ChildProcess } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { waitTauriDriverReady } from '@crabnebula/tauri-driver'
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = path.resolve(dirname, '..', '..')
|
||||
const appDir = path.join(repoRoot, 'examples', 'api')
|
||||
const targetDir = process.env.CARGO_TARGET_DIR ?? path.join(repoRoot, 'target')
|
||||
|
||||
// macOS has no native WebDriver for WKWebView, so the CrabNebula Webdriver
|
||||
// (backed by tauri-plugin-automation + the test-runner-backend) is required there.
|
||||
// It can be opted into on the other platforms via E2E_CN_WEBDRIVER for parity.
|
||||
const useCrabNebulaWebdriver =
|
||||
process.platform === 'darwin' || !!process.env.E2E_CN_WEBDRIVER
|
||||
|
||||
// Path passed to the driver as `tauri:options.application`.
|
||||
const application =
|
||||
process.env.E2E_APP_PATH
|
||||
?? (process.platform === 'darwin'
|
||||
? path.join(targetDir, 'debug', 'bundle', 'macos', 'Tauri API.app')
|
||||
: process.platform === 'win32'
|
||||
? path.join(targetDir, 'debug', 'api.exe')
|
||||
: path.join(targetDir, 'debug', 'api'))
|
||||
|
||||
let tauriDriver: ChildProcess | undefined
|
||||
let killedTauriDriver = false
|
||||
let testRunnerBackend: ChildProcess | undefined
|
||||
let killedTestRunnerBackend = false
|
||||
|
||||
export const config: WebdriverIO.Config = {
|
||||
hostname: '127.0.0.1',
|
||||
port: 4444,
|
||||
specs: ['./test/specs/**/*.spec.ts'],
|
||||
// The driver spawns a single app instance and speaks WebDriver to it, so the
|
||||
// suite must run serially.
|
||||
maxInstances: 1,
|
||||
capabilities: [
|
||||
{
|
||||
maxInstances: 1,
|
||||
// `tauri:options` is understood by tauri-driver, not by the WebdriverIO types.
|
||||
'tauri:options': { application }
|
||||
} as unknown as WebdriverIO.Capabilities
|
||||
],
|
||||
reporters: ['spec'],
|
||||
framework: 'mocha',
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
timeout: 120000
|
||||
},
|
||||
connectionRetryCount: 0,
|
||||
specFileRetries: Number(process.env.E2E_SPEC_RETRIES ?? 0),
|
||||
|
||||
onPrepare: async () => {
|
||||
// The example resolves `@tauri-apps/api` from `packages/api/dist`, and its
|
||||
// frontend build (vite) needs it too. Fail early with a clear message.
|
||||
if (
|
||||
!fs.existsSync(path.join(repoRoot, 'packages', 'api', 'dist', 'index.js'))
|
||||
) {
|
||||
throw new Error(
|
||||
'packages/api/dist is missing — run `pnpm build:api` at the repo root before the e2e suite.'
|
||||
)
|
||||
}
|
||||
|
||||
if (!process.env.E2E_SKIP_BUILD && !process.env.E2E_APP_PATH) {
|
||||
// `examples/api`'s `tauri` script is `node ../../packages/cli/tauri.js`,
|
||||
// which requires the native CLI to be built (`pnpm build:cli`).
|
||||
//
|
||||
// The override config does two things for this (debug, test-only) build:
|
||||
// - enables the example's off-by-default `automation` feature, which
|
||||
// registers tauri-plugin-automation;
|
||||
// - disables `removeUnusedCommands`, which the example otherwise sets.
|
||||
// It strips command handlers that no static capability grants, and the
|
||||
// plugin adds its `resolve` command through a *runtime* capability, so
|
||||
// it would be stripped — breaking the CrabNebula Webdriver on macOS.
|
||||
// Passed as an appDir-relative path to sidestep shell quoting.
|
||||
const overrideConfig = path.relative(
|
||||
appDir,
|
||||
path.join(dirname, 'tauri.e2e.conf.json')
|
||||
)
|
||||
const buildArgs = [
|
||||
'tauri',
|
||||
'build',
|
||||
'--debug',
|
||||
'--config',
|
||||
overrideConfig,
|
||||
...(process.platform === 'darwin'
|
||||
? ['--bundles', 'app'] // the .app bundle is needed for tauri:options
|
||||
: ['--no-bundle'])
|
||||
]
|
||||
const build = spawnSync('pnpm', buildArgs, {
|
||||
cwd: appDir,
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
})
|
||||
if (build.status !== 0) {
|
||||
throw new Error(
|
||||
`\`pnpm ${buildArgs.join(' ')}\` failed with status ${build.status}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(application)) {
|
||||
throw new Error(
|
||||
`app not found at ${application} — build it (unset E2E_SKIP_BUILD) or point E2E_APP_PATH at an existing build.`
|
||||
)
|
||||
}
|
||||
|
||||
if (useCrabNebulaWebdriver) {
|
||||
if (!process.env.CN_API_KEY) {
|
||||
throw new Error(
|
||||
'CN_API_KEY is required for the CrabNebula Webdriver (mandatory on macOS, or when E2E_CN_WEBDRIVER=1).'
|
||||
)
|
||||
}
|
||||
testRunnerBackend = spawn('pnpm', ['exec', 'test-runner-backend'], {
|
||||
cwd: dirname,
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
// Lead a new process group so the whole tree (sh -> pnpm ->
|
||||
// test-runner-backend) can be torn down together. See killProcessTree.
|
||||
detached: process.platform !== 'win32'
|
||||
})
|
||||
testRunnerBackend.on('error', (error) => {
|
||||
console.error('test-runner-backend error:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
testRunnerBackend.on('exit', (code) => {
|
||||
if (!killedTestRunnerBackend) {
|
||||
console.error('test-runner-backend exited with code:', code)
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
const { waitTestRunnerBackendReady } =
|
||||
await import('@crabnebula/test-runner-backend')
|
||||
await waitTestRunnerBackendReady()
|
||||
process.env.REMOTE_WEBDRIVER_URL = 'http://127.0.0.1:3000'
|
||||
}
|
||||
},
|
||||
|
||||
// A fresh tauri-driver (and therefore a fresh app instance) per spec file,
|
||||
// so each module's suite runs in isolation.
|
||||
beforeSession: async () => {
|
||||
const args = ['exec', 'tauri-driver']
|
||||
if (process.env.E2E_NATIVE_DRIVER) {
|
||||
args.push('--native-driver', process.env.E2E_NATIVE_DRIVER)
|
||||
}
|
||||
// Reset before each (re)spawn so the `exit` handler below still treats an
|
||||
// unexpected driver crash as fatal on retried spec files.
|
||||
killedTauriDriver = false
|
||||
tauriDriver = spawn('pnpm', args, {
|
||||
cwd: dirname,
|
||||
stdio: [null, process.stdout, process.stderr],
|
||||
shell: true,
|
||||
// Lead a new process group so the whole tree (sh -> pnpm -> tauri-driver
|
||||
// -> native webdriver) can be torn down together. See killProcessTree.
|
||||
detached: process.platform !== 'win32'
|
||||
})
|
||||
tauriDriver.on('error', (error) => {
|
||||
console.error('tauri-driver error:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
tauriDriver.on('exit', (code) => {
|
||||
if (!killedTauriDriver) {
|
||||
console.error('tauri-driver exited with code:', code)
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
await waitTauriDriverReady()
|
||||
},
|
||||
|
||||
// The session is created as soon as the app's window exists, which can be
|
||||
// before the webview has navigated to the app's page: WebView2 on a cold start
|
||||
// (the first launches on a fresh Windows runner) still shows `about:blank`
|
||||
// for a few seconds, so the first spec's scripts ran in a page without
|
||||
// `window.__TAURI__`. Every spec goes through that global, so block until
|
||||
// it exists.
|
||||
before: async (_capabilities, _specs, browser) => {
|
||||
await browser.waitUntil(
|
||||
async () => {
|
||||
try {
|
||||
return (await browser.executeAsync(
|
||||
'var done = arguments[arguments.length - 1]; done(typeof window.__TAURI__ !== "undefined");'
|
||||
)) as boolean
|
||||
} catch {
|
||||
// A command issued mid-navigation can fail on a stale execution
|
||||
// context; that just means "not ready yet".
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
timeout: 30_000,
|
||||
interval: 250,
|
||||
timeoutMsg:
|
||||
'window.__TAURI__ never became available — the app did not load its page.'
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
// Awaited so the driver (and its port) is fully gone before the next spec's
|
||||
// beforeSession spawns a new one on the same port.
|
||||
afterSession: async () => {
|
||||
await closeTauriDriver()
|
||||
},
|
||||
|
||||
onComplete: () => {
|
||||
closeAll()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills a shell-spawned child and everything it started. `child.kill()` only
|
||||
* signals the `sh`/`cmd` wrapper, orphaning the real process (which keeps
|
||||
* holding the WebDriver port and poisons every later spec), so we take down the
|
||||
* whole process group/tree instead.
|
||||
*/
|
||||
function killProcessTree(
|
||||
child: ChildProcess | undefined,
|
||||
signal: NodeJS.Signals = 'SIGTERM'
|
||||
): void {
|
||||
const pid = child?.pid
|
||||
if (!pid || child?.exitCode !== null) return
|
||||
if (process.platform === 'win32') {
|
||||
spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], {
|
||||
stdio: 'ignore'
|
||||
})
|
||||
} else {
|
||||
try {
|
||||
// Negative pid targets the whole process group (see `detached` above).
|
||||
process.kill(-pid, signal)
|
||||
} catch {
|
||||
try {
|
||||
child!.kill(signal)
|
||||
} catch {
|
||||
// already gone
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function closeTauriDriver(): Promise<void> {
|
||||
killedTauriDriver = true
|
||||
const driver = tauriDriver
|
||||
tauriDriver = undefined
|
||||
if (!driver || driver.exitCode !== null) return
|
||||
const exited = new Promise<void>((resolve) =>
|
||||
driver.once('exit', () => resolve())
|
||||
)
|
||||
killProcessTree(driver)
|
||||
// Give it a moment to release the port, then escalate to SIGKILL.
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
await Promise.race([
|
||||
exited,
|
||||
new Promise<void>((resolve) => {
|
||||
timer = setTimeout(() => {
|
||||
killProcessTree(driver, 'SIGKILL')
|
||||
resolve()
|
||||
}, 5000)
|
||||
})
|
||||
])
|
||||
if (timer) clearTimeout(timer)
|
||||
}
|
||||
|
||||
function closeAll() {
|
||||
killedTauriDriver = true
|
||||
killProcessTree(tauriDriver)
|
||||
tauriDriver = undefined
|
||||
killedTestRunnerBackend = true
|
||||
killProcessTree(testRunnerBackend)
|
||||
testRunnerBackend = undefined
|
||||
}
|
||||
|
||||
function onShutdown(fn: () => void) {
|
||||
const cleanup = () => {
|
||||
try {
|
||||
fn()
|
||||
} finally {
|
||||
process.exit()
|
||||
}
|
||||
}
|
||||
for (const signal of [
|
||||
'exit',
|
||||
'SIGINT',
|
||||
'SIGTERM',
|
||||
'SIGHUP',
|
||||
'SIGBREAK'
|
||||
] as const) {
|
||||
process.on(signal, cleanup)
|
||||
}
|
||||
}
|
||||
|
||||
onShutdown(closeAll)
|
||||
10
packages/api-e2e/wdio.ios.conf.ts
Normal file
10
packages/api-e2e/wdio.ios.conf.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// `@tauri-apps/api` e2e suite against the examples/api app on an iOS
|
||||
// simulator, through Appium's XCUITest driver. See wdio.mobile.ts.
|
||||
|
||||
import { mobileConfig } from './wdio.mobile.js'
|
||||
|
||||
export const config = mobileConfig('ios')
|
||||
425
packages/api-e2e/wdio.mobile.ts
Normal file
425
packages/api-e2e/wdio.mobile.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Shared WebdriverIO configuration for the mobile suites (`wdio.android.conf.ts`
|
||||
// and `wdio.ios.conf.ts`). Instead of tauri-driver, the app is driven through
|
||||
// Appium: the UiAutomator2 driver (Android; chromedriver attaches to the
|
||||
// WebView) or the XCUITest driver (iOS simulator; WebKit remote inspector).
|
||||
// Debug builds enable webview debugging on both platforms, which is what makes
|
||||
// the `WEBVIEW_*` context — and `browser.executeAsync` inside it — available.
|
||||
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export type MobilePlatform = 'android' | 'ios'
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = path.resolve(dirname, '..', '..')
|
||||
const appDir = path.join(repoRoot, 'examples', 'api')
|
||||
const tauriDir = path.join(appDir, 'src-tauri')
|
||||
|
||||
/** `identifier` in examples/api's tauri.conf.json. */
|
||||
const appId = 'com.tauri.api'
|
||||
|
||||
/** Where Appium looks for drivers; they are devDependencies of this package. */
|
||||
process.env.APPIUM_HOME ??= dirname
|
||||
|
||||
export function mobileConfig(platform: MobilePlatform): WebdriverIO.Config {
|
||||
const ios = platform === 'ios' ? iosTarget() : undefined
|
||||
|
||||
// Path passed to the driver as `appium:app`.
|
||||
const application =
|
||||
process.env.E2E_APP_PATH
|
||||
?? (ios
|
||||
? path.join(
|
||||
tauriDir,
|
||||
'gen',
|
||||
'apple',
|
||||
'build',
|
||||
ios.outputArch,
|
||||
'Tauri API.app'
|
||||
)
|
||||
: path.join(
|
||||
tauriDir,
|
||||
'gen',
|
||||
'android',
|
||||
'app',
|
||||
'build',
|
||||
'outputs',
|
||||
'apk',
|
||||
'universal',
|
||||
'debug',
|
||||
'app-universal-debug.apk'
|
||||
))
|
||||
|
||||
return {
|
||||
specs: ['./test/specs/**/*.spec.ts'],
|
||||
// One device/simulator, one app instance: the suite runs serially.
|
||||
maxInstances: 1,
|
||||
capabilities: [
|
||||
platform === 'android'
|
||||
? androidCapabilities(application)
|
||||
: iosCapabilities(application)
|
||||
],
|
||||
services: [
|
||||
[
|
||||
'appium',
|
||||
{
|
||||
args: {
|
||||
address: '127.0.0.1',
|
||||
// Appium 3 requires insecure features to be scoped to a driver.
|
||||
// chromedriver_autodownload lets the UiAutomator2 driver fetch a
|
||||
// chromedriver matching the device's WebView (see E2E_CHROMEDRIVER).
|
||||
allowInsecure: 'uiautomator2:chromedriver_autodownload'
|
||||
},
|
||||
// First-session setup (chromedriver download, WebDriverAgent build)
|
||||
// can be slow, hence the generous timeout.
|
||||
appiumStartTimeout: 120_000,
|
||||
logPath: path.join(dirname, 'logs')
|
||||
}
|
||||
]
|
||||
],
|
||||
reporters: ['spec'],
|
||||
framework: 'mocha',
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
timeout: 120000
|
||||
},
|
||||
connectionRetryCount: 0,
|
||||
// The first session boots the simulator/emulator, installs the app and, on
|
||||
// iOS, compiles WebDriverAgent — well over the 120s default that the
|
||||
// request to create the session would otherwise be cut at (the driver
|
||||
// timeouts in the capabilities below are what actually bound it).
|
||||
connectionRetryTimeout: 600_000,
|
||||
specFileRetries: Number(process.env.E2E_SPEC_RETRIES ?? 0),
|
||||
// Tells the specs (which run in worker processes) what the app runs on;
|
||||
// `process.platform` there is the host. See `platform` in test/helpers.
|
||||
runnerEnv: { E2E_PLATFORM: platform },
|
||||
|
||||
onPrepare: async () => {
|
||||
if (
|
||||
!fs.existsSync(
|
||||
path.join(repoRoot, 'packages', 'api', 'dist', 'index.js')
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'packages/api/dist is missing — run `pnpm build:api` at the repo root before the e2e suite.'
|
||||
)
|
||||
}
|
||||
|
||||
if (!process.env.E2E_SKIP_BUILD && !process.env.E2E_APP_PATH) {
|
||||
// `examples/api`'s `tauri` script is `node ../../packages/cli/tauri.js`,
|
||||
// which requires the native CLI to be built (`pnpm build:cli`).
|
||||
//
|
||||
// The generated Android Studio / Xcode project lives in `src-tauri/gen`
|
||||
// (gitignored), so it is initialized on a fresh checkout. The build
|
||||
// installs the Rust target it needs itself, so init skips that.
|
||||
if (
|
||||
!fs.existsSync(path.join(tauriDir, 'gen', ios ? 'apple' : 'android'))
|
||||
) {
|
||||
tauriCli([platform, 'init', '--ci', '--skip-targets-install'])
|
||||
}
|
||||
// A debug build, so wry turns on webview debugging (Android
|
||||
// `setWebContentsDebuggingEnabled`, iOS `isInspectable`), which is what
|
||||
// lets Appium reach the page. Only the target that the device/emulator
|
||||
// actually runs is compiled.
|
||||
tauriCli(
|
||||
ios
|
||||
? [
|
||||
'ios',
|
||||
'build',
|
||||
'--debug',
|
||||
'--target',
|
||||
ios.name,
|
||||
// Simulator builds are not code signed (see
|
||||
// `additionalWebviewBundleIds` in `iosCapabilities`).
|
||||
'--no-sign'
|
||||
]
|
||||
: [
|
||||
'android',
|
||||
'build',
|
||||
'--debug',
|
||||
'--apk',
|
||||
'--target',
|
||||
androidTarget()
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(application)) {
|
||||
throw new Error(
|
||||
`app not found at ${application} — build it (unset E2E_SKIP_BUILD) or point E2E_APP_PATH at an existing build.`
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
// The session starts in the native (`NATIVE_APP`) context. Every spec goes
|
||||
// through `window.__TAURI__`, so switch to the app's webview as soon as it
|
||||
// is attachable and then block until the page has loaded (as the desktop
|
||||
// config does).
|
||||
before: async (_capabilities, _specs, browser: WebdriverIO.Browser) => {
|
||||
let webview: string | undefined
|
||||
await browser.waitUntil(
|
||||
async () => {
|
||||
const contexts = (await browser.getAppiumContexts())
|
||||
// Detailed objects are only returned with `appium:fullContextList`.
|
||||
.map((context) =>
|
||||
typeof context === 'string' ? context : context.id
|
||||
)
|
||||
// Android names the context after the package, and lists every
|
||||
// debuggable WebView on the device (other apps included), so match
|
||||
// ours exactly. iOS names it `WEBVIEW_<pid>.<n>` and only lists the
|
||||
// app under test's webviews, so the first one is it.
|
||||
webview =
|
||||
contexts.find((name) => name === `WEBVIEW_${appId}`)
|
||||
?? (platform === 'ios'
|
||||
? contexts.find((name) => name.startsWith('WEBVIEW_'))
|
||||
: undefined)
|
||||
return webview !== undefined
|
||||
},
|
||||
{
|
||||
// Covers a cold app start plus, on Android, the on-demand chromedriver
|
||||
// download for the first session.
|
||||
timeout: 120_000,
|
||||
interval: 1000,
|
||||
timeoutMsg:
|
||||
'no WEBVIEW context appeared — is the app a debug build (webview debugging enabled)?'
|
||||
}
|
||||
)
|
||||
await browser.switchAppiumContext(webview!)
|
||||
// The specs run the page through `executeAsync`, and the XCUITest driver
|
||||
// starts with a script timeout of 0 (every async script times out at
|
||||
// once) rather than the 30s the other drivers default to.
|
||||
await browser.setTimeout({ script: 30_000 })
|
||||
|
||||
await browser.waitUntil(
|
||||
async () => {
|
||||
try {
|
||||
return (await browser.executeAsync(
|
||||
'var done = arguments[arguments.length - 1]; done(typeof window.__TAURI__ !== "undefined");'
|
||||
)) as boolean
|
||||
} catch {
|
||||
// A command issued mid-navigation can fail on a stale execution
|
||||
// context; that just means "not ready yet".
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
timeout: 30_000,
|
||||
interval: 250,
|
||||
timeoutMsg:
|
||||
'window.__TAURI__ never became available — the app did not load its page.'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs `pnpm tauri <args>` in examples/api, failing loudly. */
|
||||
function tauriCli(args: string[]): void {
|
||||
const result = spawnSync('pnpm', ['tauri', ...args], {
|
||||
cwd: appDir,
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
})
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`\`pnpm tauri ${args.join(' ')}\` failed with status ${result.status}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Android -----------------------------------------------------------------
|
||||
|
||||
function androidCapabilities(app: string): WebdriverIO.Capabilities {
|
||||
return {
|
||||
platformName: 'Android',
|
||||
'appium:automationName': 'UiAutomator2',
|
||||
'appium:app': app,
|
||||
'appium:appPackage': appId,
|
||||
'appium:appActivity': '.MainActivity',
|
||||
// Which device/emulator to use; Appium picks the first connected one
|
||||
// otherwise. E2E_ANDROID_AVD instead boots that AVD.
|
||||
...(process.env.E2E_ANDROID_DEVICE
|
||||
? { 'appium:udid': process.env.E2E_ANDROID_DEVICE }
|
||||
: {}),
|
||||
...(process.env.E2E_ANDROID_AVD
|
||||
? { 'appium:avd': process.env.E2E_ANDROID_AVD }
|
||||
: {}),
|
||||
// chromedriver must match the WebView's Chrome version: either a specific
|
||||
// binary, or one Appium downloads on demand.
|
||||
...(process.env.E2E_CHROMEDRIVER
|
||||
? { 'appium:chromedriverExecutable': process.env.E2E_CHROMEDRIVER }
|
||||
: { 'appium:chromedriverAutodownload': true }),
|
||||
'appium:autoGrantPermissions': true,
|
||||
// Every build keeps the same version, so without this Appium sees the app
|
||||
// as already installed and runs the previous build instead of this one.
|
||||
'appium:enforceAppInstall': true,
|
||||
// Emulators in CI are slow; give the UiAutomator2 server and adb room.
|
||||
'appium:uiautomator2ServerInstallTimeout': 120_000,
|
||||
'appium:uiautomator2ServerLaunchTimeout': 120_000,
|
||||
'appium:adbExecTimeout': 60_000,
|
||||
'appium:newCommandTimeout': 300
|
||||
} as WebdriverIO.Capabilities
|
||||
}
|
||||
|
||||
const androidTargets: Record<string, string> = {
|
||||
// `ro.product.cpu.abi` -> `tauri android build --target`
|
||||
'arm64-v8a': 'aarch64',
|
||||
'armeabi-v7a': 'armv7',
|
||||
x86_64: 'x86_64',
|
||||
x86: 'i686'
|
||||
}
|
||||
|
||||
/**
|
||||
* The Rust target to build the APK for: `E2E_ANDROID_TARGET`, else the ABI of
|
||||
* the connected device/emulator (via adb), else the host's (emulators run the
|
||||
* host architecture).
|
||||
*/
|
||||
function androidTarget(): string {
|
||||
if (process.env.E2E_ANDROID_TARGET) {
|
||||
return process.env.E2E_ANDROID_TARGET
|
||||
}
|
||||
const sdk = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT
|
||||
const adb = sdk ? path.join(sdk, 'platform-tools', 'adb') : 'adb'
|
||||
const abi = spawnSync(
|
||||
adb,
|
||||
[
|
||||
...(process.env.E2E_ANDROID_DEVICE
|
||||
? ['-s', process.env.E2E_ANDROID_DEVICE]
|
||||
: []),
|
||||
'shell',
|
||||
'getprop',
|
||||
'ro.product.cpu.abi'
|
||||
],
|
||||
{ encoding: 'utf8', timeout: 10_000 }
|
||||
)
|
||||
const detected =
|
||||
abi.status === 0 ? androidTargets[abi.stdout.trim()] : undefined
|
||||
return detected ?? (process.arch === 'arm64' ? 'aarch64' : 'x86_64')
|
||||
}
|
||||
|
||||
// --- iOS ---------------------------------------------------------------------
|
||||
|
||||
function iosCapabilities(app: string): WebdriverIO.Capabilities {
|
||||
return {
|
||||
platformName: 'iOS',
|
||||
'appium:automationName': 'XCUITest',
|
||||
'appium:app': app,
|
||||
'appium:bundleId': appId,
|
||||
// The driver looks the app up in the simulator's Web Inspector listing by
|
||||
// bundle identifier. The inspector identifies an app by the
|
||||
// `application-identifier` entitlement Xcode embeds when it code signs a
|
||||
// simulator build (`__TEXT,__entitlements`); `--no-sign` skips signing
|
||||
// altogether (`CODE_SIGNING_ALLOWED=NO`), so the entitlement is missing and
|
||||
// the inspector falls back to `process-<executable name>`. Match that too.
|
||||
'appium:additionalWebviewBundleIds': [
|
||||
`process-${path.basename(app, '.app')}`
|
||||
],
|
||||
'appium:udid': iosSimulator(),
|
||||
// As on Android: the version never changes between builds.
|
||||
'appium:enforceAppInstall': true,
|
||||
// No Simulator.app window. Besides not needing one, the driver otherwise
|
||||
// shuts a simulator that is booted without a visible UI down to relaunch it
|
||||
// with one, on every session — and that shutdown regularly outlasts the
|
||||
// driver's 15s limit on it.
|
||||
'appium:isHeadless': true,
|
||||
// A prebuilt WebDriverAgent (Appium's release build for the simulator, see
|
||||
// `appium driver run xcuitest download-wda -- --kind sim`) is installed and
|
||||
// launched as is, instead of being compiled with xcodebuild on the first
|
||||
// session, which takes minutes on a CI runner.
|
||||
...(process.env.E2E_IOS_WDA
|
||||
? {
|
||||
'appium:usePreinstalledWDA': true,
|
||||
'appium:prebuiltWDAPath': process.env.E2E_IOS_WDA
|
||||
}
|
||||
: {}),
|
||||
// Without one, WebDriverAgent is compiled on the first session, which takes
|
||||
// minutes on a CI runner.
|
||||
'appium:wdaLaunchTimeout': 240_000,
|
||||
'appium:wdaStartupRetries': 3,
|
||||
'appium:simulatorStartupTimeout': 240_000,
|
||||
'appium:newCommandTimeout': 300
|
||||
} as WebdriverIO.Capabilities
|
||||
}
|
||||
|
||||
/**
|
||||
* The Rust target to build the simulator app for (`E2E_IOS_TARGET`, else the
|
||||
* host's architecture) and the directory the CLI exports it to.
|
||||
*/
|
||||
function iosTarget(): { name: string; outputArch: string } {
|
||||
const name =
|
||||
process.env.E2E_IOS_TARGET
|
||||
?? (process.arch === 'arm64' ? 'aarch64-sim' : 'x86_64')
|
||||
// `tauri ios build` writes to gen/apple/build/<arch>/ (cargo-mobile2's
|
||||
// `arch` for the target).
|
||||
const outputArch = name === 'aarch64-sim' ? 'arm64-sim' : name
|
||||
return { name, outputArch }
|
||||
}
|
||||
|
||||
interface SimctlDevice {
|
||||
name: string
|
||||
udid: string
|
||||
state: string
|
||||
isAvailable: boolean
|
||||
deviceTypeIdentifier?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* UDID of the simulator to run on: `E2E_IOS_DEVICE` (a UDID or device name),
|
||||
* else an already-booted iPhone, else the iPhone on the newest installed
|
||||
* runtime. Resolved through `simctl` so nothing has to be hardcoded per Xcode
|
||||
* version.
|
||||
*/
|
||||
function iosSimulator(): string {
|
||||
const requested = process.env.E2E_IOS_DEVICE
|
||||
if (requested && /^[0-9A-F-]{36}$/i.test(requested)) {
|
||||
return requested
|
||||
}
|
||||
const list = spawnSync(
|
||||
'xcrun',
|
||||
['simctl', 'list', 'devices', 'available', '--json'],
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
if (list.status !== 0) {
|
||||
throw new Error(
|
||||
`\`xcrun simctl list\` failed — is Xcode installed?\n${list.stderr}`
|
||||
)
|
||||
}
|
||||
const runtimes = JSON.parse(list.stdout).devices as Record<
|
||||
string,
|
||||
SimctlDevice[]
|
||||
>
|
||||
const iphones = Object.entries(runtimes)
|
||||
// e.g. `com.apple.CoreSimulator.SimRuntime.iOS-18-2`
|
||||
.filter(([runtime]) => runtime.includes('.iOS-'))
|
||||
.sort(([a], [b]) => runtimeVersion(b) - runtimeVersion(a))
|
||||
.flatMap(([, devices]) =>
|
||||
devices.filter(
|
||||
(device) =>
|
||||
device.isAvailable
|
||||
&& (device.deviceTypeIdentifier ?? device.name).includes('iPhone')
|
||||
)
|
||||
)
|
||||
const device = requested
|
||||
? iphones.find((candidate) => candidate.name === requested)
|
||||
: (iphones.find((candidate) => candidate.state === 'Booted') ?? iphones[0])
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
requested
|
||||
? `no available iPhone simulator named "${requested}" (E2E_IOS_DEVICE)`
|
||||
: 'no available iPhone simulator found — install an iOS runtime in Xcode.'
|
||||
)
|
||||
}
|
||||
return device.udid
|
||||
}
|
||||
|
||||
function runtimeVersion(runtime: string): number {
|
||||
const [major = '0', minor = '0'] = (runtime.split('.iOS-')[1] ?? '').split(
|
||||
'-'
|
||||
)
|
||||
return Number(major) * 100 + Number(minor)
|
||||
}
|
||||
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"]
|
||||
}
|
||||
}
|
||||
23
packages/cli/.cargo/config.toml
Normal file
23
packages/cli/.cargo/config.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
# napi-rs can't handle custom target-dir configs yet so we make sure it's set to the default
|
||||
[build]
|
||||
target-dir = "target"
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
linker = "aarch64-linux-musl-gcc"
|
||||
rustflags = ["-C", "target-feature=-crt-static"]
|
||||
|
||||
[target.armv7-unknown-linux-gnueabihf]
|
||||
linker = "arm-linux-gnueabihf-gcc"
|
||||
|
||||
[target.riscv64gc-unknown-linux-gnu]
|
||||
linker = "riscv64-linux-gnu-gcc"
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
[target.i686-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
[target.aarch64-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
2
packages/cli/.gitignore
vendored
Normal file
2
packages/cli/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# debug builds of NAPI
|
||||
*.node
|
||||
14
packages/cli/.npmignore
Normal file
14
packages/cli/.npmignore
Normal file
@@ -0,0 +1,14 @@
|
||||
target
|
||||
Cargo.lock
|
||||
.cargo
|
||||
.github
|
||||
npm
|
||||
test
|
||||
.eslintrc
|
||||
.prettierignore
|
||||
rustfmt.toml
|
||||
*.node
|
||||
__tests__
|
||||
src
|
||||
Cargo.toml
|
||||
build.rs
|
||||
24
packages/cli/Cargo.toml
Normal file
24
packages/cli/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "tauri-cli-node"
|
||||
version = "0.0.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
napi = "3.12.2"
|
||||
napi-derive = "3.6.3"
|
||||
tauri-cli = { path = "../../crates/tauri-cli", default-features = false }
|
||||
log = "0.4.21"
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2.4.1"
|
||||
|
||||
[features]
|
||||
default = ["tauri-cli/default"]
|
||||
native-tls = ["tauri-cli/native-tls"]
|
||||
native-tls-vendored = ["tauri-cli/native-tls-vendored"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
177
packages/cli/LICENSE-APACHE-2.0
Normal file
177
packages/cli/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/cli/LICENSE-MIT
Normal file
21
packages/cli/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/cli/README.md
Normal file
52
packages/cli/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# @tauri-apps/cli
|
||||
|
||||
<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/test-cli-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/cli |  |
|
||||
|
||||
## About Tauri
|
||||
|
||||
Tauri is a polyglot and generic system that is very composable and allows engineers to make a wide variety of applications. It is used for building applications for Desktop Computers using a combination of Rust tools and HTML rendered in a Webview. Apps built with Tauri can ship with any number of pieces of an optional JS API / Rust API so that webviews can control the system via message passing. In fact, developers can extend the default API with their own functionality and bridge the Webview and Rust-based backend easily.
|
||||
|
||||
Tauri apps can have custom menus and have tray-type interfaces. They can be updated, and are managed by the user's operating system as expected. They are very small, because they use the system's webview. They do not ship a runtime, since the final binary is compiled from rust. This makes the reversing of Tauri apps not a trivial task.
|
||||
|
||||
## This module
|
||||
|
||||
Written in Typescript and packaged such that it can be used with `npm`, `pnpm`, `yarn`, and `bun`, this library provides a node.js runner for common tasks when using Tauri, like `pnpm tauri dev`. For the most part it is a wrapper around [tauri-cli](https://github.com/tauri-apps/tauri/blob/dev/crates/tauri-cli).
|
||||
|
||||
To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document.
|
||||
|
||||
## Installation
|
||||
|
||||
The preferred method is to install this module locally as a development dependency:
|
||||
|
||||
```
|
||||
$ pnpm add -D @tauri-apps/cli
|
||||
$ yarn add -D @tauri-apps/cli
|
||||
$ npm add -D @tauri-apps/cli
|
||||
```
|
||||
|
||||
## Semver
|
||||
|
||||
**tauri** is following [Semantic Versioning 2.0](https://semver.org/).
|
||||
|
||||
## Licenses
|
||||
|
||||
Code: (c) 2019 - 2021 - The Tauri Programme within The Commons Conservancy.
|
||||
|
||||
MIT or MIT/Apache 2.0 where applicable.
|
||||
|
||||
Logo: CC-BY-NC-ND
|
||||
|
||||
- Original Tauri Logo Designs by [Daniel Thompson-Yvetot](https://github.com/nothingismagick) and [Guillaume Chau](https://github.com/akryum)
|
||||
1
packages/cli/__tests__/fixtures/empty/.gitignore
vendored
Normal file
1
packages/cli/__tests__/fixtures/empty/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
src-tauri
|
||||
6
packages/cli/__tests__/fixtures/empty/dist/index.html
vendored
Normal file
6
packages/cli/__tests__/fixtures/empty/dist/index.html
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div></div>
|
||||
</body>
|
||||
</html>
|
||||
5
packages/cli/__tests__/fixtures/empty/package.json
Normal file
5
packages/cli/__tests__/fixtures/empty/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"scripts": {
|
||||
"build": ""
|
||||
}
|
||||
}
|
||||
89
packages/cli/__tests__/template.spec.ts
Normal file
89
packages/cli/__tests__/template.spec.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { resolve } from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import {
|
||||
existsSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
rmSync,
|
||||
renameSync
|
||||
} from 'node:fs'
|
||||
import { beforeAll, describe, it } from 'vitest'
|
||||
|
||||
// Build CLI before tests, for local usage only.
|
||||
// CI builds the CLI on different platforms and architectures
|
||||
if (!process.env.CI) {
|
||||
beforeAll(() => {
|
||||
const cliDir = resolve(__dirname, '..')
|
||||
exec('pnpm', ['build:debug'], { cwd: cliDir })
|
||||
})
|
||||
}
|
||||
|
||||
describe('[CLI] @tauri-apps/cli template', () => {
|
||||
it('init a project and builds it', { timeout: 15 * 60 * 1000 }, async () => {
|
||||
const cwd = process.cwd()
|
||||
const fixturePath = resolve(__dirname, './fixtures/empty')
|
||||
const tauriFixturePath = resolve(fixturePath, 'src-tauri')
|
||||
const outPath = resolve(tauriFixturePath, 'target')
|
||||
const cacheOutPath = resolve(fixturePath, 'target')
|
||||
|
||||
process.chdir(fixturePath)
|
||||
|
||||
const outExists = existsSync(outPath)
|
||||
if (outExists) {
|
||||
if (existsSync(cacheOutPath)) {
|
||||
rmSync(cacheOutPath, { recursive: true, force: true })
|
||||
}
|
||||
renameSync(outPath, cacheOutPath)
|
||||
}
|
||||
|
||||
const cli = await import('../main.js')
|
||||
|
||||
await cli.run([
|
||||
'init',
|
||||
'-vv',
|
||||
'--directory',
|
||||
process.cwd(),
|
||||
'--force',
|
||||
'--tauri-path',
|
||||
resolve(__dirname, '../../..'),
|
||||
'--before-build-command',
|
||||
'',
|
||||
'--before-dev-command',
|
||||
'',
|
||||
'--ci'
|
||||
])
|
||||
|
||||
if (outExists) {
|
||||
renameSync(cacheOutPath, outPath)
|
||||
}
|
||||
|
||||
process.chdir(tauriFixturePath)
|
||||
|
||||
const manifestPath = resolve(tauriFixturePath, 'Cargo.toml')
|
||||
const manifestFile = readFileSync(manifestPath).toString()
|
||||
writeFileSync(manifestPath, `workspace = { }\n${manifestFile}`)
|
||||
|
||||
const configPath = resolve(tauriFixturePath, 'tauri.conf.json')
|
||||
const config = readFileSync(configPath).toString()
|
||||
writeFileSync(configPath, config.replace('com.tauri.dev', 'com.tauri.test'))
|
||||
|
||||
await cli.run(['build', '-vv'])
|
||||
process.chdir(cwd)
|
||||
})
|
||||
})
|
||||
|
||||
function exec(
|
||||
bin: string,
|
||||
args?: string[],
|
||||
opts?: {
|
||||
cwd?: string
|
||||
}
|
||||
) {
|
||||
process.platform === 'win32'
|
||||
? spawnSync('cmd', ['/c', bin, ...(args ?? [])], { cwd: opts?.cwd })
|
||||
: spawnSync(bin, args, { cwd: opts?.cwd })
|
||||
}
|
||||
15
packages/cli/append-headers.js
Normal file
15
packages/cli/append-headers.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
const HEADERS = `// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT`
|
||||
|
||||
const fs = require('fs')
|
||||
|
||||
for (const file of ['index.js', 'index.d.ts']) {
|
||||
const content = fs.readFileSync(file, 'utf8')
|
||||
const newContent = `${HEADERS}\n\n${content}`
|
||||
fs.writeFileSync(file, newContent, 'utf8')
|
||||
}
|
||||
7
packages/cli/build.rs
Normal file
7
packages/cli/build.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
fn main() {
|
||||
::napi_build::setup();
|
||||
}
|
||||
9
packages/cli/index.d.ts
vendored
Normal file
9
packages/cli/index.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/* auto-generated by NAPI-RS */
|
||||
/* eslint-disable */
|
||||
export declare function logError(error: string): void
|
||||
|
||||
export declare function run(args: Array<string>, binName: string | undefined | null, callback: ((err: Error | null, arg: boolean) => any)): void
|
||||
595
packages/cli/index.js
Normal file
595
packages/cli/index.js
Normal file
@@ -0,0 +1,595 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// prettier-ignore
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
const { readFileSync } = require('fs')
|
||||
let nativeBinding = null
|
||||
const loadErrors = []
|
||||
|
||||
const isMusl = () => {
|
||||
let musl = false
|
||||
if (process.platform === 'linux') {
|
||||
musl = isMuslFromFilesystem()
|
||||
if (musl === null) {
|
||||
musl = isMuslFromReport()
|
||||
}
|
||||
if (musl === null) {
|
||||
musl = isMuslFromChildProcess()
|
||||
}
|
||||
}
|
||||
return musl
|
||||
}
|
||||
|
||||
const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-')
|
||||
|
||||
const isMuslFromFilesystem = () => {
|
||||
try {
|
||||
return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const isMuslFromReport = () => {
|
||||
let report = null
|
||||
if (process.report && typeof process.report.getReport === 'function') {
|
||||
process.report.excludeNetwork = true
|
||||
report = process.report.getReport()
|
||||
}
|
||||
if (!report) {
|
||||
return null
|
||||
}
|
||||
if (report.header && report.header.glibcVersionRuntime) {
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(report.sharedObjects)) {
|
||||
if (report.sharedObjects.some(isFileMusl)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const isMuslFromChildProcess = () => {
|
||||
try {
|
||||
return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl')
|
||||
} catch (e) {
|
||||
// If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function requireNative() {
|
||||
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
|
||||
try {
|
||||
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH);
|
||||
} catch (err) {
|
||||
loadErrors.push(err)
|
||||
}
|
||||
} else if (process.platform === 'android') {
|
||||
if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./cli.android-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-android-arm64')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-android-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
try {
|
||||
return require('./cli.android-arm-eabi.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-android-arm-eabi')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-android-arm-eabi/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'win32') {
|
||||
if (process.arch === 'x64') {
|
||||
if ((process.config && process.config.variables && process.config.variables.shlib_suffix === 'dll.a') || (process.config && process.config.variables && process.config.variables.node_target_type === 'shared_library')) {
|
||||
try {
|
||||
return require('./cli.win32-x64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-win32-x64-gnu')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-win32-x64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./cli.win32-x64-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-win32-x64-msvc')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-win32-x64-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'ia32') {
|
||||
try {
|
||||
return require('./cli.win32-ia32-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-win32-ia32-msvc')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-win32-ia32-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./cli.win32-arm64-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-win32-arm64-msvc')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-win32-arm64-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'darwin') {
|
||||
try {
|
||||
return require('./cli.darwin-universal.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-darwin-universal')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-darwin-universal/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./cli.darwin-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-darwin-x64')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-darwin-x64/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./cli.darwin-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-darwin-arm64')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-darwin-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'freebsd') {
|
||||
if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./cli.freebsd-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-freebsd-x64')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-freebsd-x64/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./cli.freebsd-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-freebsd-arm64')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-freebsd-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'linux') {
|
||||
if (process.arch === 'x64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./cli.linux-x64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-linux-x64-musl')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-linux-x64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./cli.linux-x64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-linux-x64-gnu')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-linux-x64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./cli.linux-arm64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-linux-arm64-musl')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-linux-arm64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./cli.linux-arm64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-linux-arm64-gnu')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-linux-arm64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./cli.linux-arm-musleabihf.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-linux-arm-musleabihf')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-linux-arm-musleabihf/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./cli.linux-arm-gnueabihf.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-linux-arm-gnueabihf')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-linux-arm-gnueabihf/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'loong64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./cli.linux-loong64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-linux-loong64-musl')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-linux-loong64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./cli.linux-loong64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-linux-loong64-gnu')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-linux-loong64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'riscv64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./cli.linux-riscv64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-linux-riscv64-musl')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-linux-riscv64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./cli.linux-riscv64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-linux-riscv64-gnu')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-linux-riscv64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'ppc64') {
|
||||
try {
|
||||
return require('./cli.linux-ppc64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-linux-ppc64-gnu')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-linux-ppc64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 's390x') {
|
||||
try {
|
||||
return require('./cli.linux-s390x-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-linux-s390x-gnu')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-linux-s390x-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'openharmony') {
|
||||
if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./cli.openharmony-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-openharmony-arm64')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-openharmony-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./cli.openharmony-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-openharmony-x64')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-openharmony-x64/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
try {
|
||||
return require('./cli.openharmony-arm.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@tauri-apps/cli-openharmony-arm')
|
||||
const bindingPackageVersion = require('@tauri-apps/cli-openharmony-arm/package.json').version
|
||||
if (bindingPackageVersion !== '2.11.4' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 2.11.4 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`))
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`))
|
||||
}
|
||||
}
|
||||
|
||||
nativeBinding = requireNative()
|
||||
|
||||
// NAPI_RS_FORCE_WASI is a tri-state flag:
|
||||
// unset / any other value → native binding preferred, WASI is only a fallback
|
||||
// 'true' → force WASI fallback even if native loaded
|
||||
// 'error' → force WASI and throw if no WASI binding is found
|
||||
// Treating any non-empty string as truthy (the historical behavior) meant
|
||||
// NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered
|
||||
// the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file.
|
||||
const forceWasi =
|
||||
process.env.NAPI_RS_FORCE_WASI === 'true' || process.env.NAPI_RS_FORCE_WASI === 'error'
|
||||
|
||||
if (!nativeBinding || forceWasi) {
|
||||
let wasiBinding = null
|
||||
let wasiBindingError = null
|
||||
try {
|
||||
wasiBinding = require('./cli.wasi.cjs')
|
||||
nativeBinding = wasiBinding
|
||||
} catch (err) {
|
||||
if (forceWasi) {
|
||||
wasiBindingError = err
|
||||
}
|
||||
}
|
||||
if (!nativeBinding || forceWasi) {
|
||||
try {
|
||||
wasiBinding = require('@tauri-apps/cli-wasm32-wasi')
|
||||
nativeBinding = wasiBinding
|
||||
} catch (err) {
|
||||
if (forceWasi) {
|
||||
if (!wasiBindingError) {
|
||||
wasiBindingError = err
|
||||
} else {
|
||||
wasiBindingError.cause = err
|
||||
}
|
||||
loadErrors.push(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (process.env.NAPI_RS_FORCE_WASI === 'error' && !wasiBinding) {
|
||||
const error = new Error('WASI binding not found and NAPI_RS_FORCE_WASI is set to error')
|
||||
error.cause = wasiBindingError
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (!nativeBinding) {
|
||||
if (loadErrors.length > 0) {
|
||||
const error = new Error(
|
||||
`Cannot find native binding. ` +
|
||||
`npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` +
|
||||
'Please try `npm i` again after removing both package-lock.json and node_modules directory.',
|
||||
)
|
||||
// assign instead of the `new Error(message, { cause })` options form,
|
||||
// which Node < 16.9 silently ignores
|
||||
error.cause = loadErrors.reduce((err, cur) => {
|
||||
cur.cause = err
|
||||
return cur
|
||||
})
|
||||
throw error
|
||||
}
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
module.exports = nativeBinding
|
||||
module.exports.logError = nativeBinding.logError
|
||||
module.exports.run = nativeBinding.run
|
||||
11
packages/cli/main.d.ts
vendored
Normal file
11
packages/cli/main.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export function run(
|
||||
args: Array<string>,
|
||||
binName?: string | undefined | null
|
||||
): Promise<void>
|
||||
19
packages/cli/main.js
Normal file
19
packages/cli/main.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
const { run, logError } = require('./index')
|
||||
|
||||
module.exports.run = (args, binName) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
run(args, binName, (error, res) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.logError = logError
|
||||
3
packages/cli/npm/darwin-arm64/README.md
Normal file
3
packages/cli/npm/darwin-arm64/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `@tauri-apps/cli-darwin-arm64`
|
||||
|
||||
This is the **aarch64-apple-darwin** binary for `@tauri-apps/cli`
|
||||
33
packages/cli/npm/darwin-arm64/package.json
Normal file
33
packages/cli/npm/darwin-arm64/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@tauri-apps/cli-darwin-arm64",
|
||||
"version": "0.0.0",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"main": "cli.darwin-arm64.node",
|
||||
"files": [
|
||||
"cli.darwin-arm64.node"
|
||||
],
|
||||
"description": "Command line interface for building Tauri apps",
|
||||
"homepage": "https://github.com/tauri-apps/tauri#readme",
|
||||
"contributors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tauri-apps/tauri.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tauri-apps/tauri/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
}
|
||||
3
packages/cli/npm/darwin-x64/README.md
Normal file
3
packages/cli/npm/darwin-x64/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `@tauri-apps/cli-darwin-x64`
|
||||
|
||||
This is the **x86_64-apple-darwin** binary for `@tauri-apps/cli`
|
||||
33
packages/cli/npm/darwin-x64/package.json
Normal file
33
packages/cli/npm/darwin-x64/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@tauri-apps/cli-darwin-x64",
|
||||
"version": "0.0.0",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"main": "cli.darwin-x64.node",
|
||||
"files": [
|
||||
"cli.darwin-x64.node"
|
||||
],
|
||||
"description": "Command line interface for building Tauri apps",
|
||||
"homepage": "https://github.com/tauri-apps/tauri#readme",
|
||||
"contributors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tauri-apps/tauri.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tauri-apps/tauri/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
}
|
||||
3
packages/cli/npm/linux-arm-gnueabihf/README.md
Normal file
3
packages/cli/npm/linux-arm-gnueabihf/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `@tauri-apps/cli-linux-arm-gnueabihf`
|
||||
|
||||
This is the **armv7-unknown-linux-gnueabihf** binary for `@tauri-apps/cli`
|
||||
33
packages/cli/npm/linux-arm-gnueabihf/package.json
Normal file
33
packages/cli/npm/linux-arm-gnueabihf/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@tauri-apps/cli-linux-arm-gnueabihf",
|
||||
"version": "0.0.0",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"main": "cli.linux-arm-gnueabihf.node",
|
||||
"files": [
|
||||
"cli.linux-arm-gnueabihf.node"
|
||||
],
|
||||
"description": "Command line interface for building Tauri apps",
|
||||
"homepage": "https://github.com/tauri-apps/tauri#readme",
|
||||
"contributors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tauri-apps/tauri.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tauri-apps/tauri/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
}
|
||||
3
packages/cli/npm/linux-arm64-gnu/README.md
Normal file
3
packages/cli/npm/linux-arm64-gnu/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `@tauri-apps/cli-linux-arm64-gnu`
|
||||
|
||||
This is the **aarch64-unknown-linux-gnu** binary for `@tauri-apps/cli`
|
||||
36
packages/cli/npm/linux-arm64-gnu/package.json
Normal file
36
packages/cli/npm/linux-arm64-gnu/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@tauri-apps/cli-linux-arm64-gnu",
|
||||
"version": "0.0.0",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"main": "cli.linux-arm64-gnu.node",
|
||||
"files": [
|
||||
"cli.linux-arm64-gnu.node"
|
||||
],
|
||||
"description": "Command line interface for building Tauri apps",
|
||||
"homepage": "https://github.com/tauri-apps/tauri#readme",
|
||||
"contributors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tauri-apps/tauri.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tauri-apps/tauri/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
]
|
||||
}
|
||||
3
packages/cli/npm/linux-arm64-musl/README.md
Normal file
3
packages/cli/npm/linux-arm64-musl/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `@tauri-apps/cli-linux-arm64-musl`
|
||||
|
||||
This is the **aarch64-unknown-linux-musl** binary for `@tauri-apps/cli`
|
||||
36
packages/cli/npm/linux-arm64-musl/package.json
Normal file
36
packages/cli/npm/linux-arm64-musl/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@tauri-apps/cli-linux-arm64-musl",
|
||||
"version": "0.0.0",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"main": "cli.linux-arm64-musl.node",
|
||||
"files": [
|
||||
"cli.linux-arm64-musl.node"
|
||||
],
|
||||
"description": "Command line interface for building Tauri apps",
|
||||
"homepage": "https://github.com/tauri-apps/tauri#readme",
|
||||
"contributors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tauri-apps/tauri.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tauri-apps/tauri/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
]
|
||||
}
|
||||
3
packages/cli/npm/linux-riscv64-gnu/README.md
Normal file
3
packages/cli/npm/linux-riscv64-gnu/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `@tauri-apps/cli-linux-riscv64-gnu`
|
||||
|
||||
This is the **riscv64gc-unknown-linux-gnu** binary for `@tauri-apps/cli`
|
||||
36
packages/cli/npm/linux-riscv64-gnu/package.json
Normal file
36
packages/cli/npm/linux-riscv64-gnu/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@tauri-apps/cli-linux-riscv64-gnu",
|
||||
"version": "0.0.0",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"main": "cli.linux-riscv64-gnu.node",
|
||||
"files": [
|
||||
"cli.linux-riscv64-gnu.node"
|
||||
],
|
||||
"description": "Command line interface for building Tauri apps",
|
||||
"homepage": "https://github.com/tauri-apps/tauri#readme",
|
||||
"contributors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tauri-apps/tauri.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tauri-apps/tauri/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
]
|
||||
}
|
||||
3
packages/cli/npm/linux-x64-gnu/README.md
Normal file
3
packages/cli/npm/linux-x64-gnu/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `@tauri-apps/cli-linux-x64-gnu`
|
||||
|
||||
This is the **x86_64-unknown-linux-gnu** binary for `@tauri-apps/cli`
|
||||
36
packages/cli/npm/linux-x64-gnu/package.json
Normal file
36
packages/cli/npm/linux-x64-gnu/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@tauri-apps/cli-linux-x64-gnu",
|
||||
"version": "0.0.0",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"main": "cli.linux-x64-gnu.node",
|
||||
"files": [
|
||||
"cli.linux-x64-gnu.node"
|
||||
],
|
||||
"description": "Command line interface for building Tauri apps",
|
||||
"homepage": "https://github.com/tauri-apps/tauri#readme",
|
||||
"contributors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tauri-apps/tauri.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tauri-apps/tauri/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
]
|
||||
}
|
||||
3
packages/cli/npm/linux-x64-musl/README.md
Normal file
3
packages/cli/npm/linux-x64-musl/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `@tauri-apps/cli-linux-x64-musl`
|
||||
|
||||
This is the **x86_64-unknown-linux-musl** binary for `@tauri-apps/cli`
|
||||
36
packages/cli/npm/linux-x64-musl/package.json
Normal file
36
packages/cli/npm/linux-x64-musl/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@tauri-apps/cli-linux-x64-musl",
|
||||
"version": "0.0.0",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"main": "cli.linux-x64-musl.node",
|
||||
"files": [
|
||||
"cli.linux-x64-musl.node"
|
||||
],
|
||||
"description": "Command line interface for building Tauri apps",
|
||||
"homepage": "https://github.com/tauri-apps/tauri#readme",
|
||||
"contributors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tauri-apps/tauri.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tauri-apps/tauri/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
]
|
||||
}
|
||||
3
packages/cli/npm/win32-arm64-msvc/README.md
Normal file
3
packages/cli/npm/win32-arm64-msvc/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `@tauri-apps/cli-win32-arm64-msvc`
|
||||
|
||||
This is the **aarch64-pc-windows-msvc** binary for `@tauri-apps/cli`
|
||||
33
packages/cli/npm/win32-arm64-msvc/package.json
Normal file
33
packages/cli/npm/win32-arm64-msvc/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@tauri-apps/cli-win32-arm64-msvc",
|
||||
"version": "0.0.0",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"main": "cli.win32-arm64-msvc.node",
|
||||
"files": [
|
||||
"cli.win32-arm64-msvc.node"
|
||||
],
|
||||
"description": "Command line interface for building Tauri apps",
|
||||
"homepage": "https://github.com/tauri-apps/tauri#readme",
|
||||
"contributors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tauri-apps/tauri.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tauri-apps/tauri/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
}
|
||||
3
packages/cli/npm/win32-ia32-msvc/README.md
Normal file
3
packages/cli/npm/win32-ia32-msvc/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `@tauri-apps/cli-win32-ia32-msvc`
|
||||
|
||||
This is the **i686-pc-windows-msvc** binary for `@tauri-apps/cli`
|
||||
33
packages/cli/npm/win32-ia32-msvc/package.json
Normal file
33
packages/cli/npm/win32-ia32-msvc/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@tauri-apps/cli-win32-ia32-msvc",
|
||||
"version": "0.0.0",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"main": "cli.win32-ia32-msvc.node",
|
||||
"files": [
|
||||
"cli.win32-ia32-msvc.node"
|
||||
],
|
||||
"description": "Command line interface for building Tauri apps",
|
||||
"homepage": "https://github.com/tauri-apps/tauri#readme",
|
||||
"contributors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tauri-apps/tauri.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tauri-apps/tauri/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
}
|
||||
3
packages/cli/npm/win32-x64-msvc/README.md
Normal file
3
packages/cli/npm/win32-x64-msvc/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `@tauri-apps/cli-win32-x64-msvc`
|
||||
|
||||
This is the **x86_64-pc-windows-msvc** binary for `@tauri-apps/cli`
|
||||
33
packages/cli/npm/win32-x64-msvc/package.json
Normal file
33
packages/cli/npm/win32-x64-msvc/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@tauri-apps/cli-win32-x64-msvc",
|
||||
"version": "0.0.0",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"main": "cli.win32-x64-msvc.node",
|
||||
"files": [
|
||||
"cli.win32-x64-msvc.node"
|
||||
],
|
||||
"description": "Command line interface for building Tauri apps",
|
||||
"homepage": "https://github.com/tauri-apps/tauri#readme",
|
||||
"contributors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tauri-apps/tauri.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/tauri-apps/tauri/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
}
|
||||
67
packages/cli/package.json
Normal file
67
packages/cli/package.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "@tauri-apps/cli",
|
||||
"version": "2.11.4",
|
||||
"description": "Command line interface for building Tauri apps",
|
||||
"type": "commonjs",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tauri-apps/tauri.git"
|
||||
},
|
||||
"contributors": [
|
||||
"Tauri Programme within The Commons Conservancy"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tauri-apps/tauri/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tauri-apps/tauri#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"main": "main.js",
|
||||
"types": "main.d.ts",
|
||||
"napi": {
|
||||
"binaryName": "cli",
|
||||
"targets": [
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-apple-darwin",
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"riscv64gc-unknown-linux-gnu",
|
||||
"i686-pc-windows-msvc",
|
||||
"aarch64-pc-windows-msvc"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "3.9.0",
|
||||
"@types/node": "^24.11.0",
|
||||
"cross-env": "10.1.0",
|
||||
"vitest": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"bin": {
|
||||
"tauri": "./tauri.js"
|
||||
},
|
||||
"scripts": {
|
||||
"artifacts": "napi artifacts",
|
||||
"build": "cross-env TARGET=node napi build --platform --profile release-size-optimized",
|
||||
"postbuild": "node append-headers.js",
|
||||
"build:debug": "cross-env TARGET=node napi build --platform",
|
||||
"postbuild:debug": "node append-headers.js",
|
||||
"prepublishOnly": "napi prepublish -t npm --gh-release-id $RELEASE_ID",
|
||||
"prepack": "cp ../../crates/tauri-schema-generator/schemas/config.schema.json .",
|
||||
"version": "napi version",
|
||||
"test": "vitest run",
|
||||
"tauri": "node ./tauri.js"
|
||||
}
|
||||
}
|
||||
53
packages/cli/src/lib.rs
Normal file
53
packages/cli/src/lib.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#![cfg(any(target_os = "macos", target_os = "linux", windows))]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use napi::{
|
||||
Error, Result, Status,
|
||||
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
||||
};
|
||||
|
||||
#[napi_derive::napi]
|
||||
pub fn run(
|
||||
args: Vec<String>,
|
||||
bin_name: Option<String>,
|
||||
callback: Arc<ThreadsafeFunction<bool>>,
|
||||
) -> Result<()> {
|
||||
// we need to run in a separate thread so Node.js consumers
|
||||
// can do work while `tauri dev` is running.
|
||||
std::thread::spawn(move || {
|
||||
let res = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
tauri_cli::try_run(args, bin_name).inspect_err(|e| eprintln!("{e:#}"))
|
||||
})) {
|
||||
Ok(t) => t,
|
||||
Err(_) => {
|
||||
return callback.call(
|
||||
Err(Error::new(
|
||||
Status::GenericFailure,
|
||||
"Tauri CLI unexpected panic",
|
||||
)),
|
||||
ThreadsafeFunctionCallMode::Blocking,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(_) => callback.call(Ok(true), ThreadsafeFunctionCallMode::Blocking),
|
||||
Err(e) => callback.call(
|
||||
Err(Error::new(Status::GenericFailure, format!("{e:#}"))),
|
||||
ThreadsafeFunctionCallMode::Blocking,
|
||||
),
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi_derive::napi]
|
||||
pub fn log_error(error: String) {
|
||||
log::error!("{}", error);
|
||||
}
|
||||
57
packages/cli/tauri.js
Normal file
57
packages/cli/tauri.js
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
const cli = require('./main')
|
||||
const path = require('path')
|
||||
|
||||
const [bin, script, ...args] = process.argv
|
||||
const binStem = path.parse(bin).name.toLowerCase()
|
||||
|
||||
// We want to make a helpful binary name for the underlying CLI helper, if we
|
||||
// can successfully detect what command likely started the execution.
|
||||
let binName
|
||||
|
||||
// deno run -A npm:@tauri-apps/cli or deno task tauri
|
||||
if (globalThis.navigator?.userAgent?.includes('Deno')) {
|
||||
binName = bin
|
||||
}
|
||||
// Even if started by a package manager, the binary will be NodeJS.
|
||||
// Some distribution still use "nodejs" as the binary name.
|
||||
else if (/(nodejs|node|bun|electron)-?[0-9]*$/.test(binStem)) {
|
||||
const managerStem = process.env.npm_execpath
|
||||
? path.parse(process.env.npm_execpath).name.toLowerCase()
|
||||
: null
|
||||
if (managerStem) {
|
||||
let manager
|
||||
switch (managerStem) {
|
||||
// Only supported package manager that has a different filename is npm.
|
||||
case 'npm-cli':
|
||||
manager = 'npm'
|
||||
break
|
||||
|
||||
// Yarn, pnpm, and bun have the same stem name as their bin.
|
||||
// We assume all unknown package managers do as well.
|
||||
default:
|
||||
manager = managerStem
|
||||
break
|
||||
}
|
||||
|
||||
binName = `${manager} run ${process.env.npm_lifecycle_event}`
|
||||
} else {
|
||||
// Assume running NodeJS if we didn't detect a manager from the env.
|
||||
// We normalize the path to prevent the script's absolute path being used.
|
||||
const scriptNormal = path.normalize(path.relative(process.cwd(), script))
|
||||
binName = `${binStem} ${scriptNormal}`
|
||||
}
|
||||
} else {
|
||||
// We don't know what started it, assume it's already stripped.
|
||||
args.unshift(bin)
|
||||
}
|
||||
|
||||
cli.run(args, binName).catch((err) => {
|
||||
cli.logError(err.message)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user