feat(deep-link): implement getCurrent on Windows/Linux (#1759)

* feat(deep-link): implement getCurrent on Windows/Linux

checks std::env::args() on initialization, also includes integration with the single-instance plugin

* fmt

* update docs, fix event

* add register_all function

* expose api

* be nicer on docs, clippy
pull/1761/head
Lucas Fernandes Nogueira 9 months ago committed by GitHub
parent 77680f6ed8
commit 64a6240f79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -268,7 +268,8 @@
},
"single-instance": {
"path": "./plugins/single-instance",
"manager": "rust"
"manager": "rust",
"dependencies": ["deep-link"]
},
"sql": {
"path": "./plugins/sql",

@ -0,0 +1,5 @@
---
"deep-link": patch
---
Implement `get_current` on Linux and Windows.

@ -0,0 +1,5 @@
---
"deep-link": patch
---
Added `register_all` to register all desktop schemes - useful for Linux to not require a formal AppImage installation.

@ -0,0 +1,5 @@
---
"single-instance": patch
---
Integrate with the deep link plugin out of the box.

2
Cargo.lock generated

@ -1534,6 +1534,7 @@ dependencies = [
"tauri-build",
"tauri-plugin-deep-link",
"tauri-plugin-log",
"tauri-plugin-single-instance",
]
[[package]]
@ -6754,6 +6755,7 @@ dependencies = [
"serde",
"serde_json",
"tauri",
"tauri-plugin-deep-link",
"thiserror",
"windows-sys 0.59.0",
"zbus",

@ -21,3 +21,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
dist/

@ -22,6 +22,7 @@ serde_json = { workspace = true }
tauri = { workspace = true, features = ["wry", "compression"] }
tauri-plugin-deep-link = { path = "../../../" }
tauri-plugin-log = { path = "../../../../log" }
tauri-plugin-single-instance = { path = "../../../../single-instance" }
log = "0.4"
[features]

@ -13,6 +13,9 @@ fn greet(name: &str) -> String {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|_app, argv, _cwd| {
println!("single instance triggered: {argv:?}");
}))
.plugin(tauri_plugin_deep_link::init())
.plugin(
tauri_plugin_log::Builder::default()
@ -20,6 +23,16 @@ pub fn run() {
.build(),
)
.setup(|app| {
// ensure deep links are registered on the system
// this is useful because AppImages requires additional setup to be available in the system
// and calling register() makes the deep links immediately available - without any user input
#[cfg(target_os = "linux")]
{
use tauri_plugin_deep_link::DeepLinkExt;
app.deep_link().register_all()?;
}
app.listen("deep-link://new-url", |url| {
dbg!(url);
});

@ -29,8 +29,13 @@
},
"deep-link": {
"mobile": [
{ "host": "fabianlars.de", "pathPrefix": ["/intent"] },
{ "host": "tauri.app" }
{
"host": "fabianlars.de",
"pathPrefix": ["/intent"]
},
{
"host": "tauri.app"
}
],
"desktop": {
"schemes": ["fabianlars", "my-tauri-app"]

@ -7,7 +7,7 @@
use serde::{Deserialize, Deserializer};
use tauri_utils::config::DeepLinkProtocol;
#[derive(Deserialize)]
#[derive(Deserialize, Clone)]
pub struct AssociatedDomain {
#[serde(deserialize_with = "deserialize_associated_host")]
pub host: String,
@ -29,7 +29,7 @@ where
}
}
#[derive(Deserialize)]
#[derive(Deserialize, Clone)]
pub struct Config {
/// Mobile requires `https://<host>` urls.
#[serde(default)]
@ -41,7 +41,7 @@ pub struct Config {
pub desktop: DesktopProtocol,
}
#[derive(Deserialize)]
#[derive(Deserialize, Clone)]
#[serde(untagged)]
#[allow(unused)] // Used in tauri-bundler
pub enum DesktopProtocol {
@ -54,3 +54,26 @@ impl Default for DesktopProtocol {
Self::List(Vec::new())
}
}
impl DesktopProtocol {
#[allow(dead_code)]
pub fn contains_scheme(&self, scheme: &String) -> bool {
match self {
Self::One(protocol) => protocol.schemes.contains(scheme),
Self::List(protocols) => protocols
.iter()
.any(|protocol| protocol.schemes.contains(scheme)),
}
}
#[allow(dead_code)]
pub fn schemes(&self) -> Vec<String> {
match self {
Self::One(protocol) => protocol.schemes.clone(),
Self::List(protocols) => protocols
.iter()
.flat_map(|protocol| protocol.schemes.clone())
.collect(),
}
}
}

@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::de::DeserializeOwned;
use tauri::{
plugin::{Builder, PluginApi, TauriPlugin},
AppHandle, Manager, Runtime,
@ -17,12 +16,14 @@ pub use error::{Error, Result};
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "app.tauri.deep_link";
fn init_deep_link<R: Runtime, C: DeserializeOwned>(
fn init_deep_link<R: Runtime>(
app: &AppHandle<R>,
_api: PluginApi<R, C>,
api: PluginApi<R, Option<config::Config>>,
) -> crate::Result<DeepLink<R>> {
#[cfg(target_os = "android")]
{
let _api = api;
use tauri::{
ipc::{Channel, InvokeResponseBody},
Emitter,
@ -59,11 +60,28 @@ fn init_deep_link<R: Runtime, C: DeserializeOwned>(
return Ok(DeepLink(handle));
}
#[cfg(not(target_os = "android"))]
Ok(DeepLink {
#[cfg(target_os = "ios")]
return Ok(DeepLink {
app: app.clone(),
current: Default::default(),
})
config: api.config().clone(),
});
#[cfg(desktop)]
{
let args = std::env::args();
let current = if let Some(config) = api.config() {
imp::deep_link_from_args(config, args)
} else {
None
};
Ok(DeepLink {
app: app.clone(),
current: std::sync::Mutex::new(current.map(|url| vec![url])),
config: api.config().clone(),
})
}
}
#[cfg(target_os = "android")]
@ -90,10 +108,6 @@ mod imp {
impl<R: Runtime> DeepLink<R> {
/// 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", ())
@ -154,23 +168,87 @@ mod imp {
/// 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>>>,
pub(crate) config: Option<crate::config::Config>,
}
pub(crate) fn deep_link_from_args<S: AsRef<str>, I: Iterator<Item = S>>(
config: &crate::config::Config,
mut args: I,
) -> Option<url::Url> {
if cfg!(windows) || cfg!(target_os = "linux") {
args.next(); // bin name
let arg = args.next();
let maybe_deep_link = args.next().is_none(); // single argument
if !maybe_deep_link {
return None;
}
if let Some(url) = arg.and_then(|arg| arg.as_ref().parse::<url::Url>().ok()) {
if config.desktop.contains_scheme(&url.scheme().to_string()) {
return Some(url);
} else if cfg!(debug_assertions) {
log::warn!("argument {url} does not match any configured deep link scheme; skipping it");
}
}
}
None
}
impl<R: Runtime> DeepLink<R> {
/// Checks if the provided list of arguments (which should match [`std::env::args`])
/// contains a deep link argument (for Linux and Windows).
///
/// On Linux and Windows the deep links trigger a new app instance with the deep link URL as its only argument.
///
/// This function does what it can to verify if the argument is actually a deep link, though it could also be a regular CLI argument.
/// To enhance its checks, we only match deep links against the schemes defined in the Tauri configuration
/// i.e. dynamic schemes WON'T be processed.
///
/// This function updates the [`Self::get_current`] value and emits a `deep-link://new-url` event.
#[cfg(desktop)]
pub fn handle_cli_arguments<S: AsRef<str>, I: Iterator<Item = S>>(&self, args: I) {
use tauri::Emitter;
let Some(config) = &self.config else {
return;
};
if let Some(url) = deep_link_from_args(config, args) {
let mut current = self.current.lock().unwrap();
current.replace(vec![url.clone()]);
let _ = self.app.emit("deep-link://new-url", vec![url]);
}
}
/// 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`).
/// - **Windows / Linux**: This function reads the command line arguments and checks if there's only one value, which must be an URL with scheme matching one of the configured values.
/// Note that you must manually check the arguments when registering deep link schemes dynamically with [`Self::register`].
/// Additionally, the deep link might have been provided as a CLI argument so you should check if its format matches what you expect.
pub fn get_current(&self) -> crate::Result<Option<Vec<url::Url>>> {
#[cfg(not(any(windows, target_os = "linux")))]
return Ok(self.current.lock().unwrap().clone());
#[cfg(any(windows, target_os = "linux"))]
Err(crate::Error::UnsupportedPlatform)
}
/// Registers all schemes defined in the configuration file.
///
/// This is useful to ensure the schemes are registered even if the user did not install the app properly
/// (e.g. an AppImage that was not properly registered with an AppImage launcher).
pub fn register_all(&self) -> crate::Result<()> {
let Some(config) = &self.config else {
return Ok(());
};
for scheme in config.desktop.schemes() {
self.register(scheme)?;
}
Ok(())
}
/// Register the app as the default handler for the specified protocol.

@ -19,6 +19,7 @@ serde_json = { workspace = true }
tauri = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
tauri-plugin-deep-link = { path = "../deep-link", version = "2.0.0-rc.3" }
semver = { version = "1", optional = true }
[target."cfg(target_os = \"windows\")".dependencies.windows-sys]

@ -13,6 +13,7 @@
#![cfg(not(any(target_os = "android", target_os = "ios")))]
use tauri::{plugin::TauriPlugin, AppHandle, Manager, Runtime};
use tauri_plugin_deep_link::DeepLink;
#[cfg(target_os = "windows")]
#[path = "platform_impl/windows.rs"]
@ -31,9 +32,14 @@ pub(crate) type SingleInstanceCallback<R> =
dyn FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static;
pub fn init<R: Runtime, F: FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static>(
f: F,
mut f: F,
) -> TauriPlugin<R> {
platform_impl::init(Box::new(f))
platform_impl::init(Box::new(move |app, args, cwd| {
if let Some(deep_link) = app.try_state::<DeepLink<R>>() {
deep_link.handle_cli_arguments(args.iter());
}
f(app, args, cwd)
}))
}
pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) {

Loading…
Cancel
Save