Skip to main content

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}