pub mod autostart;
pub mod b64;
pub mod broker;
pub mod clipboard;
pub mod commands;
pub mod deep_link;
pub mod devtools;
#[cfg(feature = "engine")]
pub mod engine;
pub mod error;
pub mod frameless;
pub mod harden;
pub mod idle;
pub mod instance;
mod legacy_hold;
mod legacy_instance;
pub mod lifecycle;
pub mod login_item;
pub mod menu;
pub mod modules;
pub mod nav;
pub mod notify;
pub mod open_url;
pub mod opener;
pub mod presence;
pub mod quit;
pub mod recovery;
pub mod single_instance;
pub mod site;
pub mod startup_cache;
pub mod stream;
pub mod tray;
pub mod watchdog;
pub mod window_state;
#[cfg(all(feature = "qa-probes", not(debug_assertions)))]
compile_error!("rightkit-tauri-shell: the `qa-probes` feature is for development and QA builds only; a build without debug assertions (release) must not enable it");
#[cfg(windows)]
pub(crate) mod winapi {
#[cfg(not(any(feature = "wv2-0_38", feature = "wv2-0_39")))]
compile_error!("rightkit-tauri-shell on Windows needs `wv2-0_38` (Tauri 2.11, in default features) or `wv2-0_39` (Tauri 2.12); with default-features = false add one, e.g. features = [\"wv2-0_38\"]");
#[cfg(all(feature = "wv2-0_38", not(feature = "wv2-0_39")))]
pub use ::{webview2_com_038 as webview2_com, windows_061 as windows};
#[cfg(feature = "wv2-0_39")]
pub use ::{webview2_com_039 as webview2_com, windows_062 as windows};
}
pub mod windows;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
use tauri::menu::MenuItem;
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
use tauri::{AppHandle, Manager, Wry};
pub use autostart::{AutostartMigration, AutostartOptions, LegacyAutostartNames, MacosBackend};
pub use broker::PathBroker;
pub use clipboard::{ClipboardItem, ClipboardOptions, ClipboardPolicy, ImagePreference};
pub use devtools::{ContentWebview, DevToolsError, DevToolsGate, DevToolsPolicy, WebviewDecision};
pub use error::ShellError;
pub use harden::{Hardening, HardeningScript};
pub use idle::{
IdleErrorCallback, IdleFailure, IdlePhase, IdleTransition, IdleTransitionCallback,
IdleTransitionKind, IdleTrigger,
};
pub use login_item::LoginItemStatus;
pub use menu::{MenuEntry, MenuGroup, MenuSpec};
pub use modules::{Module, ModuleSet, ShellPlugin, ShellPluginBuilder};
pub use nav::{NavDecision, NavPolicy};
pub use opener::{ExternalOpener, SystemOpener};
pub use presence::{AutoPrefs, PresenceState};
pub use recovery::{MigrationOutcome, RecoverySpec};
pub use single_instance::{
ClaimFailure, ClaimOptions, ClaimOutcome, InstanceMessage, LegacySingleInstanceNames,
SingleInstance, SingleInstanceGuard,
};
pub use site::SitePolicy;
pub use stream::{StreamEvent, StreamEventKind, StreamObserver, StreamPolicy};
pub use tray::{LeftClick, TraySpec};
pub use watchdog::{ShellRuntimeStatus, StallReport, WatchdogConfig, WatchdogStatus};
pub const EVENT_MENU: &str = "rightkit-shell://menu";
pub const EVENT_TRAY: &str = "rightkit-shell://tray";
pub const EVENT_DEEP_LINK: &str = "rightkit-shell://deep-link";
pub const EVENT_SECOND_INSTANCE: &str = "rightkit-shell://second-instance";
pub const EVENT_STALL: &str = "rightkit-shell://stall";
pub const EVENT_ENGINE: &str = "rightkit-shell://engine";
pub const EVENT_WINDOW: &str = "rightkit-shell://window";
pub const EVENT_IDLE: &str = "rightkit-shell://idle";
pub const EVENT_PRESENCE: &str = "rightkit-shell://presence";
pub const EVENT_QUIT_REQUESTED: &str = "rightkit-shell://quit-requested";
type MenuCallback = Arc<dyn Fn(&AppHandle<Wry>, &str) + Send + Sync>;
type PresenceCallback = Arc<dyn Fn(&AppHandle<Wry>, PresenceState) + Send + Sync>;
type VisibilityCallback = Arc<dyn Fn(&AppHandle<Wry>, lifecycle::Visibility) + Send + Sync>;
type RevealDecider = lifecycle::RevealDecider<AppHandle<Wry>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CloseBehavior {
#[default]
Quit,
HideToTray,
HideToBackground,
}
pub struct ShellState {
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) main_thread_id: std::thread::ThreadId,
pub app_id: String,
pub app_name: String,
pub main_label: String,
pub nav: Arc<NavPolicy>,
pub opener: Arc<dyn ExternalOpener>,
pub frameless: bool,
pub close_to_tray: bool,
pub hide_to_background: bool,
pub on_visibility: Option<VisibilityCallback>,
pub(crate) activation: lifecycle::ActivationTracker,
pub show_policy: lifecycle::ShowPolicy,
pub quit: Arc<quit::QuitGate>,
pub windows: Arc<window_state::WindowStateStore>,
pub deep: deep_link::DeepLinkQueue,
pub schemes: Vec<String>,
pub frontend_ready: AtomicBool,
pub exiting: AtomicBool,
pub traffic: frameless::TrafficLights,
pub autostart: autostart::AutostartSpec,
pub menu_items: Mutex<HashMap<String, MenuItem<Wry>>>,
pub menu_rows: Mutex<Vec<(String, String)>>,
pub tray: Option<tray::TrayRuntime>,
pub on_menu: Option<MenuCallback>,
#[cfg(feature = "engine")]
pub engine: Option<Arc<engine::EngineHost>>,
pub hosted: Mutex<Option<rightkit_daemon::Hosted>>,
pub single_instance: Mutex<String>,
pub broker: Arc<PathBroker>,
pub notifications: notify::NotificationState,
pub presence: Arc<presence::PresenceDnd>,
pub idle: Option<idle::IdleHost>,
pub recovery: Option<recovery::RecoveryStore>,
pub cache_repair: Mutex<Option<(PathBuf, recovery::CacheDirOutcome)>>,
pub stream_scheme: Option<String>,
pub stream_schemes: Vec<String>,
pub site_scheme: Option<String>,
pub site_policy: site::SitePolicy,
pub modules: ModuleSet,
pub clipboard: clipboard::ClipboardPolicy,
pub clipboard_options: clipboard::ClipboardOptions,
pub clipboard_dir: PathBuf,
}
pub type SecondInstanceCallback = Arc<dyn Fn(&AppHandle<Wry>, &[String], &str) + Send + Sync>;
pub struct ShellBuilder {
app_id: String,
app_name: String,
main_label: String,
hardening: Hardening,
hardening_script: HardeningScript,
devtools_policy: DevToolsPolicy,
content_webviews: Vec<ContentWebview>,
nav: NavPolicy,
extra_internal_schemes: Vec<String>,
opener: Arc<dyn ExternalOpener>,
single_instance: bool,
close_to_tray: bool,
hide_to_background: bool,
on_visibility: Option<VisibilityCallback>,
never_show: bool,
never_show_env: Vec<String>,
never_show_env_values: Vec<(String, String)>,
reveal_on_reopen: bool,
lifecycle_only: bool,
qa_instance_exempt: bool,
on_second_instance_reveal: Option<RevealDecider>,
quit_drain: Option<std::time::Duration>,
frameless: bool,
show_on_ready: Option<bool>,
state_dir: Option<PathBuf>,
schemes: Vec<String>,
menu: Option<MenuSpec>,
tray: Option<TraySpec>,
watchdog: Option<WatchdogConfig>,
on_menu: Option<MenuCallback>,
on_second_instance: Option<SecondInstanceCallback>,
streams: Vec<(String, StreamPolicy)>,
site: Option<(String, site::SitePolicy)>,
modules: ModuleSet,
clipboard: clipboard::ClipboardPolicy,
clipboard_options: clipboard::ClipboardOptions,
idle_on_error: Option<idle::IdleErrorCallback>,
idle_on_transition: Option<idle::IdleTransitionCallback>,
autostart: autostart::AutostartOptions,
recovery: Option<RecoverySpec>,
idle_after: Option<std::time::Duration>,
idle_options: Option<idle::IdleOptions>,
cache_dir: Option<PathBuf>,
repair_cache: Option<bool>,
on_presence: Option<PresenceCallback>,
#[cfg(feature = "engine")]
engine: Option<engine::EngineSpec>,
#[cfg(feature = "engine")]
engine_policy: engine::EnginePolicy,
}
pub struct Shell {
cfg: ShellBuilder,
}
impl Shell {
pub fn builder(app_id: &str) -> ShellBuilder {
ShellBuilder {
app_id: app_id.to_owned(),
app_name: app_id.to_owned(),
main_label: "main".into(),
hardening: Hardening::default(),
hardening_script: HardeningScript::default(),
devtools_policy: DevToolsPolicy::default(),
content_webviews: Vec::new(),
nav: NavPolicy::default(),
extra_internal_schemes: Vec::new(),
opener: Arc::new(SystemOpener),
single_instance: true,
close_to_tray: false,
hide_to_background: false,
on_visibility: None,
never_show: false,
never_show_env: Vec::new(),
never_show_env_values: Vec::new(),
reveal_on_reopen: true,
lifecycle_only: false,
qa_instance_exempt: false,
on_second_instance_reveal: None,
quit_drain: None,
frameless: false,
show_on_ready: None,
state_dir: None,
schemes: Vec::new(),
menu: None,
tray: None,
watchdog: Some(WatchdogConfig::default()),
on_menu: None,
on_second_instance: None,
streams: Vec::new(),
site: None,
modules: ModuleSet::all(),
clipboard: clipboard::ClipboardPolicy::default(),
clipboard_options: clipboard::ClipboardOptions::default(),
idle_on_error: None,
idle_on_transition: None,
autostart: autostart::AutostartOptions::default(),
recovery: None,
idle_after: None,
idle_options: None,
cache_dir: None,
repair_cache: None,
on_presence: None,
#[cfg(feature = "engine")]
engine: None,
#[cfg(feature = "engine")]
engine_policy: Default::default(),
}
}
}
impl ShellBuilder {
pub fn app_name(mut self, name: &str) -> Self {
self.app_name = name.to_owned();
self
}
pub fn main_label(mut self, label: &str) -> Self {
self.main_label = label.to_owned();
self
}
pub fn hardening(mut self, h: Hardening) -> Self {
self.hardening = h;
self
}
pub fn hardening_script(mut self, scope: HardeningScript) -> Self {
self.hardening_script = scope;
self
}
pub fn devtools_policy(mut self, p: DevToolsPolicy) -> Self {
self.devtools_policy = p;
self
}
pub fn content_webview(mut self, c: ContentWebview) -> Self {
self.content_webviews.push(c);
self
}
pub fn nav_policy(mut self, p: NavPolicy) -> Self {
self.nav = p;
self
}
pub fn internal_scheme(mut self, scheme: &str) -> Self {
nav::validate_internal_scheme(scheme).expect("rightkit-tauri-shell: internal_scheme");
self.extra_internal_schemes.push(scheme.to_owned());
self
}
pub fn opener(mut self, o: impl ExternalOpener) -> Self {
self.opener = Arc::new(o);
self
}
pub fn single_instance(mut self, on: bool) -> Self {
self.single_instance = on;
self
}
pub fn close_to_tray(mut self, on: bool) -> Self {
self.close_to_tray = on;
if !on {
self.hide_to_background = false;
}
self
}
pub fn close_behavior(mut self, behavior: CloseBehavior) -> Self {
self.close_to_tray = behavior != CloseBehavior::Quit;
self.hide_to_background = behavior == CloseBehavior::HideToBackground;
self
}
pub fn on_visibility(
mut self,
f: impl Fn(&AppHandle<Wry>, lifecycle::Visibility) + Send + Sync + 'static,
) -> Self {
self.on_visibility = Some(Arc::new(f));
self
}
pub fn never_show(mut self, on: bool) -> Self {
self.never_show = on;
self
}
pub fn never_show_env(mut self, name: &str) -> Self {
assert!(
!name.is_empty() && !name.contains(['=', '\0']),
"rightkit-tauri-shell: never_show_env needs a plain environment variable name"
);
if !self.never_show_env.iter().any(|n| n == name) {
self.never_show_env.push(name.to_owned());
}
self
}
pub fn never_show_env_value(mut self, name: &str, expected: &str) -> Self {
assert!(
!name.is_empty() && !name.contains(['=', '\0']) && !expected.is_empty(),
"rightkit-tauri-shell: never_show_env_value needs a plain variable name and a non-empty expected value"
);
let pair = (name.to_owned(), expected.to_owned());
if !self.never_show_env_values.contains(&pair) {
self.never_show_env_values.push(pair);
}
self
}
pub fn reveal_on_reopen(mut self, on: bool) -> Self {
self.reveal_on_reopen = on;
self
}
pub fn qa_instance_exempt(mut self, on: bool) -> Self {
self.qa_instance_exempt = on;
self
}
pub fn second_instance_reveal(
mut self,
f: impl Fn(&AppHandle<Wry>, &[String], &str) -> bool + Send + Sync + 'static,
) -> Self {
self.on_second_instance_reveal = Some(Arc::new(f));
self
}
pub fn quit_drain(mut self, on: bool) -> Self {
self.quit_drain = on.then_some(quit::DEFAULT_QUIT_DRAIN_BOUND);
self
}
pub fn quit_drain_bound(mut self, bound: std::time::Duration) -> Self {
self.quit_drain = Some(bound);
self
}
pub fn frameless(mut self, on: bool) -> Self {
self.frameless = on;
self
}
pub fn show_on_ready(mut self, on: bool) -> Self {
self.show_on_ready = Some(on);
self
}
pub fn state_dir(mut self, dir: PathBuf) -> Self {
self.state_dir = Some(dir);
self
}
pub fn deep_link_scheme(mut self, scheme: &str) -> Self {
self.schemes.push(scheme.to_ascii_lowercase());
self
}
pub fn menu(mut self, m: MenuSpec) -> Self {
self.menu = Some(m);
self
}
pub fn tray(mut self, t: TraySpec) -> Self {
self.tray = Some(t);
self
}
pub fn autostart_options(mut self, options: autostart::AutostartOptions) -> Self {
self.autostart = options;
self
}
pub fn on_stall(mut self, f: impl Fn(&watchdog::StallReport) + Send + Sync + 'static) -> Self {
if let Some(config) = self.watchdog.as_mut() {
config.on_stall = Some(Arc::new(f));
}
self
}
pub fn watchdog(mut self, w: Option<WatchdogConfig>) -> Self {
self.watchdog = w;
self
}
pub fn on_second_instance(
mut self,
f: impl Fn(&AppHandle<Wry>, &[String], &str) + Send + Sync + 'static,
) -> Self {
self.on_second_instance = Some(Arc::new(f));
self
}
pub fn on_menu(mut self, f: impl Fn(&AppHandle<Wry>, &str) + Send + Sync + 'static) -> Self {
self.on_menu = Some(Arc::new(f));
self
}
pub fn stream_protocol(mut self, scheme: &str, policy: StreamPolicy) -> Self {
let scheme = scheme.to_ascii_lowercase();
self.streams.retain(|(s, _)| *s != scheme);
self.streams.push((scheme, policy));
self
}
pub fn site_protocol(mut self, scheme: &str, policy: site::SitePolicy) -> Self {
self.site = Some((scheme.to_ascii_lowercase(), policy));
self
}
pub fn clipboard_purge_after(mut self, age: std::time::Duration) -> Self {
self.clipboard.purge_after = age;
self
}
pub fn clipboard_options(mut self, options: clipboard::ClipboardOptions) -> Self {
self.clipboard_options = options;
self
}
pub fn recovery(mut self, spec: RecoverySpec) -> Self {
self.recovery = Some(spec);
self
}
pub fn webview_idle(mut self, suspend_after: std::time::Duration) -> Self {
self.idle_after = Some(suspend_after);
self
}
pub fn webview_idle_options(mut self, options: idle::IdleOptions) -> Self {
self.idle_after = Some(options.suspend_after);
self.idle_options = Some(options);
self
}
pub fn webview_idle_on_error(
mut self,
f: impl Fn(&idle::IdleFailure) + Send + Sync + 'static,
) -> Self {
self.idle_on_error = Some(Arc::new(f));
self
}
pub fn webview_idle_on_transition(
mut self,
f: impl Fn(&idle::IdleTransition) + Send + Sync + 'static,
) -> Self {
self.idle_on_transition = Some(Arc::new(f));
self
}
pub fn cache_dir(mut self, dir: PathBuf) -> Self {
self.cache_dir = Some(dir);
self
}
pub fn repair_cache(mut self, on: bool) -> Self {
self.repair_cache = Some(on);
self
}
pub fn on_presence(
mut self,
f: impl Fn(&AppHandle<Wry>, PresenceState) + Send + Sync + 'static,
) -> Self {
self.on_presence = Some(Arc::new(f));
self
}
#[cfg(feature = "engine")]
pub fn engine_with_policy(
mut self,
spec: engine::EngineSpec,
policy: engine::EnginePolicy,
) -> Self {
self.engine = Some(spec);
self.engine_policy = policy;
self
}
pub fn build(self) -> Shell {
assert!(
!self.app_id.is_empty()
&& self.app_id.len() <= 40
&& self
.app_id
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
"rightkit-tauri-shell: app_id must be lowercase letters, digits and '-' (<= 40 chars)"
);
Shell { cfg: self }
}
}
impl Shell {
pub fn single_instance_plugin(
self,
on_second_instance: impl Fn(&AppHandle<Wry>, &[String], &str) + Send + Sync + 'static,
) -> TauriPlugin<Wry> {
let app_id = self.cfg.app_id;
PluginBuilder::<Wry>::new("rightkit-single-instance")
.setup(move |app, _api| {
let handle = app.clone();
let version = app.package_info().version.to_string();
let identifier = app.config().identifier.clone();
let arbitration = instance::arbitrate_with_legacy(
&app_id,
&version,
Some(identifier.as_str()),
Arc::new(move |second| {
on_second_instance(&handle, &second.argv, &second.cwd);
}),
);
match arbitration {
instance::Arbitration::Primary(hosted) => {
app.manage(PluginClaim(single_instance::hold(hosted)));
}
instance::Arbitration::Secondary => std::process::exit(0),
instance::Arbitration::Unavailable(why) => {
return Err(unavailable_error(&why));
}
instance::Arbitration::InvalidAppId(e) => return Err(invalid_app_id_error(&e)),
}
Ok(())
})
.on_event(|app, event| {
if matches!(event, tauri::RunEvent::Exit) {
if let Some(claim) = app.try_state::<PluginClaim>() {
single_instance::release(claim.0);
}
}
})
.build()
}
pub fn lifecycle_plugin(self) -> TauriPlugin<Wry> {
ShellPlugin::builder().shell(self).lifecycle_only()
}
pub fn plugin(self) -> TauriPlugin<Wry> {
ShellPlugin::builder().shell(self).build()
}
}
pub(crate) fn install(cfg: ShellBuilder) -> TauriPlugin<Wry> {
let modules = cfg.modules;
let mut nav_policy = cfg.nav.clone();
for scheme in &cfg.extra_internal_schemes {
nav_policy = nav_policy
.with_internal_scheme(scheme)
.expect("rightkit-tauri-shell: internal_scheme");
}
let nav = Arc::new(nav_policy);
let opener = Arc::clone(&cfg.opener);
let hardening = cfg.hardening.clone();
let script_scope = cfg.hardening_script;
let (nav_hook, opener_hook) = (Arc::clone(&nav), Arc::clone(&opener));
let hardening_for_views = hardening.clone();
let guard_result = devtools::guard_process_start();
let gate_result = devtools::build_gate(
cfg.devtools_policy,
&cfg.main_label,
&cfg.content_webviews,
cfg!(feature = "content-devtools"),
);
let gate_for_views = gate_result
.clone()
.unwrap_or_else(|_| DevToolsGate::denied(&cfg.main_label));
let content_labels = gate_for_views.content_labels();
let harden_webviews = modules.contains(Module::JsHardening);
let show_on_ready_explicit = cfg.show_on_ready;
let show_on_ready_module = lifecycle::show_on_ready_installed(
modules.contains(Module::ShowOnReady),
cfg.lifecycle_only,
show_on_ready_explicit,
);
let ready_policy = show_policy_of(&cfg);
let restore_windows = modules.contains(Module::WindowState);
let main_label = cfg.main_label.clone();
let (streams, site) = if modules.contains(Module::Stream) {
(cfg.streams.clone(), cfg.site.clone())
} else {
(Vec::new(), None)
};
let mut plugin = PluginBuilder::<Wry>::new("rightkit-shell");
plugin = plugin.on_page_load(|webview, payload| {
if matches!(payload.event(), tauri::webview::PageLoadEvent::Finished) {
if let Err(e) = devtools::verify_after_first_navigation(webview.app_handle()) {
eprintln!("{e}");
webview.app_handle().exit(1);
}
}
});
if modules.contains(Module::NavigationPolicy) {
plugin = plugin.on_navigation(move |_webview, url| match nav_hook.decide(url) {
NavDecision::Allow => true,
NavDecision::External { url } => {
let _ = opener::open_checked(opener_hook.as_ref(), &url);
false
}
NavDecision::Block => false,
});
}
plugin = plugin
.on_webview_ready(move |webview| {
let decision = gate_for_views.decide(webview.label());
if devtools::apply_native(&webview, decision).is_err() {
return;
}
if harden_webviews {
harden::apply_native(&webview, &hardening_for_views, decision);
}
if let Err(e) = devtools::verify_not_remote_debuggable(webview.app_handle()) {
eprintln!("{e}");
webview.app_handle().exit(1);
return;
}
let app = webview.app_handle().clone();
let label = webview.label().to_owned();
lifecycle::enforce_never_show(&app, &ready_policy);
windows::with_placement_announced(&app, &label, || {
if restore_windows {
if let Some(state) = app.try_state::<ShellState>() {
let _ = state.windows.restore(&app, &label);
}
}
});
if label == main_label
&& lifecycle::show_on_ready_decision(
show_on_ready_explicit,
show_on_ready_module,
lifecycle::launched_in_background(),
lifecycle::qa_hidden(),
)
{
if let Some(w) = app.get_webview_window(&label) {
let _ = crate::windows::show_window(
&app,
&w,
lifecycle::RevealSource::ShowOnReady,
windows::RevealStyle::ShowOnly,
);
}
}
})
.on_event(|app, event| {
if app.try_state::<ShellState>().is_some() {
lifecycle::run_event(app, event);
}
})
.setup(move |app, _api| {
let app = app.clone();
if let Err(e) = guard_result {
eprintln!("{e}");
return Err(Box::new(e));
}
match gate_result {
Ok(gate) => {
app.manage(gate);
}
Err(e) => {
eprintln!("{e}");
return Err(Box::new(e));
}
}
setup(&app, cfg, nav, opener)
});
if harden_webviews {
plugin = match script_scope {
HardeningScript::AllFrames => {
plugin.js_init_script_on_all_frames(hardening.script_for(&content_labels))
}
HardeningScript::TopFrameOnly => plugin.js_init_script(format!(
"if (window.top !== window.self) {{ return; }}\n{}",
hardening.script_for(&content_labels)
)),
HardeningScript::Off => plugin,
};
}
for (scheme, policy) in streams {
plugin = plugin.register_asynchronous_uri_scheme_protocol(
scheme,
move |ctx, request, responder| {
let state = ctx.app_handle().state::<ShellState>();
stream::serve(&state.broker, &policy, &request, move |response| {
responder.respond(response)
});
},
);
}
if let Some((scheme, policy)) = site {
plugin = plugin.register_asynchronous_uri_scheme_protocol(
scheme,
move |ctx, request, responder| {
let state = ctx.app_handle().state::<ShellState>();
site::serve_site_request(&state.broker, &policy, &request, move |response| {
responder.respond(response)
});
},
);
}
plugin.build()
}
struct PluginClaim(u64);
fn unavailable_error(why: &str) -> Box<dyn std::error::Error> {
format!("rightkit-tauri-shell: single-instance could not be decided, refusing to start as a second primary: {why}").into()
}
fn invalid_app_id_error(e: &instance::InvalidAppId) -> Box<dyn std::error::Error> {
format!("rightkit-tauri-shell: single-instance cannot start, app id is not usable: {e}").into()
}
fn show_policy_of(cfg: &ShellBuilder) -> lifecycle::ShowPolicy {
lifecycle::ShowPolicy {
never_show: cfg.never_show,
never_show_env: cfg.never_show_env.clone(),
never_show_env_values: cfg.never_show_env_values.clone(),
reveal_on_reopen: cfg.reveal_on_reopen && cfg.modules.contains(Module::RevealOnReopen),
}
}
fn setup(
app: &AppHandle<Wry>,
cfg: ShellBuilder,
nav: Arc<NavPolicy>,
opener: Arc<dyn ExternalOpener>,
) -> Result<(), Box<dyn std::error::Error>> {
let show_policy = show_policy_of(&cfg);
lifecycle::enforce_never_show(app, &show_policy);
let mut single = String::from("disabled");
let mode = lifecycle::instance_mode(
cfg.single_instance,
cfg.qa_instance_exempt,
lifecycle::qa_hidden(),
);
if mode == lifecycle::InstanceMode::QaExempt {
single = "qa-exempt".into();
}
if mode == lifecycle::InstanceMode::Arbitrate {
let for_cb = app.clone();
let custom = cfg.on_second_instance.clone();
let decider = cfg.on_second_instance_reveal.clone();
let version = app.package_info().version.to_string();
let identifier = app.config().identifier.clone();
match instance::arbitrate_with_legacy(
&cfg.app_id,
&version,
Some(identifier.as_str()),
Arc::new(move |s| {
lifecycle::route_second_instance(
&for_cb,
custom.as_ref(),
&s.argv,
&s.cwd,
|app, argv, cwd| {
lifecycle::on_second_instance(app, argv, cwd, decider.as_ref())
},
)
}),
) {
instance::Arbitration::Primary(h) => {
let _claim = single_instance::hold(h);
single = "primary".into();
}
instance::Arbitration::Secondary => std::process::exit(0),
instance::Arbitration::Unavailable(why) => return Err(unavailable_error(&why)),
instance::Arbitration::InvalidAppId(e) => return Err(invalid_app_id_error(&e)),
}
}
let state_dir = match cfg.state_dir.clone() {
Some(d) => d,
None => app
.path()
.app_data_dir()
.map(|d| d.join("shell"))
.unwrap_or_else(|_| std::env::temp_dir().join(&cfg.app_id)),
};
let exe = std::env::current_exe().unwrap_or_default();
let tray_runtime = cfg.tray.as_ref().map(|t| tray::TrayRuntime {
icon: Mutex::new(None),
ids: Mutex::new(Default::default()),
show_label: t.show_label.clone(),
quit_label: t.quit_label.clone(),
});
#[cfg(feature = "engine")]
let engine = cfg.engine.clone().map(|s| {
Arc::new(engine::EngineHost::with_policy(
s,
cfg.engine_policy.clone(),
))
});
let cache_repair = if lifecycle::repair_cache_enabled(cfg.repair_cache, cfg.lifecycle_only) {
let dir = cfg
.cache_dir
.clone()
.or_else(|| app.path().app_cache_dir().ok());
dir.and_then(|d| recovery::ensure_cache_dir(&d).ok().map(|o| (d, o)))
} else {
None
};
let presence = Arc::new(presence::PresenceDnd::new());
if cfg.modules.contains(Module::Presence) {
let (handle, on_presence) = (app.clone(), cfg.on_presence.clone());
presence::start_loop(Arc::clone(&presence), cfg.app_name.clone(), move |s| {
use tauri::Emitter;
let _ = handle.emit(EVENT_PRESENCE, s);
if let Some(cb) = &on_presence {
cb(&handle, s);
}
});
}
let recovery = if cfg.modules.contains(Module::Recovery) {
cfg.recovery
.clone()
.map(|spec| recovery::RecoveryStore::new(spec, &state_dir))
} else {
None
};
let stream_schemes: Vec<String> = if cfg.modules.contains(Module::Stream) {
cfg.streams.iter().map(|(s, _)| s.clone()).collect()
} else {
Vec::new()
};
let site_scheme = if cfg.modules.contains(Module::Stream) {
cfg.site.as_ref().map(|(s, _)| s.clone())
} else {
None
};
let autostart_spec = autostart::AutostartSpec {
app_id: cfg.app_id.clone(),
app_name: cfg.app_name.clone(),
executable: exe,
options: cfg.autostart.clone(),
legacy_plugin_name: cfg
.autostart
.migrate_from_plugin_entry
.then(|| app.package_info().name.clone()),
};
if cfg.autostart.migrate_from_plugin_entry {
let legacy = app.package_info().name.clone();
match autostart::migrate_plugin_entry(&autostart_spec, &legacy) {
Ok(_) => {}
Err(e) => eprintln!(
"rightkit-tauri-shell: could not migrate the tauri-plugin-autostart login entry: {e}"
),
}
}
if cfg.autostart.macos_backend == autostart::MacosBackend::ServiceManagement {
if let Err(e) = autostart::migrate_launch_agent(&autostart_spec) {
eprintln!("rightkit-tauri-shell: could not move the LaunchAgent login item to SMAppService: {e}");
}
}
app.manage(ShellState {
main_thread_id: std::thread::current().id(),
app_id: cfg.app_id.clone(),
app_name: cfg.app_name.clone(),
main_label: cfg.main_label.clone(),
nav,
opener,
frameless: cfg.frameless && cfg.modules.contains(Module::Frameless),
close_to_tray: cfg.close_to_tray,
hide_to_background: cfg.hide_to_background,
on_visibility: cfg.on_visibility.clone(),
activation: Default::default(),
show_policy,
quit: Arc::new(quit::QuitGate::new(cfg.quit_drain)),
windows: Arc::new(window_state::WindowStateStore::new(state_dir)),
deep: Default::default(),
schemes: cfg.schemes.clone(),
frontend_ready: AtomicBool::new(false),
exiting: AtomicBool::new(false),
traffic: Default::default(),
autostart: autostart_spec,
menu_items: Mutex::new(HashMap::new()),
menu_rows: Mutex::new(Vec::new()),
tray: tray_runtime,
on_menu: cfg.on_menu.clone(),
#[cfg(feature = "engine")]
engine: engine.clone(),
hosted: Mutex::new(None),
single_instance: Mutex::new(single),
broker: Arc::new(PathBroker::with_app_name(&cfg.app_name)),
notifications: notify::NotificationState::new(),
presence,
idle: cfg.idle_after.map(|d| {
let mut options = cfg.idle_options.unwrap_or_default();
options.suspend_after = d;
idle::IdleHost::new(d)
.with_options(options)
.with_on_error(cfg.idle_on_error.clone())
.with_on_transition(cfg.idle_on_transition.clone())
}),
recovery,
cache_repair: Mutex::new(cache_repair),
stream_scheme: stream_schemes.first().cloned(),
stream_schemes,
site_scheme,
site_policy: cfg
.site
.as_ref()
.map(|(_, p)| p.clone())
.unwrap_or_default(),
modules: cfg.modules,
clipboard: cfg.clipboard,
clipboard_options: cfg.clipboard_options,
clipboard_dir: clipboard::clipboard_temp_dir(&cfg.app_id),
});
if let Some(spec) = &cfg.menu {
let built = menu::build(
app,
&MenuSpec {
app_name: cfg.app_name.clone(),
..spec.clone()
},
)?;
let state = app.state::<ShellState>();
*state.menu_rows.lock().unwrap_or_else(|e| e.into_inner()) = menu::describe(&built.menu);
*state.menu_items.lock().unwrap_or_else(|e| e.into_inner()) = built.items;
app.set_menu(built.menu)?;
app.on_menu_event(|app, event| lifecycle::on_menu_event(app, event.id().as_ref()));
}
if let Some(spec) = &cfg.tray {
let icon = tray::install(app, spec, &cfg.main_label)?;
let state = app.state::<ShellState>();
if let Some(rt) = &state.tray {
*rt.icon.lock().unwrap_or_else(|e| e.into_inner()) = Some(icon);
}
}
#[cfg(feature = "engine")]
if let Some(engine) = engine {
let handle = app.clone();
let lifecycle_events = engine.subscribe_lifecycle();
let supervised = lifecycle_events.is_some();
if let Some(events) = lifecycle_events {
let handle = app.clone();
let host = Arc::downgrade(&engine);
std::thread::Builder::new()
.name("rk-shell-engine-events".into())
.spawn(move || {
use tauri::Emitter;
loop {
let Some(host) = host.upgrade() else { break };
if host.is_shutting_down() { break; }
match events.recv_timeout(std::time::Duration::from_millis(250)) {
Ok(event) => {
let _ = handle.emit(EVENT_ENGINE, serde_json::json!({
"call": "$lifecycle", "event": host.lifecycle(), "supervisor": event
}));
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
}
}
})?;
}
if let Some(events) = engine.subscribe_events() {
let handle = app.clone();
let host = Arc::downgrade(&engine);
std::thread::Builder::new()
.name("rk-shell-engine-unsolicited".into())
.spawn(move || {
use tauri::Emitter;
loop {
let Some(host) = host.upgrade() else { break };
if host.is_shutting_down() {
break;
}
match events.recv_timeout(std::time::Duration::from_millis(250)) {
Ok(frame) => {
let _ = handle.emit(
EVENT_ENGINE,
engine::EngineEvent {
call: "$unsolicited".into(),
id: None,
tag: None,
event: frame["event"].clone(),
},
);
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
}
}
})?;
}
std::thread::Builder::new()
.name("rk-shell-engine".into())
.spawn(move || {
use tauri::Emitter;
let payload = match engine.start() {
Ok(hello) => serde_json::json!({"state": "ready", "hello": hello}),
Err(e) => {
serde_json::json!({"state": "error", "code": e.code, "message": e.message})
}
};
engine.set_lifecycle(payload.clone());
if !supervised {
let _ = handle.emit(
EVENT_ENGINE,
serde_json::json!({"call": "$lifecycle", "event": payload}),
);
}
})?;
}
if cfg.modules.contains(Module::Watchdog) {
if let Some(w) = cfg.watchdog.clone() {
watchdog::install(app.clone(), w);
}
}
let argv: Vec<String> = std::env::args().skip(1).collect();
let state = app.state::<ShellState>();
state
.deep
.push_all(argv.iter().map(String::as_str), &state.schemes);
notify::set_default_app_id(&app.config().identifier);
open_url::deliver(&argv);
Ok(())
}
#[macro_export]
macro_rules! handler {
($($app_cmd:path),* $(,)?) => {
::tauri::generate_handler![
$crate::commands::rk_shell_open_external,
$crate::commands::rk_shell_ready,
$crate::commands::rk_shell_status,
$crate::commands::rk_shell_window,
$crate::commands::rk_shell_open_window,
$crate::commands::rk_shell_set_tray,
$crate::commands::rk_shell_set_menu_item_enabled,
$crate::commands::rk_shell_autostart,
$crate::commands::rk_shell_open_login_items_settings,
$crate::commands::rk_shell_clipboard_read,
$crate::commands::rk_shell_clipboard_write,
$crate::commands::rk_shell_engine_call,
$crate::commands::rk_shell_engine_call_tagged,
$crate::commands::rk_shell_engine_status,
$crate::commands::rk_shell_traffic_lights,
$crate::commands::rk_shell_quit,
$crate::commands::rk_shell_finish_quit,
$crate::commands::rk_shell_capabilities,
$crate::commands::rk_shell_set_theme,
$crate::commands::rk_shell_notify,
$crate::commands::rk_shell_notify_status,
$crate::commands::rk_shell_notify_clear,
$crate::commands::rk_shell_badge,
$crate::commands::rk_shell_set_dnd,
$crate::commands::rk_shell_presence,
$crate::commands::rk_shell_presence_prefs,
$crate::commands::rk_shell_presence_probe,
$crate::commands::rk_shell_presence_evaluate,
$crate::commands::rk_shell_idle_status,
$crate::commands::rk_shell_idle_note,
$crate::commands::rk_shell_path_require,
$crate::commands::rk_shell_path_revoke,
$crate::commands::rk_shell_path_grant_recent,
$crate::commands::rk_shell_path_regrant_saved,
$crate::commands::rk_shell_stream_grant,
$crate::commands::rk_shell_recovery_save,
$crate::commands::rk_shell_recovery_load,
$crate::commands::rk_shell_recovery_clear,
$crate::commands::rk_shell_clipboard_item,
$crate::commands::rk_shell_site_grant,
$crate::commands::rk_shell_site_revoke
$(, $app_cmd)*
]
};
}