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:
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
Reference in New Issue
Block a user