feat(deep-link): Add deep link support for desktop (#916)

pull/1279/head
Fabian-Lars 1 year ago committed by Lucas Nogueira
parent eb1679b997
commit 021d23bef3
No known key found for this signature in database
GPG Key ID: A05EE2227C581CD7

@ -0,0 +1,6 @@
---
"deep-link": patch
"deep-link-js": patch
---
Added desktop support.

@ -56,6 +56,9 @@ jobs:
tauri-plugin-clipboard-manager:
- .github/workflows/test-rust.yml
- plugins/clipboard-manager/**
tauri-plugin-deep-link:
- .github/workflows/test-rust.yml
- plugins/deep-link/**
tauri-plugin-dialog:
- .github/workflows/test-rust.yml
- plugins/dialog/**

82
Cargo.lock generated

@ -1161,6 +1161,26 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom 0.2.12",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
@ -1621,6 +1641,15 @@ dependencies = [
"syn 2.0.52",
]
[[package]]
name = "dlv-list"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
dependencies = [
"const-random",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
@ -3199,7 +3228,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.5",
]
[[package]]
@ -3830,6 +3859,16 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "ordered-multimap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
dependencies = [
"dlv-list",
"hashbrown 0.14.3",
]
[[package]]
name = "ordered-stream"
version = "0.2.0"
@ -4779,6 +4818,17 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "rust-ini"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d625ed57d8f49af6cfa514c42e1a71fadcff60eb0b1c517ff82fe41aa025b41"
dependencies = [
"cfg-if",
"ordered-multimap",
"trim-in-place",
]
[[package]]
name = "rust_decimal"
version = "1.34.3"
@ -6132,13 +6182,18 @@ dependencies = [
name = "tauri-plugin-deep-link"
version = "2.0.0-beta.4"
dependencies = [
"dunce",
"log",
"rust-ini",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror",
"url",
"windows-registry",
"windows-result",
]
[[package]]
@ -6702,6 +6757,15 @@ dependencies = [
"time-core",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tiny_http"
version = "0.12.0"
@ -7024,6 +7088,12 @@ dependencies = [
"serde_json",
]
[[package]]
name = "trim-in-place"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
[[package]]
name = "try-lock"
version = "0.2.5"
@ -7742,6 +7812,16 @@ dependencies = [
"syn 2.0.52",
]
[[package]]
name = "windows-registry"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f721bc2e55efb506a1a395a545cb76c2481fb023d33b51f0050e7888716281cf"
dependencies = [
"windows-result",
"windows-targets 0.52.5",
]
[[package]]
name = "windows-result"
version = "0.1.1"

@ -13,6 +13,7 @@ log = "0.4"
tauri = "2.0.0-beta.17"
tauri-build = "2.0.0-beta.13"
tauri-plugin = "2.0.0-beta.13"
tauri-utils = "2.0.0-beta.13"
serde_json = "1"
thiserror = "1"
url = "2"

@ -17,12 +17,22 @@ targets = [ "x86_64-linux-android" ]
[build-dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
tauri-utils = { workspace = true }
tauri-plugin = { workspace = true, features = [ "build" ] }
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
tauri = { workspace = true }
tauri-utils = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
[target."cfg(windows)".dependencies]
dunce = "1"
windows-registry = "0.1"
windows-result = "0.1"
[target.'cfg(target_os = "linux")'.dependencies]
rust-ini = "0.21"

@ -93,16 +93,19 @@ See [supporting associated domains](https://developer.apple.com/documentation/xc
## Configuration
Under `tauri.conf.json > plugins > deep-link`, configure the domains you want to associate with your application:
Under `tauri.conf.json > plugins > deep-link`, configure the domains (mobile) and schemes (desktop) you want to associate with your application:
```json
{
"plugins": {
"deep-link": {
"domains": [
"mobile": [
{ "host": "your.website.com", "pathPrefix": ["/open"] },
{ "host": "another.site.br" }
]
],
"desktop": {
"schemes": ["something", "my-tauri-app"]
}
}
}
}
@ -128,10 +131,12 @@ Afterwards all the plugin's APIs are available through the JavaScript guest bind
```javascript
import { onOpenUrl } from "@tauri-apps/plugin-deep-link";
await onOpenUrl((urls) => {
console.log('deep link:', urls);
console.log("deep link:", urls);
});
```
Note that the Plugin will only emit events on macOS, iOS and Android. On Windows and Linux the OS will spawn a new instance of your app with the URL as a CLI argument. If you want your app to behave on Windows & Linux similar to the other platforms you can use the [single-instance](../single-instance/) plugin.
## Contributing
PRs accepted. Please make sure to read the Contributing Guide before making a pull request.

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_DEEPLINK__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var t;async function i(e,t,i){const _=(void 0,{kind:"Any"});return r("plugin:event|listen",{event:e,target:_,handler:n(t)}).then((n=>async()=>async function(e,n){await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}async function _(){return await r("plugin:deep-link|get_current")}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG="tauri://drag",e.DROP="tauri://drop",e.DROP_OVER="tauri://drop-over",e.DROP_CANCELLED="tauri://drag-cancelled"}(t||(t={})),e.getCurrent=_,e.onOpenUrl=async function(e){const n=await _();return null!=n&&e(n),await i("deep-link://new-url",(n=>{e(n.payload)}))},e}({});Object.defineProperty(window.__TAURI__,"deepLink",{value:__TAURI_PLUGIN_DEEPLINK__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_DEEPLINK__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var t;async function i(e,t,i){const a=(void 0,{kind:"Any"});return r("plugin:event|listen",{event:e,target:a,handler:n(t)}).then((n=>async()=>async function(e,n){await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}async function a(){return await r("plugin:deep-link|get_current")}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG="tauri://drag",e.DROP="tauri://drop",e.DROP_OVER="tauri://drop-over",e.DROP_CANCELLED="tauri://drag-cancelled"}(t||(t={})),e.getCurrent=a,e.isRegistered=async function(e){return await r("plugin:deep-link|i_registered",{protocol:e})},e.onOpenUrl=async function(e){const n=await a();return null!=n&&e(n),await i("deep-link://new-url",(n=>{e(n.payload)}))},e.register=async function(e){return await r("plugin:deep-link|register",{protocol:e})},e.unregister=async function(e){return await r("plugin:deep-link|unregister",{protocol:e})},e}({});Object.defineProperty(window.__TAURI__,"deepLink",{value:__TAURI_PLUGIN_DEEPLINK__})}

@ -6,7 +6,7 @@
mod config;
use config::{AssociatedDomain, Config};
const COMMANDS: &[&str] = &["get_current"];
const COMMANDS: &[&str] = &["get_current", "register", "unregister", "is_registered"];
// TODO: Consider using activity-alias in case users may have multiple activities in their app.
// TODO: Do we want to support the other path* configs too?
@ -48,7 +48,7 @@ fn main() {
"DEEP LINK PLUGIN",
"activity",
config
.domains
.mobile
.iter()
.map(intent_filter)
.collect::<Vec<_>>()
@ -62,7 +62,7 @@ fn main() {
entitlements.insert(
"com.apple.developer.associated-domains".into(),
config
.domains
.mobile
.into_iter()
.map(|d| format!("applinks:{}", d.host).into())
.collect::<Vec<_>>()

@ -28,15 +28,13 @@
"hello": "world"
},
"deep-link": {
"domains": [
{
"host": "fabianlars.de",
"pathPrefix": ["/intent"]
},
{
"host": "tauri.app"
}
]
"mobile": [
{ "host": "fabianlars.de", "pathPrefix": ["/intent"] },
{ "host": "tauri.app" }
],
"desktop": {
"schemes": ["fabianlars", "my-tauri-app"]
}
}
},
"bundle": {

@ -3,7 +3,7 @@
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"moduleResolution": "bundler",
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,

@ -5,12 +5,95 @@
import { invoke } from "@tauri-apps/api/core";
import { type UnlistenFn, listen } from "@tauri-apps/api/event";
/**
* Get the current URLs that triggered the deep link. Use this on app load to check whether your app was started via a deep link.
*
* @example
* ```typescript
* import { getCurrent } from '@tauri-apps/plugin-deep-link';
* const urls = await getCurrent();
* ```
*
* #### - **Windows / Linux**: Unsupported.
*
* @since 2.0.0
*/
export async function getCurrent(): Promise<string[] | null> {
return await invoke<string[] | null>("plugin:deep-link|get_current");
return await invoke("plugin:deep-link|get_current");
}
/**
* Register the app as the default handler for the specified protocol.
*
* @param protocol The name of the protocol without `://`. For example, if you want your app to handle `tauri://` links, call this method with `tauri` as the protocol.
*
* @example
* ```typescript
* import { register } from '@tauri-apps/plugin-deep-link';
* await register("my-scheme");
* ```
*
* #### - **macOS / Android / iOS**: Unsupported.
*
* @since 2.0.0
*/
export async function register(protocol: string): Promise<null> {
return await invoke("plugin:deep-link|register", { protocol });
}
/**
* Unregister the app as the default handler for the specified protocol.
*
* @param protocol The name of the protocol without `://`.
*
* @example
* ```typescript
* import { unregister } from '@tauri-apps/plugin-deep-link';
* await unregister("my-scheme");
* ```
*
* #### - **macOS / Linux / Android / iOS**: Unsupported.
*
* @since 2.0.0
*/
export async function unregister(protocol: string): Promise<null> {
return await invoke("plugin:deep-link|unregister", { protocol });
}
// return await invoke("plugin:deep-link|get_current");
/**
* Check whether the app is the default handler for the specified protocol.
*
* @param protocol The name of the protocol without `://`.
*
* @example
* ```typescript
* import { isRegistered } from '@tauri-apps/plugin-deep-link';
* await isRegistered("my-scheme");
* ```
*
* #### - **macOS / Android / iOS**: Unsupported, always returns `true`.
*
* @since 2.0.0
*/
export async function isRegistered(protocol: string): Promise<boolean> {
return await invoke("plugin:deep-link|i_registered", { protocol });
}
/**
* Helper function for the `deep-link://new-url` event to run a function each time the protocol is triggered while the app is running. Use `getCurrent` on app load to check whether your app was started via a deep link.
*
* @param protocol The name of the protocol without `://`.
*
* @example
* ```typescript
* import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
* await onOpenUrl((urls) => { console.log(urls) });
* ```
*
* #### - **Windows / Linux**: Unsupported, the OS will spawn a new app instance passing the URL as a CLI argument.
*
* @since 2.0.0
*/
export async function onOpenUrl(
handler: (urls: string[]) => void,
): Promise<UnlistenFn> {

@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-is-registered"
description = "Enables the is_registered command without any pre-configured scope."
commands.allow = ["is_registered"]
[[permission]]
identifier = "deny-is-registered"
description = "Denies the is_registered command without any pre-configured scope."
commands.deny = ["is_registered"]

@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-register"
description = "Enables the register command without any pre-configured scope."
commands.allow = ["register"]
[[permission]]
identifier = "deny-register"
description = "Denies the register command without any pre-configured scope."
commands.deny = ["register"]

@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-unregister"
description = "Enables the unregister command without any pre-configured scope."
commands.allow = ["unregister"]
[[permission]]
identifier = "deny-unregister"
description = "Denies the unregister command without any pre-configured scope."
commands.deny = ["unregister"]

@ -2,4 +2,10 @@
|------|-----|
|`allow-get-current`|Enables the get_current command without any pre-configured scope.|
|`deny-get-current`|Denies the get_current command without any pre-configured scope.|
|`allow-is-registered`|Enables the is_registered command without any pre-configured scope.|
|`deny-is-registered`|Denies the is_registered command without any pre-configured scope.|
|`allow-register`|Enables the register command without any pre-configured scope.|
|`deny-register`|Denies the register command without any pre-configured scope.|
|`allow-unregister`|Enables the unregister command without any pre-configured scope.|
|`deny-unregister`|Denies the unregister command without any pre-configured scope.|
|`default`|Allows reading the opened deep link via the get_current command|

@ -308,6 +308,48 @@
"deny-get-current"
]
},
{
"description": "allow-is-registered -> Enables the is_registered command without any pre-configured scope.",
"type": "string",
"enum": [
"allow-is-registered"
]
},
{
"description": "deny-is-registered -> Denies the is_registered command without any pre-configured scope.",
"type": "string",
"enum": [
"deny-is-registered"
]
},
{
"description": "allow-register -> Enables the register command without any pre-configured scope.",
"type": "string",
"enum": [
"allow-register"
]
},
{
"description": "deny-register -> Denies the register command without any pre-configured scope.",
"type": "string",
"enum": [
"deny-register"
]
},
{
"description": "allow-unregister -> Enables the unregister command without any pre-configured scope.",
"type": "string",
"enum": [
"allow-unregister"
]
},
{
"description": "deny-unregister -> Denies the unregister command without any pre-configured scope.",
"type": "string",
"enum": [
"deny-unregister"
]
},
{
"description": "default -> Allows reading the opened deep link via the get_current command",
"type": "string",

@ -14,3 +14,33 @@ pub(crate) async fn get_current<R: Runtime>(
) -> Result<Option<Vec<url::Url>>> {
deep_link.get_current()
}
#[command]
pub(crate) async fn register<R: Runtime>(
_app: AppHandle<R>,
_window: Window<R>,
deep_link: State<'_, DeepLink<R>>,
protocol: String,
) -> Result<()> {
deep_link.register(protocol)
}
#[command]
pub(crate) async fn unregister<R: Runtime>(
_app: AppHandle<R>,
_window: Window<R>,
deep_link: State<'_, DeepLink<R>>,
protocol: String,
) -> Result<()> {
deep_link.unregister(protocol)
}
#[command]
pub(crate) async fn is_registered<R: Runtime>(
_app: AppHandle<R>,
_window: Window<R>,
deep_link: State<'_, DeepLink<R>>,
protocol: String,
) -> Result<bool> {
deep_link.is_registered(protocol)
}

@ -4,9 +4,8 @@
// This module is also imported in build.rs!
#![allow(dead_code)]
use serde::{Deserialize, Deserializer};
use tauri_utils::config::DeepLinkProtocol;
#[derive(Deserialize)]
pub struct AssociatedDomain {
@ -32,5 +31,16 @@ where
#[derive(Deserialize)]
pub struct Config {
pub domains: Vec<AssociatedDomain>,
/// Mobile requires `https://<host>` urls.
pub mobile: Vec<AssociatedDomain>,
/// Desktop requires urls starting with `<scheme>://`.
/// These urls are also active in dev mode on Android.
pub desktop: DesktopProtocol,
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum DesktopProtocol {
One(DeepLinkProtocol),
List(Vec<DeepLinkProtocol>),
}

@ -8,8 +8,21 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("unsupported platform")]
UnsupportedPlatform,
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Tauri(#[from] tauri::Error),
#[cfg(target_os = "windows")]
#[error(transparent)]
Windows(#[from] windows_result::Error),
#[cfg(target_os = "linux")]
#[error(transparent)]
Ini(#[from] ini::Error),
#[cfg(target_os = "linux")]
#[error(transparent)]
ParseIni(#[from] ini::ParseError),
#[cfg(mobile)]
#[error(transparent)]
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),

@ -82,32 +82,279 @@ mod imp {
pub struct DeepLink<R: Runtime>(pub(crate) PluginHandle<R>);
impl<R: Runtime> DeepLink<R> {
/// Get the current URLs that triggered the deep link.
/// Get the current URLs that triggered the deep link. Use this on app load to check whether your app was started via a deep link.
///
/// ## Platform-specific:
///
/// - **Windows / Linux**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
pub fn get_current(&self) -> crate::Result<Option<Vec<url::Url>>> {
self.0
.run_mobile_plugin::<GetCurrentResponse>("getCurrent", ())
.map(|v| v.url.map(|url| vec![url]))
.map_err(Into::into)
}
/// Register the app as the default handler for the specified protocol.
///
/// - `protocol`: The name of the protocol without `://`. For example, if you want your app to handle `tauri://` links, call this method with `tauri` as the protocol.
///
/// ## Platform-specific:
///
/// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
pub fn register<S: AsRef<str>>(&self, _protocol: S) -> crate::Result<()> {
Err(crate::Error::UnsupportedPlatform)
}
/// Unregister the app as the default handler for the specified protocol.
///
/// - `protocol`: The name of the protocol without `://`.
///
/// ## Platform-specific:
///
/// - **Linux**: Can only unregister the scheme if it was initially registered with [`register`](`Self::register`). May not work on older distros.
/// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
pub fn unregister<S: AsRef<str>>(&self, _protocol: S) -> crate::Result<()> {
Err(crate::Error::UnsupportedPlatform)
}
/// Check whether the app is the default handler for the specified protocol.
///
/// - `protocol`: The name of the protocol without `://`.
///
/// ## Platform-specific:
///
/// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
pub fn is_registered<S: AsRef<str>>(&self, _protocol: S) -> crate::Result<bool> {
Err(crate::Error::UnsupportedPlatform)
}
}
}
#[cfg(not(target_os = "android"))]
mod imp {
use std::sync::Mutex;
#[cfg(target_os = "linux")]
use std::{
fs::{create_dir_all, File},
io::Write,
process::Command,
};
#[cfg(target_os = "linux")]
use tauri::Manager;
use tauri::{AppHandle, Runtime};
#[cfg(windows)]
use windows_registry::CURRENT_USER;
/// Access to the deep-link APIs.
pub struct DeepLink<R: Runtime> {
#[allow(dead_code)]
pub(crate) app: AppHandle<R>,
#[allow(dead_code)]
pub(crate) current: Mutex<Option<Vec<url::Url>>>,
}
impl<R: Runtime> DeepLink<R> {
/// Get the current URLs that triggered the deep link.
/// Get the current URLs that triggered the deep link. Use this on app load to check whether your app was started via a deep link.
///
/// ## Platform-specific:
///
/// - **Windows / Linux**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
pub fn get_current(&self) -> crate::Result<Option<Vec<url::Url>>> {
Ok(self.current.lock().unwrap().clone())
#[cfg(not(any(windows, target_os = "linux")))]
return Ok(self.current.lock().unwrap().clone());
#[cfg(any(windows, target_os = "linux"))]
Err(crate::Error::UnsupportedPlatform)
}
/// Register the app as the default handler for the specified protocol.
///
/// - `protocol`: The name of the protocol without `://`. For example, if you want your app to handle `tauri://` links, call this method with `tauri` as the protocol.
///
/// ## Platform-specific:
///
/// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
pub fn register<S: AsRef<str>>(&self, _protocol: S) -> crate::Result<()> {
#[cfg(windows)]
{
let key_base = format!("Software\\Classes\\{}", _protocol.as_ref());
let exe = dunce::simplified(&tauri::utils::platform::current_exe()?)
.display()
.to_string();
let key_reg = CURRENT_USER.create(&key_base)?;
key_reg.set_string(
"",
&format!("URL:{} protocol", self.app.config().identifier),
)?;
key_reg.set_string("URL Protocol", "")?;
let icon_reg = CURRENT_USER.create(format!("{key_base}\\DefaultIcon"))?;
icon_reg.set_string("", &format!("{},0", &exe))?;
let cmd_reg = CURRENT_USER.create(format!("{key_base}\\shell\\open\\command"))?;
cmd_reg.set_string("", &format!("{} \"%1\"", &exe))?;
Ok(())
}
#[cfg(target_os = "linux")]
{
let bin = tauri::utils::platform::current_exe()?;
let file_name = format!(
"{}-handler.desktop",
bin.file_name().unwrap().to_string_lossy()
);
let appimage = self.app.env().appimage;
let exec = appimage
.clone()
.unwrap_or_else(|| bin.into_os_string())
.to_string_lossy()
.to_string();
let target = self.app.path().data_dir()?.join("applications");
create_dir_all(&target)?;
let target_file = target.join(&file_name);
let mime_type = format!("x-scheme-handler/{};", _protocol.as_ref());
if let Ok(mut desktop_file) = ini::Ini::load_from_file(&target_file) {
if let Some(section) = desktop_file.section_mut(Some("Desktop Entry")) {
if let Some(mimes) = section.remove("MimeType") {
section.append("MimeType", format!("{mimes};{mime_type};"))
} else {
section.append("MimeType", format!("{mime_type};"))
}
desktop_file.write_to_file(&target_file)?;
}
} else {
let mut file = File::create(target_file)?;
file.write_all(
format!(
include_str!("template.desktop"),
name = self
.app
.config()
.product_name
.clone()
.unwrap_or_else(|| file_name.clone()),
exec = exec,
mime_type = mime_type
)
.as_bytes(),
)?;
}
Command::new("update-desktop-database")
.arg(target)
.status()?;
Command::new("xdg-mime")
.args(["default", &file_name, _protocol.as_ref()])
.status()?;
Ok(())
}
#[cfg(not(any(windows, target_os = "linux")))]
Err(crate::Error::UnsupportedPlatform)
}
/// Unregister the app as the default handler for the specified protocol.
///
/// - `protocol`: The name of the protocol without `://`.
///
/// ## Platform-specific:
///
/// - **Linux**: Can only unregister the scheme if it was initially registered with [`register`](`Self::register`). May not work on older distros.
/// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
pub fn unregister<S: AsRef<str>>(&self, _protocol: S) -> crate::Result<()> {
#[cfg(windows)]
{
CURRENT_USER.remove_tree(format!("Software\\Classes\\{}", _protocol.as_ref()))?;
Ok(())
}
#[cfg(target_os = "linux")]
{
let mimeapps_path = self.app.path().config_dir()?.join("mimeapps.list");
let mut mimeapps = ini::Ini::load_from_file(&mimeapps_path)?;
let file_name = format!(
"{}-handler.desktop",
tauri::utils::platform::current_exe()?
.file_name()
.unwrap()
.to_string_lossy()
);
if let Some(section) = mimeapps.section_mut(Some("Default Applications")) {
let scheme = format!("x-scheme-handler/{}", _protocol.as_ref());
if section.get(&scheme).unwrap_or_default() == file_name {
section.remove(scheme);
}
}
mimeapps.write_to_file(mimeapps_path)?;
Ok(())
}
#[cfg(not(any(windows, target_os = "linux")))]
Err(crate::Error::UnsupportedPlatform)
}
/// Check whether the app is the default handler for the specified protocol.
///
/// - `protocol`: The name of the protocol without `://`.
///
/// ## Platform-specific:
///
/// - **macOS / Android / iOS**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
pub fn is_registered<S: AsRef<str>>(&self, _protocol: S) -> crate::Result<bool> {
#[cfg(windows)]
{
let cmd_reg = CURRENT_USER.open(format!(
"Software\\Classes\\{}\\shell\\open\\command",
_protocol.as_ref()
))?;
let registered_cmd: String = cmd_reg.get_string("")?;
let exe = dunce::simplified(&tauri::utils::platform::current_exe()?)
.display()
.to_string();
Ok(registered_cmd == format!("{} \"%1\"", &exe))
}
#[cfg(target_os = "linux")]
{
let file_name = format!(
"{}-handler.desktop",
tauri::utils::platform::current_exe()?
.file_name()
.unwrap()
.to_string_lossy()
);
let output = Command::new("xdg-mime")
.args([
"query",
"default",
&format!("x-scheme-handler/{}", _protocol.as_ref()),
])
.output()?;
Ok(String::from_utf8_lossy(&output.stdout).contains(&file_name))
}
#[cfg(not(any(windows, target_os = "linux")))]
Err(crate::Error::UnsupportedPlatform)
}
}
}
@ -128,7 +375,12 @@ impl<R: Runtime, T: Manager<R>> crate::DeepLinkExt<R> for T {
/// Initializes the plugin.
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
Builder::new("deep-link")
.invoke_handler(tauri::generate_handler![commands::get_current])
.invoke_handler(tauri::generate_handler![
commands::get_current,
commands::register,
commands::unregister,
commands::is_registered
])
.setup(|app, api| {
app.manage(init_deep_link(app, api)?);
Ok(())

@ -0,0 +1,7 @@
[Desktop Entry]
Type=Application
Name={name}
Exec={exec} %u
Terminal=false
MimeType={mime_type}
NoDisplay=true
Loading…
Cancel
Save