mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-09-22 21:30:44 +02:00
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
This commit is contained in:
@@ -268,7 +268,8 @@
|
|||||||
},
|
},
|
||||||
"single-instance": {
|
"single-instance": {
|
||||||
"path": "./plugins/single-instance",
|
"path": "./plugins/single-instance",
|
||||||
"manager": "rust"
|
"manager": "rust",
|
||||||
|
"dependencies": ["deep-link"]
|
||||||
},
|
},
|
||||||
"sql": {
|
"sql": {
|
||||||
"path": "./plugins/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.
|
||||||
Generated
+2
@@ -1534,6 +1534,7 @@ dependencies = [
|
|||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-deep-link",
|
"tauri-plugin-deep-link",
|
||||||
"tauri-plugin-log",
|
"tauri-plugin-log",
|
||||||
|
"tauri-plugin-single-instance",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6754,6 +6755,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
"tauri-plugin-deep-link",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
"zbus",
|
"zbus",
|
||||||
|
|||||||
@@ -21,3 +21,5 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
dist/
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ serde_json = { workspace = true }
|
|||||||
tauri = { workspace = true, features = ["wry", "compression"] }
|
tauri = { workspace = true, features = ["wry", "compression"] }
|
||||||
tauri-plugin-deep-link = { path = "../../../" }
|
tauri-plugin-deep-link = { path = "../../../" }
|
||||||
tauri-plugin-log = { path = "../../../../log" }
|
tauri-plugin-log = { path = "../../../../log" }
|
||||||
|
tauri-plugin-single-instance = { path = "../../../../single-instance" }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ fn greet(name: &str) -> String {
|
|||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
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_deep_link::init())
|
||||||
.plugin(
|
.plugin(
|
||||||
tauri_plugin_log::Builder::default()
|
tauri_plugin_log::Builder::default()
|
||||||
@@ -20,6 +23,16 @@ pub fn run() {
|
|||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.setup(|app| {
|
.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| {
|
app.listen("deep-link://new-url", |url| {
|
||||||
dbg!(url);
|
dbg!(url);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,8 +29,13 @@
|
|||||||
},
|
},
|
||||||
"deep-link": {
|
"deep-link": {
|
||||||
"mobile": [
|
"mobile": [
|
||||||
{ "host": "fabianlars.de", "pathPrefix": ["/intent"] },
|
{
|
||||||
{ "host": "tauri.app" }
|
"host": "fabianlars.de",
|
||||||
|
"pathPrefix": ["/intent"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "tauri.app"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"desktop": {
|
"desktop": {
|
||||||
"schemes": ["fabianlars", "my-tauri-app"]
|
"schemes": ["fabianlars", "my-tauri-app"]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
use tauri_utils::config::DeepLinkProtocol;
|
use tauri_utils::config::DeepLinkProtocol;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Clone)]
|
||||||
pub struct AssociatedDomain {
|
pub struct AssociatedDomain {
|
||||||
#[serde(deserialize_with = "deserialize_associated_host")]
|
#[serde(deserialize_with = "deserialize_associated_host")]
|
||||||
pub host: String,
|
pub host: String,
|
||||||
@@ -29,7 +29,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Mobile requires `https://<host>` urls.
|
/// Mobile requires `https://<host>` urls.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -41,7 +41,7 @@ pub struct Config {
|
|||||||
pub desktop: DesktopProtocol,
|
pub desktop: DesktopProtocol,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Clone)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
#[allow(unused)] // Used in tauri-bundler
|
#[allow(unused)] // Used in tauri-bundler
|
||||||
pub enum DesktopProtocol {
|
pub enum DesktopProtocol {
|
||||||
@@ -54,3 +54,26 @@ impl Default for DesktopProtocol {
|
|||||||
Self::List(Vec::new())
|
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: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use tauri::{
|
use tauri::{
|
||||||
plugin::{Builder, PluginApi, TauriPlugin},
|
plugin::{Builder, PluginApi, TauriPlugin},
|
||||||
AppHandle, Manager, Runtime,
|
AppHandle, Manager, Runtime,
|
||||||
@@ -17,12 +16,14 @@ pub use error::{Error, Result};
|
|||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
const PLUGIN_IDENTIFIER: &str = "app.tauri.deep_link";
|
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>,
|
app: &AppHandle<R>,
|
||||||
_api: PluginApi<R, C>,
|
api: PluginApi<R, Option<config::Config>>,
|
||||||
) -> crate::Result<DeepLink<R>> {
|
) -> crate::Result<DeepLink<R>> {
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
{
|
{
|
||||||
|
let _api = api;
|
||||||
|
|
||||||
use tauri::{
|
use tauri::{
|
||||||
ipc::{Channel, InvokeResponseBody},
|
ipc::{Channel, InvokeResponseBody},
|
||||||
Emitter,
|
Emitter,
|
||||||
@@ -59,11 +60,28 @@ fn init_deep_link<R: Runtime, C: DeserializeOwned>(
|
|||||||
return Ok(DeepLink(handle));
|
return Ok(DeepLink(handle));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(target_os = "ios")]
|
||||||
Ok(DeepLink {
|
return Ok(DeepLink {
|
||||||
app: app.clone(),
|
app: app.clone(),
|
||||||
current: Default::default(),
|
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")]
|
#[cfg(target_os = "android")]
|
||||||
@@ -90,10 +108,6 @@ mod imp {
|
|||||||
|
|
||||||
impl<R: Runtime> DeepLink<R> {
|
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.
|
/// 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>>> {
|
pub fn get_current(&self) -> crate::Result<Option<Vec<url::Url>>> {
|
||||||
self.0
|
self.0
|
||||||
.run_mobile_plugin::<GetCurrentResponse>("getCurrent", ())
|
.run_mobile_plugin::<GetCurrentResponse>("getCurrent", ())
|
||||||
@@ -154,23 +168,87 @@ mod imp {
|
|||||||
|
|
||||||
/// Access to the deep-link APIs.
|
/// Access to the deep-link APIs.
|
||||||
pub struct DeepLink<R: Runtime> {
|
pub struct DeepLink<R: Runtime> {
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) app: AppHandle<R>,
|
pub(crate) app: AppHandle<R>,
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) current: Mutex<Option<Vec<url::Url>>>,
|
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> {
|
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.
|
/// 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:
|
/// ## 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>>> {
|
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());
|
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.
|
/// Register the app as the default handler for the specified protocol.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ serde_json = { workspace = true }
|
|||||||
tauri = { workspace = true }
|
tauri = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
tauri-plugin-deep-link = { path = "../deep-link", version = "2.0.0-rc.3" }
|
||||||
semver = { version = "1", optional = true }
|
semver = { version = "1", optional = true }
|
||||||
|
|
||||||
[target."cfg(target_os = \"windows\")".dependencies.windows-sys]
|
[target."cfg(target_os = \"windows\")".dependencies.windows-sys]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#![cfg(not(any(target_os = "android", target_os = "ios")))]
|
#![cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
|
||||||
use tauri::{plugin::TauriPlugin, AppHandle, Manager, Runtime};
|
use tauri::{plugin::TauriPlugin, AppHandle, Manager, Runtime};
|
||||||
|
use tauri_plugin_deep_link::DeepLink;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
#[path = "platform_impl/windows.rs"]
|
#[path = "platform_impl/windows.rs"]
|
||||||
@@ -31,9 +32,14 @@ pub(crate) type SingleInstanceCallback<R> =
|
|||||||
dyn FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static;
|
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>(
|
pub fn init<R: Runtime, F: FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static>(
|
||||||
f: F,
|
mut f: F,
|
||||||
) -> TauriPlugin<R> {
|
) -> 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) {
|
pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) {
|
||||||
|
|||||||
Reference in New Issue
Block a user