diff --git a/.changes/config.json b/.changes/config.json index c0135c1e..5837be88 100644 --- a/.changes/config.json +++ b/.changes/config.json @@ -268,7 +268,8 @@ }, "single-instance": { "path": "./plugins/single-instance", - "manager": "rust" + "manager": "rust", + "dependencies": ["deep-link"] }, "sql": { "path": "./plugins/sql", diff --git a/.changes/deep-link-get-current-desktop.md b/.changes/deep-link-get-current-desktop.md new file mode 100644 index 00000000..e03fef81 --- /dev/null +++ b/.changes/deep-link-get-current-desktop.md @@ -0,0 +1,5 @@ +--- +"deep-link": patch +--- + +Implement `get_current` on Linux and Windows. diff --git a/.changes/deep-link-register-all.md b/.changes/deep-link-register-all.md new file mode 100644 index 00000000..63edee03 --- /dev/null +++ b/.changes/deep-link-register-all.md @@ -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. diff --git a/.changes/single-instance-deep-link.md b/.changes/single-instance-deep-link.md new file mode 100644 index 00000000..43aac1bf --- /dev/null +++ b/.changes/single-instance-deep-link.md @@ -0,0 +1,5 @@ +--- +"single-instance": patch +--- + +Integrate with the deep link plugin out of the box. diff --git a/Cargo.lock b/Cargo.lock index 356a1730..9a570898 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/plugins/deep-link/examples/app/.gitignore b/plugins/deep-link/examples/app/.gitignore index 251ce6d2..c9b61864 100644 --- a/plugins/deep-link/examples/app/.gitignore +++ b/plugins/deep-link/examples/app/.gitignore @@ -21,3 +21,5 @@ dist-ssr *.njsproj *.sln *.sw? + +dist/ diff --git a/plugins/deep-link/examples/app/src-tauri/Cargo.toml b/plugins/deep-link/examples/app/src-tauri/Cargo.toml index 05d2319f..b4139524 100644 --- a/plugins/deep-link/examples/app/src-tauri/Cargo.toml +++ b/plugins/deep-link/examples/app/src-tauri/Cargo.toml @@ -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] diff --git a/plugins/deep-link/examples/app/src-tauri/src/lib.rs b/plugins/deep-link/examples/app/src-tauri/src/lib.rs index c3948d90..4efa6e2a 100644 --- a/plugins/deep-link/examples/app/src-tauri/src/lib.rs +++ b/plugins/deep-link/examples/app/src-tauri/src/lib.rs @@ -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); }); diff --git a/plugins/deep-link/examples/app/src-tauri/tauri.conf.json b/plugins/deep-link/examples/app/src-tauri/tauri.conf.json index 8ce12b26..ac1c292b 100644 --- a/plugins/deep-link/examples/app/src-tauri/tauri.conf.json +++ b/plugins/deep-link/examples/app/src-tauri/tauri.conf.json @@ -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"] diff --git a/plugins/deep-link/src/config.rs b/plugins/deep-link/src/config.rs index 9cd2e66b..d7bad5b4 100644 --- a/plugins/deep-link/src/config.rs +++ b/plugins/deep-link/src/config.rs @@ -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://` 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 { + match self { + Self::One(protocol) => protocol.schemes.clone(), + Self::List(protocols) => protocols + .iter() + .flat_map(|protocol| protocol.schemes.clone()) + .collect(), + } + } +} diff --git a/plugins/deep-link/src/lib.rs b/plugins/deep-link/src/lib.rs index 52e37cd1..4dafde7b 100644 --- a/plugins/deep-link/src/lib.rs +++ b/plugins/deep-link/src/lib.rs @@ -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( +fn init_deep_link( app: &AppHandle, - _api: PluginApi, + api: PluginApi>, ) -> crate::Result> { #[cfg(target_os = "android")] { + let _api = api; + use tauri::{ ipc::{Channel, InvokeResponseBody}, Emitter, @@ -59,11 +60,28 @@ fn init_deep_link( 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 DeepLink { /// 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>> { self.0 .run_mobile_plugin::("getCurrent", ()) @@ -154,23 +168,87 @@ mod imp { /// Access to the deep-link APIs. pub struct DeepLink { - #[allow(dead_code)] pub(crate) app: AppHandle, - #[allow(dead_code)] pub(crate) current: Mutex>>, + pub(crate) config: Option, + } + + pub(crate) fn deep_link_from_args, I: Iterator>( + config: &crate::config::Config, + mut args: I, + ) -> Option { + 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::().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 DeepLink { + /// 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, I: Iterator>(&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>> { - #[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. diff --git a/plugins/single-instance/Cargo.toml b/plugins/single-instance/Cargo.toml index 5ccd9598..586f698c 100644 --- a/plugins/single-instance/Cargo.toml +++ b/plugins/single-instance/Cargo.toml @@ -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] diff --git a/plugins/single-instance/src/lib.rs b/plugins/single-instance/src/lib.rs index 5d2a5e53..1dc9d61a 100644 --- a/plugins/single-instance/src/lib.rs +++ b/plugins/single-instance/src/lib.rs @@ -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 = dyn FnMut(&AppHandle, Vec, String) + Send + Sync + 'static; pub fn init, Vec, String) + Send + Sync + 'static>( - f: F, + mut f: F, ) -> TauriPlugin { - platform_impl::init(Box::new(f)) + platform_impl::init(Box::new(move |app, args, cwd| { + if let Some(deep_link) = app.try_state::>() { + deep_link.handle_cli_arguments(args.iter()); + } + f(app, args, cwd) + })) } pub fn destroy>(manager: &M) {