tauri_plugin_shell/
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//! Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application.
6
7#![doc(
8    html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
9    html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
10)]
11
12use std::{
13    collections::HashMap,
14    ffi::OsStr,
15    path::Path,
16    sync::{Arc, Mutex},
17};
18
19use process::{Command, CommandChild};
20use regex::Regex;
21use tauri::{
22    plugin::{Builder, TauriPlugin},
23    AppHandle, Manager, RunEvent, Runtime,
24};
25
26mod commands;
27mod config;
28mod error;
29#[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
30#[allow(deprecated)]
31pub mod open;
32pub mod process;
33mod scope;
34mod scope_entry;
35
36pub use error::Error;
37type Result<T> = std::result::Result<T, Error>;
38
39#[cfg(mobile)]
40use tauri::plugin::PluginHandle;
41#[cfg(target_os = "android")]
42const PLUGIN_IDENTIFIER: &str = "app.tauri.shell";
43#[cfg(target_os = "ios")]
44tauri::ios_plugin_binding!(init_plugin_shell);
45
46type ChildStore = Arc<Mutex<HashMap<u32, CommandChild>>>;
47
48pub struct Shell<R: Runtime> {
49    #[allow(dead_code)]
50    app: AppHandle<R>,
51    #[cfg(mobile)]
52    mobile_plugin_handle: PluginHandle<R>,
53    open_scope: scope::OpenScope,
54    children: ChildStore,
55}
56
57impl<R: Runtime> Shell<R> {
58    /// Creates a new Command for launching the given program.
59    pub fn command(&self, program: impl AsRef<OsStr>) -> Command {
60        Command::new(program)
61    }
62
63    /// Creates a new Command for launching the given sidecar program.
64    ///
65    /// A sidecar program is a embedded external binary in order to make your application work
66    /// or to prevent users having to install additional dependencies (e.g. Node.js, Python, etc).
67    pub fn sidecar(&self, program: impl AsRef<Path>) -> Result<Command> {
68        Command::new_sidecar(program)
69    }
70
71    /// Open a (url) path with a default or specific browser opening program.
72    ///
73    /// See [`crate::open::open`] for how it handles security-related measures.
74    #[cfg(desktop)]
75    #[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
76    #[allow(deprecated)]
77    pub fn open(&self, path: impl Into<String>, with: Option<open::Program>) -> Result<()> {
78        open::open(&self.open_scope, path.into(), with).map_err(Into::into)
79    }
80
81    /// Open a (url) path with a default or specific browser opening program.
82    ///
83    /// See [`crate::open::open`] for how it handles security-related measures.
84    #[cfg(mobile)]
85    #[deprecated(since = "2.1.0", note = "Use tauri-plugin-opener instead.")]
86    pub fn open(&self, path: impl Into<String>, _with: Option<open::Program>) -> Result<()> {
87        self.mobile_plugin_handle
88            .run_mobile_plugin("open", path.into())
89            .map_err(Into::into)
90    }
91}
92
93pub trait ShellExt<R: Runtime> {
94    fn shell(&self) -> &Shell<R>;
95}
96
97impl<R: Runtime, T: Manager<R>> ShellExt<R> for T {
98    fn shell(&self) -> &Shell<R> {
99        self.state::<Shell<R>>().inner()
100    }
101}
102
103pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
104    Builder::<R, Option<config::Config>>::new("shell")
105        .js_init_script(include_str!("init-iife.js").to_string())
106        .invoke_handler(tauri::generate_handler![
107            commands::execute,
108            commands::spawn,
109            commands::stdin_write,
110            commands::kill,
111            commands::open
112        ])
113        .setup(|app, api| {
114            let default_config = config::Config::default();
115            let config = api.config().as_ref().unwrap_or(&default_config);
116
117            #[cfg(target_os = "android")]
118            let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "ShellPlugin")?;
119            #[cfg(target_os = "ios")]
120            let handle = api.register_ios_plugin(init_plugin_shell)?;
121
122            app.manage(Shell {
123                app: app.clone(),
124                children: Default::default(),
125                open_scope: open_scope(&config.open),
126
127                #[cfg(mobile)]
128                mobile_plugin_handle: handle,
129            });
130            Ok(())
131        })
132        .on_event(|app, event| {
133            if let RunEvent::Exit = event {
134                let shell = app.state::<Shell<R>>();
135                let children = {
136                    let mut lock = shell.children.lock().unwrap();
137                    std::mem::take(&mut *lock)
138                };
139                for child in children.into_values() {
140                    let _ = child.kill();
141                }
142            }
143        })
144        .build()
145}
146
147fn open_scope(open: &config::ShellAllowlistOpen) -> scope::OpenScope {
148    let shell_scope_open = match open {
149        config::ShellAllowlistOpen::Flag(false) => None,
150        config::ShellAllowlistOpen::Flag(true) => {
151            Some(Regex::new(r"^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+").unwrap())
152        }
153        config::ShellAllowlistOpen::Validate(validator) => {
154            let regex = format!("^{validator}$");
155            let validator =
156                Regex::new(&regex).unwrap_or_else(|e| panic!("invalid regex {regex}: {e}"));
157            Some(validator)
158        }
159    };
160
161    scope::OpenScope {
162        open: shell_scope_open,
163    }
164}