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

This commit is contained in:
upstream-mirror
2026-09-23 17:00:42 +05:00
commit feda81bf2f
1178 changed files with 156404 additions and 0 deletions

421
packages/api/src/app.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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'

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff