tauri_plugin_shell/
lib.rs#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use std::{
collections::HashMap,
ffi::OsStr,
path::Path,
sync::{Arc, Mutex},
};
use process::{Command, CommandChild};
use regex::Regex;
use tauri::{
plugin::{Builder, TauriPlugin},
AppHandle, Manager, RunEvent, Runtime,
};
mod commands;
mod config;
mod error;
pub mod open;
pub mod process;
mod scope;
mod scope_entry;
pub use error::Error;
type Result<T> = std::result::Result<T, Error>;
#[cfg(mobile)]
use tauri::plugin::PluginHandle;
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "app.tauri.shell";
#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_shell);
type ChildStore = Arc<Mutex<HashMap<u32, CommandChild>>>;
pub struct Shell<R: Runtime> {
#[allow(dead_code)]
app: AppHandle<R>,
#[cfg(mobile)]
mobile_plugin_handle: PluginHandle<R>,
open_scope: scope::OpenScope,
children: ChildStore,
}
impl<R: Runtime> Shell<R> {
pub fn command(&self, program: impl AsRef<OsStr>) -> Command {
Command::new(program)
}
pub fn sidecar(&self, program: impl AsRef<Path>) -> Result<Command> {
Command::new_sidecar(program)
}
#[cfg(desktop)]
pub fn open(&self, path: impl Into<String>, with: Option<open::Program>) -> Result<()> {
open::open(&self.open_scope, path.into(), with).map_err(Into::into)
}
#[cfg(mobile)]
pub fn open(&self, path: impl Into<String>, _with: Option<open::Program>) -> Result<()> {
self.mobile_plugin_handle
.run_mobile_plugin("open", path.into())
.map_err(Into::into)
}
}
pub trait ShellExt<R: Runtime> {
fn shell(&self) -> &Shell<R>;
}
impl<R: Runtime, T: Manager<R>> ShellExt<R> for T {
fn shell(&self) -> &Shell<R> {
self.state::<Shell<R>>().inner()
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
Builder::<R, Option<config::Config>>::new("shell")
.js_init_script(include_str!("init-iife.js").to_string())
.invoke_handler(tauri::generate_handler![
commands::execute,
commands::spawn,
commands::stdin_write,
commands::kill,
commands::open
])
.setup(|app, api| {
let default_config = config::Config::default();
let config = api.config().as_ref().unwrap_or(&default_config);
#[cfg(target_os = "android")]
let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "ShellPlugin")?;
#[cfg(target_os = "ios")]
let handle = api.register_ios_plugin(init_plugin_shell)?;
app.manage(Shell {
app: app.clone(),
children: Default::default(),
open_scope: open_scope(&config.open),
#[cfg(mobile)]
mobile_plugin_handle: handle,
});
Ok(())
})
.on_event(|app, event| {
if let RunEvent::Exit = event {
let shell = app.state::<Shell<R>>();
let children = {
let mut lock = shell.children.lock().unwrap();
std::mem::take(&mut *lock)
};
for child in children.into_values() {
let _ = child.kill();
}
}
})
.build()
}
fn open_scope(open: &config::ShellAllowlistOpen) -> scope::OpenScope {
let shell_scope_open = match open {
config::ShellAllowlistOpen::Flag(false) => None,
config::ShellAllowlistOpen::Flag(true) => {
Some(Regex::new(r"^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+").unwrap())
}
config::ShellAllowlistOpen::Validate(validator) => {
let regex = format!("^{validator}$");
let validator =
Regex::new(®ex).unwrap_or_else(|e| panic!("invalid regex {regex}: {e}"));
Some(validator)
}
};
scope::OpenScope {
open: shell_scope_open,
}
}