tauri_plugin_opener/lib.rs
1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Open files and URLs using their default application, and reveal files in the system's file explorer.
6//!
7//! Use the [`OpenerExt`] trait to access [`Opener::open_url`] and [`Opener::open_path`] from a
8//! running Tauri app; the plugin's `open_url` and `open_path` commands enforce the scope
9//! configured for the plugin before delegating to them. The [`open_url`] and [`open_path`] free
10//! functions and the [`Opener`] methods themselves do not perform any scope check.
11//! [`reveal_item_in_dir`] and [`reveal_items_in_dir`] return [`Error::UnsupportedPlatform`] on
12//! Android and iOS.
13
14use std::path::Path;
15
16use tauri::{Manager, Runtime, plugin::TauriPlugin};
17
18#[cfg(mobile)]
19use tauri::plugin::PluginHandle;
20#[cfg(target_os = "android")]
21const PLUGIN_IDENTIFIER: &str = "app.tauri.opener";
22#[cfg(target_os = "ios")]
23tauri::ios_plugin_binding!(init_plugin_opener);
24
25mod commands;
26mod config;
27mod error;
28mod open;
29mod reveal_item_in_dir;
30mod scope;
31mod scope_entry;
32#[cfg(windows)]
33mod windows_shell_path;
34
35pub use error::Error;
36type Result<T> = std::result::Result<T, Error>;
37
38pub use open::{open_path, open_url};
39pub use reveal_item_in_dir::{reveal_item_in_dir, reveal_items_in_dir};
40
41/// Access to the opener APIs, managed by the plugin as app state.
42///
43/// Obtain an instance via [`OpenerExt::opener`].
44pub struct Opener<R: Runtime> {
45 // we use `fn() -> R` to silence the unused generic error
46 // while keeping this struct `Send + Sync` without requiring `R` to be
47 #[cfg(not(mobile))]
48 _marker: std::marker::PhantomData<fn() -> R>,
49 #[cfg(mobile)]
50 mobile_plugin_handle: PluginHandle<R>,
51 require_literal_leading_dot: Option<bool>,
52}
53
54impl<R: Runtime> Opener<R> {
55 /// Open a url with a default or specific program.
56 ///
57 /// # Examples
58 ///
59 /// ```rust,no_run
60 /// use tauri_plugin_opener::OpenerExt;
61 ///
62 /// tauri::Builder::default()
63 /// .setup(|app| {
64 /// // open the given URL on the system default browser
65 /// app.opener().open_url("https://github.com/tauri-apps/tauri", None::<&str>)?;
66 /// Ok(())
67 /// });
68 /// ```
69 ///
70 /// ## Platform-specific:
71 ///
72 /// - **Android / iOS**: Always opens using default program, unless `with` is provided as "inAppBrowser".
73 #[cfg(desktop)]
74 pub fn open_url(&self, url: impl Into<String>, with: Option<impl Into<String>>) -> Result<()> {
75 crate::open::open(
76 url.into(),
77 with.map(Into::into).filter(|with| with != "inAppBrowser"),
78 )
79 }
80
81 /// Open a url with a default or specific program.
82 ///
83 /// # Examples
84 ///
85 /// ```rust,no_run
86 /// use tauri_plugin_opener::OpenerExt;
87 ///
88 /// tauri::Builder::default()
89 /// .setup(|app| {
90 /// // open the given URL on the system default browser
91 /// app.opener().open_url("https://github.com/tauri-apps/tauri", None::<&str>)?;
92 /// Ok(())
93 /// });
94 /// ```
95 ///
96 /// ## Platform-specific:
97 ///
98 /// - **Android / iOS**: Always opens using default program, unless `with` is provided as "inAppBrowser".
99 #[cfg(mobile)]
100 pub fn open_url(&self, url: impl Into<String>, with: Option<impl Into<String>>) -> Result<()> {
101 self.mobile_plugin_handle
102 .run_mobile_plugin(
103 "open",
104 serde_json::json!({ "url": url.into(), "with": with.map(Into::into) }),
105 )
106 .map_err(Into::into)
107 }
108
109 /// Open a path with a default or specific program.
110 ///
111 /// # Examples
112 ///
113 /// ```rust,no_run
114 /// use tauri_plugin_opener::OpenerExt;
115 ///
116 /// tauri::Builder::default()
117 /// .setup(|app| {
118 /// // open the given path on the system default explorer
119 /// app.opener().open_path("/path/to/file", None::<&str>)?;
120 /// Ok(())
121 /// });
122 /// ```
123 ///
124 /// ## Platform-specific:
125 ///
126 /// - **Android / iOS**: Always opens using default program.
127 #[cfg(desktop)]
128 pub fn open_path(
129 &self,
130 path: impl Into<String>,
131 with: Option<impl Into<String>>,
132 ) -> Result<()> {
133 crate::open::open(
134 path.into(),
135 with.map(Into::into).filter(|with| with != "inAppBrowser"),
136 )
137 }
138
139 /// Open a path with a default or specific program.
140 ///
141 /// # Examples
142 ///
143 /// ```rust,no_run
144 /// use tauri_plugin_opener::OpenerExt;
145 ///
146 /// tauri::Builder::default()
147 /// .setup(|app| {
148 /// // open the given path on the system default explorer
149 /// app.opener().open_path("/path/to/file", None::<&str>)?;
150 /// Ok(())
151 /// });
152 /// ```
153 ///
154 /// ## Platform-specific:
155 ///
156 /// - **Android / iOS**: Always opens using default program.
157 #[cfg(mobile)]
158 pub fn open_path(
159 &self,
160 path: impl Into<String>,
161 _with: Option<impl Into<String>>,
162 ) -> Result<()> {
163 self.mobile_plugin_handle
164 .run_mobile_plugin("open", path.into())
165 .map_err(Into::into)
166 }
167
168 /// Reveal a path in the system's default explorer. See [`reveal_item_in_dir`] for details.
169 ///
170 /// ## Platform-specific:
171 ///
172 /// - **Android / iOS:** Unsupported, returns [`Error::UnsupportedPlatform`].
173 pub fn reveal_item_in_dir<P: AsRef<Path>>(&self, p: P) -> Result<()> {
174 reveal_item_in_dir(p)
175 }
176
177 /// Reveal multiple paths in the system's default explorer. See [`reveal_items_in_dir`] for details.
178 ///
179 /// ## Platform-specific:
180 ///
181 /// - **Android / iOS:** Unsupported, returns [`Error::UnsupportedPlatform`].
182 pub fn reveal_items_in_dir<I, P>(&self, paths: I) -> Result<()>
183 where
184 I: IntoIterator<Item = P>,
185 P: AsRef<Path>,
186 {
187 reveal_items_in_dir(paths)
188 }
189}
190
191/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the opener APIs.
192pub trait OpenerExt<R: Runtime> {
193 /// Returns the [`Opener`] instance managed by the plugin.
194 fn opener(&self) -> &Opener<R>;
195}
196
197impl<R: Runtime, T: Manager<R>> OpenerExt<R> for T {
198 fn opener(&self) -> &Opener<R> {
199 self.state::<Opener<R>>().inner()
200 }
201}
202
203/// The opener plugin Builder.
204pub struct Builder {
205 open_js_links_on_click: bool,
206}
207
208impl Default for Builder {
209 fn default() -> Self {
210 Self {
211 open_js_links_on_click: true,
212 }
213 }
214}
215
216impl Builder {
217 /// Create a new opener plugin Builder.
218 pub fn new() -> Self {
219 Self::default()
220 }
221
222 /// Whether the plugin should inject a JS script to open URLs in default browser
223 /// when clicking on `<a>` elements that has `_blank` target, or when pressing `Ctrl` or `Shift` while clicking it.
224 ///
225 /// Enabled by default for `http:`, `https:`, `mailto:`, `tel:` links.
226 pub fn open_js_links_on_click(mut self, open: bool) -> Self {
227 self.open_js_links_on_click = open;
228 self
229 }
230
231 /// Build and Initializes the plugin.
232 pub fn build<R: Runtime>(self) -> TauriPlugin<R, Option<config::Config>> {
233 let mut builder = tauri::plugin::Builder::<R, Option<config::Config>>::new("opener")
234 .setup(|app, api| {
235 #[cfg(target_os = "android")]
236 let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "OpenerPlugin")?;
237 #[cfg(target_os = "ios")]
238 let handle = api.register_ios_plugin(init_plugin_opener)?;
239
240 app.manage(Opener {
241 #[cfg(not(mobile))]
242 _marker: std::marker::PhantomData::<fn() -> R>,
243 #[cfg(mobile)]
244 mobile_plugin_handle: handle,
245 require_literal_leading_dot: api
246 .config()
247 .as_ref()
248 .and_then(|c| c.require_literal_leading_dot),
249 });
250 Ok(())
251 })
252 .invoke_handler(tauri::generate_handler![
253 commands::open_url,
254 commands::open_path,
255 commands::reveal_item_in_dir,
256 ]);
257
258 if self.open_js_links_on_click {
259 builder = builder.js_init_script(include_str!("init-iife.js").to_string());
260 }
261
262 builder.build()
263 }
264}
265
266/// Initializes the plugin.
267pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
268 Builder::default().build()
269}