Skip to content

Commit 64a6240

Browse files
authored
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
1 parent 77680f6 commit 64a6240

File tree

13 files changed

+171
-24
lines changed

13 files changed

+171
-24
lines changed

.changes/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,8 @@
268268
},
269269
"single-instance": {
270270
"path": "./plugins/single-instance",
271-
"manager": "rust"
271+
"manager": "rust",
272+
"dependencies": ["deep-link"]
272273
},
273274
"sql": {
274275
"path": "./plugins/sql",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"deep-link": patch
3+
---
4+
5+
Implement `get_current` on Linux and Windows.

.changes/deep-link-register-all.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"deep-link": patch
3+
---
4+
5+
Added `register_all` to register all desktop schemes - useful for Linux to not require a formal AppImage installation.

.changes/single-instance-deep-link.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"single-instance": patch
3+
---
4+
5+
Integrate with the deep link plugin out of the box.

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/deep-link/examples/app/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ dist-ssr
2121
*.njsproj
2222
*.sln
2323
*.sw?
24+
25+
dist/

plugins/deep-link/examples/app/src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ serde_json = { workspace = true }
2222
tauri = { workspace = true, features = ["wry", "compression"] }
2323
tauri-plugin-deep-link = { path = "../../../" }
2424
tauri-plugin-log = { path = "../../../../log" }
25+
tauri-plugin-single-instance = { path = "../../../../single-instance" }
2526
log = "0.4"
2627

2728
[features]

plugins/deep-link/examples/app/src-tauri/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,26 @@ fn greet(name: &str) -> String {
1313
#[cfg_attr(mobile, tauri::mobile_entry_point)]
1414
pub fn run() {
1515
tauri::Builder::default()
16+
.plugin(tauri_plugin_single_instance::init(|_app, argv, _cwd| {
17+
println!("single instance triggered: {argv:?}");
18+
}))
1619
.plugin(tauri_plugin_deep_link::init())
1720
.plugin(
1821
tauri_plugin_log::Builder::default()
1922
.level(log::LevelFilter::Info)
2023
.build(),
2124
)
2225
.setup(|app| {
26+
// ensure deep links are registered on the system
27+
// this is useful because AppImages requires additional setup to be available in the system
28+
// and calling register() makes the deep links immediately available - without any user input
29+
#[cfg(target_os = "linux")]
30+
{
31+
use tauri_plugin_deep_link::DeepLinkExt;
32+
33+
app.deep_link().register_all()?;
34+
}
35+
2336
app.listen("deep-link://new-url", |url| {
2437
dbg!(url);
2538
});

plugins/deep-link/examples/app/src-tauri/tauri.conf.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@
2929
},
3030
"deep-link": {
3131
"mobile": [
32-
{ "host": "fabianlars.de", "pathPrefix": ["/intent"] },
33-
{ "host": "tauri.app" }
32+
{
33+
"host": "fabianlars.de",
34+
"pathPrefix": ["/intent"]
35+
},
36+
{
37+
"host": "tauri.app"
38+
}
3439
],
3540
"desktop": {
3641
"schemes": ["fabianlars", "my-tauri-app"]

plugins/deep-link/src/config.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use serde::{Deserialize, Deserializer};
88
use tauri_utils::config::DeepLinkProtocol;
99

10-
#[derive(Deserialize)]
10+
#[derive(Deserialize, Clone)]
1111
pub struct AssociatedDomain {
1212
#[serde(deserialize_with = "deserialize_associated_host")]
1313
pub host: String,
@@ -29,7 +29,7 @@ where
2929
}
3030
}
3131

32-
#[derive(Deserialize)]
32+
#[derive(Deserialize, Clone)]
3333
pub struct Config {
3434
/// Mobile requires `https://<host>` urls.
3535
#[serde(default)]
@@ -41,7 +41,7 @@ pub struct Config {
4141
pub desktop: DesktopProtocol,
4242
}
4343

44-
#[derive(Deserialize)]
44+
#[derive(Deserialize, Clone)]
4545
#[serde(untagged)]
4646
#[allow(unused)] // Used in tauri-bundler
4747
pub enum DesktopProtocol {
@@ -54,3 +54,26 @@ impl Default for DesktopProtocol {
5454
Self::List(Vec::new())
5555
}
5656
}
57+
58+
impl DesktopProtocol {
59+
#[allow(dead_code)]
60+
pub fn contains_scheme(&self, scheme: &String) -> bool {
61+
match self {
62+
Self::One(protocol) => protocol.schemes.contains(scheme),
63+
Self::List(protocols) => protocols
64+
.iter()
65+
.any(|protocol| protocol.schemes.contains(scheme)),
66+
}
67+
}
68+
69+
#[allow(dead_code)]
70+
pub fn schemes(&self) -> Vec<String> {
71+
match self {
72+
Self::One(protocol) => protocol.schemes.clone(),
73+
Self::List(protocols) => protocols
74+
.iter()
75+
.flat_map(|protocol| protocol.schemes.clone())
76+
.collect(),
77+
}
78+
}
79+
}

plugins/deep-link/src/lib.rs

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// SPDX-License-Identifier: Apache-2.0
33
// SPDX-License-Identifier: MIT
44

5-
use serde::de::DeserializeOwned;
65
use tauri::{
76
plugin::{Builder, PluginApi, TauriPlugin},
87
AppHandle, Manager, Runtime,
@@ -17,12 +16,14 @@ pub use error::{Error, Result};
1716
#[cfg(target_os = "android")]
1817
const PLUGIN_IDENTIFIER: &str = "app.tauri.deep_link";
1918

20-
fn init_deep_link<R: Runtime, C: DeserializeOwned>(
19+
fn init_deep_link<R: Runtime>(
2120
app: &AppHandle<R>,
22-
_api: PluginApi<R, C>,
21+
api: PluginApi<R, Option<config::Config>>,
2322
) -> crate::Result<DeepLink<R>> {
2423
#[cfg(target_os = "android")]
2524
{
25+
let _api = api;
26+
2627
use tauri::{
2728
ipc::{Channel, InvokeResponseBody},
2829
Emitter,
@@ -59,11 +60,28 @@ fn init_deep_link<R: Runtime, C: DeserializeOwned>(
5960
return Ok(DeepLink(handle));
6061
}
6162

62-
#[cfg(not(target_os = "android"))]
63-
Ok(DeepLink {
63+
#[cfg(target_os = "ios")]
64+
return Ok(DeepLink {
6465
app: app.clone(),
6566
current: Default::default(),
66-
})
67+
config: api.config().clone(),
68+
});
69+
70+
#[cfg(desktop)]
71+
{
72+
let args = std::env::args();
73+
let current = if let Some(config) = api.config() {
74+
imp::deep_link_from_args(config, args)
75+
} else {
76+
None
77+
};
78+
79+
Ok(DeepLink {
80+
app: app.clone(),
81+
current: std::sync::Mutex::new(current.map(|url| vec![url])),
82+
config: api.config().clone(),
83+
})
84+
}
6785
}
6886

6987
#[cfg(target_os = "android")]
@@ -90,10 +108,6 @@ mod imp {
90108

91109
impl<R: Runtime> DeepLink<R> {
92110
/// 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.
93-
///
94-
/// ## Platform-specific:
95-
///
96-
/// - **Windows / Linux**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
97111
pub fn get_current(&self) -> crate::Result<Option<Vec<url::Url>>> {
98112
self.0
99113
.run_mobile_plugin::<GetCurrentResponse>("getCurrent", ())
@@ -154,23 +168,87 @@ mod imp {
154168

155169
/// Access to the deep-link APIs.
156170
pub struct DeepLink<R: Runtime> {
157-
#[allow(dead_code)]
158171
pub(crate) app: AppHandle<R>,
159-
#[allow(dead_code)]
160172
pub(crate) current: Mutex<Option<Vec<url::Url>>>,
173+
pub(crate) config: Option<crate::config::Config>,
174+
}
175+
176+
pub(crate) fn deep_link_from_args<S: AsRef<str>, I: Iterator<Item = S>>(
177+
config: &crate::config::Config,
178+
mut args: I,
179+
) -> Option<url::Url> {
180+
if cfg!(windows) || cfg!(target_os = "linux") {
181+
args.next(); // bin name
182+
let arg = args.next();
183+
184+
let maybe_deep_link = args.next().is_none(); // single argument
185+
if !maybe_deep_link {
186+
return None;
187+
}
188+
189+
if let Some(url) = arg.and_then(|arg| arg.as_ref().parse::<url::Url>().ok()) {
190+
if config.desktop.contains_scheme(&url.scheme().to_string()) {
191+
return Some(url);
192+
} else if cfg!(debug_assertions) {
193+
log::warn!("argument {url} does not match any configured deep link scheme; skipping it");
194+
}
195+
}
196+
}
197+
198+
None
161199
}
162200

163201
impl<R: Runtime> DeepLink<R> {
202+
/// Checks if the provided list of arguments (which should match [`std::env::args`])
203+
/// contains a deep link argument (for Linux and Windows).
204+
///
205+
/// On Linux and Windows the deep links trigger a new app instance with the deep link URL as its only argument.
206+
///
207+
/// 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.
208+
/// To enhance its checks, we only match deep links against the schemes defined in the Tauri configuration
209+
/// i.e. dynamic schemes WON'T be processed.
210+
///
211+
/// This function updates the [`Self::get_current`] value and emits a `deep-link://new-url` event.
212+
#[cfg(desktop)]
213+
pub fn handle_cli_arguments<S: AsRef<str>, I: Iterator<Item = S>>(&self, args: I) {
214+
use tauri::Emitter;
215+
216+
let Some(config) = &self.config else {
217+
return;
218+
};
219+
220+
if let Some(url) = deep_link_from_args(config, args) {
221+
let mut current = self.current.lock().unwrap();
222+
current.replace(vec![url.clone()]);
223+
let _ = self.app.emit("deep-link://new-url", vec![url]);
224+
}
225+
}
226+
164227
/// 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.
165228
///
166229
/// ## Platform-specific:
167230
///
168-
/// - **Windows / Linux**: Unsupported, will return [`Error::UnsupportedPlatform`](`crate::Error::UnsupportedPlatform`).
231+
/// - **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.
232+
/// Note that you must manually check the arguments when registering deep link schemes dynamically with [`Self::register`].
233+
/// Additionally, the deep link might have been provided as a CLI argument so you should check if its format matches what you expect.
169234
pub fn get_current(&self) -> crate::Result<Option<Vec<url::Url>>> {
170-
#[cfg(not(any(windows, target_os = "linux")))]
171235
return Ok(self.current.lock().unwrap().clone());
172-
#[cfg(any(windows, target_os = "linux"))]
173-
Err(crate::Error::UnsupportedPlatform)
236+
}
237+
238+
/// Registers all schemes defined in the configuration file.
239+
///
240+
/// This is useful to ensure the schemes are registered even if the user did not install the app properly
241+
/// (e.g. an AppImage that was not properly registered with an AppImage launcher).
242+
pub fn register_all(&self) -> crate::Result<()> {
243+
let Some(config) = &self.config else {
244+
return Ok(());
245+
};
246+
247+
for scheme in config.desktop.schemes() {
248+
self.register(scheme)?;
249+
}
250+
251+
Ok(())
174252
}
175253

176254
/// Register the app as the default handler for the specified protocol.

plugins/single-instance/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ serde_json = { workspace = true }
1919
tauri = { workspace = true }
2020
log = { workspace = true }
2121
thiserror = { workspace = true }
22+
tauri-plugin-deep-link = { path = "../deep-link", version = "2.0.0-rc.3" }
2223
semver = { version = "1", optional = true }
2324

2425
[target."cfg(target_os = \"windows\")".dependencies.windows-sys]

plugins/single-instance/src/lib.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#![cfg(not(any(target_os = "android", target_os = "ios")))]
1414

1515
use tauri::{plugin::TauriPlugin, AppHandle, Manager, Runtime};
16+
use tauri_plugin_deep_link::DeepLink;
1617

1718
#[cfg(target_os = "windows")]
1819
#[path = "platform_impl/windows.rs"]
@@ -31,9 +32,14 @@ pub(crate) type SingleInstanceCallback<R> =
3132
dyn FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static;
3233

3334
pub fn init<R: Runtime, F: FnMut(&AppHandle<R>, Vec<String>, String) + Send + Sync + 'static>(
34-
f: F,
35+
mut f: F,
3536
) -> TauriPlugin<R> {
36-
platform_impl::init(Box::new(f))
37+
platform_impl::init(Box::new(move |app, args, cwd| {
38+
if let Some(deep_link) = app.try_state::<DeepLink<R>>() {
39+
deep_link.handle_cli_arguments(args.iter());
40+
}
41+
f(app, args, cwd)
42+
}))
3743
}
3844

3945
pub fn destroy<R: Runtime, M: Manager<R>>(manager: &M) {

0 commit comments

Comments
 (0)