1pub mod autostart;
18pub mod b64;
19pub mod broker;
20pub mod clipboard;
21pub mod commands;
22pub mod deep_link;
23pub mod devtools;
24#[cfg(feature = "engine")]
25pub mod engine;
26pub mod error;
27pub mod frameless;
28pub mod harden;
29pub mod idle;
30pub mod instance;
31mod legacy_hold;
32mod legacy_instance;
33pub mod lifecycle;
34pub mod menu;
35pub mod modules;
36pub mod nav;
37pub mod notify;
38pub mod open_url;
39pub mod opener;
40pub mod presence;
41pub mod quit;
42pub mod recovery;
43pub mod single_instance;
44pub mod site;
45pub mod startup_cache;
46pub mod stream;
47pub mod tray;
48pub mod watchdog;
49pub mod window_state;
50#[cfg(windows)]
52pub(crate) mod winapi {
53 #[cfg(not(any(feature = "wv2-0_38", feature = "wv2-0_39")))]
54 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\"]");
55 #[cfg(all(feature = "wv2-0_38", not(feature = "wv2-0_39")))]
56 pub use ::{webview2_com_038 as webview2_com, windows_061 as windows};
57 #[cfg(feature = "wv2-0_39")]
58 pub use ::{webview2_com_039 as webview2_com, windows_062 as windows};
59}
60pub mod windows;
61
62use std::collections::HashMap;
63use std::path::PathBuf;
64use std::sync::atomic::AtomicBool;
65use std::sync::{Arc, Mutex};
66
67use tauri::menu::MenuItem;
68use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
69use tauri::{AppHandle, Manager, Wry};
70
71pub use autostart::{AutostartMigration, AutostartOptions, LegacyAutostartNames};
72pub use broker::PathBroker;
73pub use clipboard::{ClipboardItem, ClipboardOptions, ClipboardPolicy, ImagePreference};
74pub use devtools::{ContentWebview, DevToolsError, DevToolsGate, DevToolsPolicy, WebviewDecision};
75pub use error::ShellError;
76pub use harden::{Hardening, HardeningScript};
77pub use idle::{
78 IdleErrorCallback, IdleFailure, IdlePhase, IdleTransition, IdleTransitionCallback,
79 IdleTransitionKind,
80};
81pub use menu::{MenuEntry, MenuGroup, MenuSpec};
82pub use modules::{Module, ModuleSet, ShellPlugin, ShellPluginBuilder};
83pub use nav::{NavDecision, NavPolicy};
84pub use opener::{ExternalOpener, SystemOpener};
85pub use presence::{AutoPrefs, PresenceState};
86pub use recovery::{MigrationOutcome, RecoverySpec};
87pub use single_instance::{
88 ClaimFailure, ClaimOptions, ClaimOutcome, InstanceMessage, LegacySingleInstanceNames,
89 SingleInstance, SingleInstanceGuard,
90};
91pub use site::SitePolicy;
92pub use stream::{StreamEvent, StreamEventKind, StreamObserver, StreamPolicy};
93pub use tray::{LeftClick, TraySpec};
94pub use watchdog::{ShellRuntimeStatus, StallReport, WatchdogConfig, WatchdogStatus};
95
96pub const EVENT_MENU: &str = "rightkit-shell://menu";
97pub const EVENT_TRAY: &str = "rightkit-shell://tray";
98pub const EVENT_DEEP_LINK: &str = "rightkit-shell://deep-link";
99pub const EVENT_SECOND_INSTANCE: &str = "rightkit-shell://second-instance";
100pub const EVENT_STALL: &str = "rightkit-shell://stall";
101pub const EVENT_ENGINE: &str = "rightkit-shell://engine";
102pub const EVENT_WINDOW: &str = "rightkit-shell://window";
103pub const EVENT_IDLE: &str = "rightkit-shell://idle";
104pub const EVENT_PRESENCE: &str = "rightkit-shell://presence";
105pub const EVENT_QUIT_REQUESTED: &str = "rightkit-shell://quit-requested";
107
108type MenuCallback = Arc<dyn Fn(&AppHandle<Wry>, &str) + Send + Sync>;
109type PresenceCallback = Arc<dyn Fn(&AppHandle<Wry>, PresenceState) + Send + Sync>;
110type VisibilityCallback = Arc<dyn Fn(&AppHandle<Wry>, lifecycle::Visibility) + Send + Sync>;
111type RevealDecider = lifecycle::RevealDecider<AppHandle<Wry>>;
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
115pub enum CloseBehavior {
116 #[default]
118 Quit,
119 HideToTray,
121 HideToBackground,
124}
125
126pub struct ShellState {
128 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
130 pub(crate) main_thread_id: std::thread::ThreadId,
131 pub app_id: String,
132 pub app_name: String,
133 pub main_label: String,
134 pub nav: Arc<NavPolicy>,
135 pub opener: Arc<dyn ExternalOpener>,
136 pub frameless: bool,
137 pub close_to_tray: bool,
138 pub hide_to_background: bool,
140 pub on_visibility: Option<VisibilityCallback>,
141 pub(crate) activation: lifecycle::ActivationTracker,
142 pub show_policy: lifecycle::ShowPolicy,
144 pub quit: Arc<quit::QuitGate>,
146 pub windows: Arc<window_state::WindowStateStore>,
147 pub deep: deep_link::DeepLinkQueue,
148 pub schemes: Vec<String>,
149 pub frontend_ready: AtomicBool,
150 pub exiting: AtomicBool,
151 pub traffic: frameless::TrafficLights,
152 pub autostart: autostart::AutostartSpec,
153 pub menu_items: Mutex<HashMap<String, MenuItem<Wry>>>,
154 pub menu_rows: Mutex<Vec<(String, String)>>,
155 pub tray: Option<tray::TrayRuntime>,
156 pub on_menu: Option<MenuCallback>,
157 #[cfg(feature = "engine")]
158 pub engine: Option<Arc<engine::EngineHost>>,
159 pub hosted: Mutex<Option<rightkit_daemon::Hosted>>,
160 pub single_instance: Mutex<String>,
161 pub broker: Arc<PathBroker>,
163 pub notifications: notify::NotificationState,
164 pub presence: Arc<presence::PresenceDnd>,
165 pub idle: Option<idle::IdleHost>,
166 pub recovery: Option<recovery::RecoveryStore>,
167 pub cache_repair: Mutex<Option<(PathBuf, recovery::CacheDirOutcome)>>,
169 pub stream_scheme: Option<String>,
171 pub stream_schemes: Vec<String>,
173 pub site_scheme: Option<String>,
175 pub site_policy: site::SitePolicy,
176 pub modules: ModuleSet,
178 pub clipboard: clipboard::ClipboardPolicy,
179 pub clipboard_options: clipboard::ClipboardOptions,
181 pub clipboard_dir: PathBuf,
183}
184
185pub type SecondInstanceCallback = Arc<dyn Fn(&AppHandle<Wry>, &[String], &str) + Send + Sync>;
187
188pub struct ShellBuilder {
189 app_id: String,
190 app_name: String,
191 main_label: String,
192 hardening: Hardening,
193 hardening_script: HardeningScript,
194 devtools_policy: DevToolsPolicy,
195 content_webviews: Vec<ContentWebview>,
196 nav: NavPolicy,
197 extra_internal_schemes: Vec<String>,
198 opener: Arc<dyn ExternalOpener>,
199 single_instance: bool,
200 close_to_tray: bool,
201 hide_to_background: bool,
202 on_visibility: Option<VisibilityCallback>,
203 never_show: bool,
204 never_show_env: Vec<String>,
205 never_show_env_values: Vec<(String, String)>,
206 reveal_on_reopen: bool,
207 lifecycle_only: bool,
208 qa_instance_exempt: bool,
209 on_second_instance_reveal: Option<RevealDecider>,
210 quit_drain: Option<std::time::Duration>,
211 frameless: bool,
212 show_on_ready: Option<bool>,
213 state_dir: Option<PathBuf>,
214 schemes: Vec<String>,
215 menu: Option<MenuSpec>,
216 tray: Option<TraySpec>,
217 watchdog: Option<WatchdogConfig>,
218 on_menu: Option<MenuCallback>,
219 on_second_instance: Option<SecondInstanceCallback>,
220 streams: Vec<(String, StreamPolicy)>,
221 site: Option<(String, site::SitePolicy)>,
222 modules: ModuleSet,
223 clipboard: clipboard::ClipboardPolicy,
224 clipboard_options: clipboard::ClipboardOptions,
225 idle_on_error: Option<idle::IdleErrorCallback>,
226 idle_on_transition: Option<idle::IdleTransitionCallback>,
227 autostart: autostart::AutostartOptions,
228 recovery: Option<RecoverySpec>,
229 idle_after: Option<std::time::Duration>,
230 idle_options: Option<idle::IdleOptions>,
231 cache_dir: Option<PathBuf>,
232 repair_cache: Option<bool>,
233 on_presence: Option<PresenceCallback>,
234 #[cfg(feature = "engine")]
235 engine: Option<engine::EngineSpec>,
236}
237
238pub struct Shell {
239 cfg: ShellBuilder,
240}
241
242impl Shell {
243 pub fn builder(app_id: &str) -> ShellBuilder {
246 ShellBuilder {
247 app_id: app_id.to_owned(),
248 app_name: app_id.to_owned(),
249 main_label: "main".into(),
250 hardening: Hardening::default(),
251 hardening_script: HardeningScript::default(),
252 devtools_policy: DevToolsPolicy::default(),
253 content_webviews: Vec::new(),
254 nav: NavPolicy::default(),
255 extra_internal_schemes: Vec::new(),
256 opener: Arc::new(SystemOpener),
257 single_instance: true,
258 close_to_tray: false,
259 hide_to_background: false,
260 on_visibility: None,
261 never_show: false,
262 never_show_env: Vec::new(),
263 never_show_env_values: Vec::new(),
264 reveal_on_reopen: true,
265 lifecycle_only: false,
266 qa_instance_exempt: false,
267 on_second_instance_reveal: None,
268 quit_drain: None,
269 frameless: false,
270 show_on_ready: None,
271 state_dir: None,
272 schemes: Vec::new(),
273 menu: None,
274 tray: None,
275 watchdog: Some(WatchdogConfig::default()),
276 on_menu: None,
277 on_second_instance: None,
278 streams: Vec::new(),
279 site: None,
280 modules: ModuleSet::all(),
281 clipboard: clipboard::ClipboardPolicy::default(),
282 clipboard_options: clipboard::ClipboardOptions::default(),
283 idle_on_error: None,
284 idle_on_transition: None,
285 autostart: autostart::AutostartOptions::default(),
286 recovery: None,
287 idle_after: None,
288 idle_options: None,
289 cache_dir: None,
290 repair_cache: None,
291 on_presence: None,
292 #[cfg(feature = "engine")]
293 engine: None,
294 }
295 }
296}
297
298impl ShellBuilder {
299 pub fn app_name(mut self, name: &str) -> Self {
300 self.app_name = name.to_owned();
301 self
302 }
303 pub fn main_label(mut self, label: &str) -> Self {
304 self.main_label = label.to_owned();
305 self
306 }
307 pub fn hardening(mut self, h: Hardening) -> Self {
308 self.hardening = h;
309 self
310 }
311 pub fn hardening_script(mut self, scope: HardeningScript) -> Self {
314 self.hardening_script = scope;
315 self
316 }
317 pub fn devtools_policy(mut self, p: DevToolsPolicy) -> Self {
322 self.devtools_policy = p;
323 self
324 }
325 pub fn content_webview(mut self, c: ContentWebview) -> Self {
329 self.content_webviews.push(c);
330 self
331 }
332 pub fn nav_policy(mut self, p: NavPolicy) -> Self {
333 self.nav = p;
334 self
335 }
336 pub fn internal_scheme(mut self, scheme: &str) -> Self {
343 nav::validate_internal_scheme(scheme).expect("rightkit-tauri-shell: internal_scheme");
344 self.extra_internal_schemes.push(scheme.to_owned());
345 self
346 }
347 pub fn opener(mut self, o: impl ExternalOpener) -> Self {
349 self.opener = Arc::new(o);
350 self
351 }
352 pub fn single_instance(mut self, on: bool) -> Self {
353 self.single_instance = on;
354 self
355 }
356 pub fn close_to_tray(mut self, on: bool) -> Self {
358 self.close_to_tray = on;
359 if !on {
360 self.hide_to_background = false;
361 }
362 self
363 }
364 pub fn close_behavior(mut self, behavior: CloseBehavior) -> Self {
369 self.close_to_tray = behavior != CloseBehavior::Quit;
370 self.hide_to_background = behavior == CloseBehavior::HideToBackground;
371 self
372 }
373 pub fn on_visibility(
378 mut self,
379 f: impl Fn(&AppHandle<Wry>, lifecycle::Visibility) + Send + Sync + 'static,
380 ) -> Self {
381 self.on_visibility = Some(Arc::new(f));
382 self
383 }
384 pub fn never_show(mut self, on: bool) -> Self {
391 self.never_show = on;
392 self
393 }
394 pub fn never_show_env(mut self, name: &str) -> Self {
402 assert!(
403 !name.is_empty() && !name.contains(['=', '\0']),
404 "rightkit-tauri-shell: never_show_env needs a plain environment variable name"
405 );
406 if !self.never_show_env.iter().any(|n| n == name) {
407 self.never_show_env.push(name.to_owned());
408 }
409 self
410 }
411 pub fn never_show_env_value(mut self, name: &str, expected: &str) -> Self {
418 assert!(
419 !name.is_empty() && !name.contains(['=', '\0']) && !expected.is_empty(),
420 "rightkit-tauri-shell: never_show_env_value needs a plain variable name and a non-empty expected value"
421 );
422 let pair = (name.to_owned(), expected.to_owned());
423 if !self.never_show_env_values.contains(&pair) {
424 self.never_show_env_values.push(pair);
425 }
426 self
427 }
428 pub fn reveal_on_reopen(mut self, on: bool) -> Self {
433 self.reveal_on_reopen = on;
434 self
435 }
436 pub fn qa_instance_exempt(mut self, on: bool) -> Self {
440 self.qa_instance_exempt = on;
441 self
442 }
443 pub fn second_instance_reveal(
447 mut self,
448 f: impl Fn(&AppHandle<Wry>, &[String], &str) -> bool + Send + Sync + 'static,
449 ) -> Self {
450 self.on_second_instance_reveal = Some(Arc::new(f));
451 self
452 }
453 pub fn quit_drain(mut self, on: bool) -> Self {
459 self.quit_drain = on.then_some(quit::DEFAULT_QUIT_DRAIN_BOUND);
460 self
461 }
462 pub fn quit_drain_bound(mut self, bound: std::time::Duration) -> Self {
466 self.quit_drain = Some(bound);
467 self
468 }
469 pub fn frameless(mut self, on: bool) -> Self {
471 self.frameless = on;
472 self
473 }
474 pub fn show_on_ready(mut self, on: bool) -> Self {
477 self.show_on_ready = Some(on);
478 self
479 }
480 pub fn state_dir(mut self, dir: PathBuf) -> Self {
482 self.state_dir = Some(dir);
483 self
484 }
485 pub fn deep_link_scheme(mut self, scheme: &str) -> Self {
487 self.schemes.push(scheme.to_ascii_lowercase());
488 self
489 }
490 pub fn menu(mut self, m: MenuSpec) -> Self {
491 self.menu = Some(m);
492 self
493 }
494 pub fn tray(mut self, t: TraySpec) -> Self {
495 self.tray = Some(t);
496 self
497 }
498 pub fn autostart_options(mut self, options: autostart::AutostartOptions) -> Self {
501 self.autostart = options;
502 self
503 }
504 pub fn on_stall(mut self, f: impl Fn(&watchdog::StallReport) + Send + Sync + 'static) -> Self {
508 if let Some(config) = self.watchdog.as_mut() {
509 config.on_stall = Some(Arc::new(f));
510 }
511 self
512 }
513 pub fn watchdog(mut self, w: Option<WatchdogConfig>) -> Self {
514 self.watchdog = w;
515 self
516 }
517 pub fn on_second_instance(
523 mut self,
524 f: impl Fn(&AppHandle<Wry>, &[String], &str) + Send + Sync + 'static,
525 ) -> Self {
526 self.on_second_instance = Some(Arc::new(f));
527 self
528 }
529 pub fn on_menu(mut self, f: impl Fn(&AppHandle<Wry>, &str) + Send + Sync + 'static) -> Self {
530 self.on_menu = Some(Arc::new(f));
531 self
532 }
533 pub fn stream_protocol(mut self, scheme: &str, policy: StreamPolicy) -> Self {
536 let scheme = scheme.to_ascii_lowercase();
537 self.streams.retain(|(s, _)| *s != scheme);
538 self.streams.push((scheme, policy));
539 self
540 }
541 pub fn site_protocol(mut self, scheme: &str, policy: site::SitePolicy) -> Self {
544 self.site = Some((scheme.to_ascii_lowercase(), policy));
545 self
546 }
547 pub fn clipboard_purge_after(mut self, age: std::time::Duration) -> Self {
549 self.clipboard.purge_after = age;
550 self
551 }
552 pub fn clipboard_options(mut self, options: clipboard::ClipboardOptions) -> Self {
556 self.clipboard_options = options;
557 self
558 }
559 pub fn recovery(mut self, spec: RecoverySpec) -> Self {
561 self.recovery = Some(spec);
562 self
563 }
564 pub fn webview_idle(mut self, suspend_after: std::time::Duration) -> Self {
566 self.idle_after = Some(suspend_after);
567 self
568 }
569 pub fn webview_idle_options(mut self, options: idle::IdleOptions) -> Self {
575 self.idle_after = Some(options.suspend_after);
576 self.idle_options = Some(options);
577 self
578 }
579 pub fn webview_idle_on_error(
582 mut self,
583 f: impl Fn(&idle::IdleFailure) + Send + Sync + 'static,
584 ) -> Self {
585 self.idle_on_error = Some(Arc::new(f));
586 self
587 }
588 pub fn webview_idle_on_transition(
591 mut self,
592 f: impl Fn(&idle::IdleTransition) + Send + Sync + 'static,
593 ) -> Self {
594 self.idle_on_transition = Some(Arc::new(f));
595 self
596 }
597 pub fn cache_dir(mut self, dir: PathBuf) -> Self {
599 self.cache_dir = Some(dir);
600 self
601 }
602 pub fn repair_cache(mut self, on: bool) -> Self {
605 self.repair_cache = Some(on);
606 self
607 }
608 pub fn on_presence(
610 mut self,
611 f: impl Fn(&AppHandle<Wry>, PresenceState) + Send + Sync + 'static,
612 ) -> Self {
613 self.on_presence = Some(Arc::new(f));
614 self
615 }
616 #[cfg(feature = "engine")]
617 pub fn engine(mut self, spec: engine::EngineSpec) -> Self {
618 self.engine = Some(spec);
619 self
620 }
621 pub fn build(self) -> Shell {
622 assert!(
623 !self.app_id.is_empty()
624 && self.app_id.len() <= 40
625 && self
626 .app_id
627 .chars()
628 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
629 "rightkit-tauri-shell: app_id must be lowercase letters, digits and '-' (<= 40 chars)"
630 );
631 Shell { cfg: self }
632 }
633}
634
635impl Shell {
636 pub fn single_instance_plugin(
642 self,
643 on_second_instance: impl Fn(&AppHandle<Wry>, &[String], &str) + Send + Sync + 'static,
644 ) -> TauriPlugin<Wry> {
645 let app_id = self.cfg.app_id;
646 PluginBuilder::<Wry>::new("rightkit-single-instance")
647 .setup(move |app, _api| {
648 let handle = app.clone();
649 let version = app.package_info().version.to_string();
650 let identifier = app.config().identifier.clone();
651 let arbitration = instance::arbitrate_with_legacy(
652 &app_id,
653 &version,
654 Some(identifier.as_str()),
655 Arc::new(move |second| {
656 on_second_instance(&handle, &second.argv, &second.cwd);
657 }),
658 );
659 match arbitration {
660 instance::Arbitration::Primary(hosted) => {
661 app.manage(PluginClaim(single_instance::hold(hosted)));
662 }
663 instance::Arbitration::Secondary => std::process::exit(0),
664 instance::Arbitration::Unavailable(why) => {
666 return Err(unavailable_error(&why));
667 }
668 instance::Arbitration::InvalidAppId(e) => return Err(invalid_app_id_error(&e)),
669 }
670 Ok(())
671 })
672 .on_event(|app, event| {
673 if matches!(event, tauri::RunEvent::Exit) {
674 if let Some(claim) = app.try_state::<PluginClaim>() {
675 single_instance::release(claim.0);
676 }
677 }
678 })
679 .build()
680 }
681
682 pub fn lifecycle_plugin(self) -> TauriPlugin<Wry> {
688 ShellPlugin::builder().shell(self).lifecycle_only()
689 }
690
691 pub fn plugin(self) -> TauriPlugin<Wry> {
695 ShellPlugin::builder().shell(self).build()
696 }
697}
698
699pub(crate) fn install(cfg: ShellBuilder) -> TauriPlugin<Wry> {
702 let modules = cfg.modules;
703 let mut nav_policy = cfg.nav.clone();
704 for scheme in &cfg.extra_internal_schemes {
705 nav_policy = nav_policy
706 .with_internal_scheme(scheme)
707 .expect("rightkit-tauri-shell: internal_scheme");
708 }
709 let nav = Arc::new(nav_policy);
710 let opener = Arc::clone(&cfg.opener);
711 let hardening = cfg.hardening.clone();
712 let script_scope = cfg.hardening_script;
713 let (nav_hook, opener_hook) = (Arc::clone(&nav), Arc::clone(&opener));
714 let hardening_for_views = hardening.clone();
715 let guard_result = devtools::guard_process_start();
718 let gate_result = devtools::build_gate_with(
719 cfg.devtools_policy,
720 &cfg.main_label,
721 &cfg.content_webviews,
722 cfg!(feature = "content-devtools"),
723 devtools::DEV_INSPECT_COMPILED,
724 );
725 let gate_for_views = gate_result
726 .clone()
727 .unwrap_or_else(|_| DevToolsGate::denied(&cfg.main_label));
728 let content_labels = gate_for_views.content_labels();
729 let harden_webviews = modules.contains(Module::JsHardening);
730 let show_on_ready_explicit = cfg.show_on_ready;
731 let show_on_ready_module = lifecycle::show_on_ready_installed(
734 modules.contains(Module::ShowOnReady),
735 cfg.lifecycle_only,
736 show_on_ready_explicit,
737 );
738 let ready_policy = show_policy_of(&cfg);
739 let restore_windows = modules.contains(Module::WindowState);
740 let main_label = cfg.main_label.clone();
741 let (streams, site) = if modules.contains(Module::Stream) {
742 (cfg.streams.clone(), cfg.site.clone())
743 } else {
744 (Vec::new(), None)
745 };
746
747 let mut plugin = PluginBuilder::<Wry>::new("rightkit-shell");
748 plugin = plugin.on_page_load(|webview, payload| {
751 if matches!(payload.event(), tauri::webview::PageLoadEvent::Finished) {
752 if let Err(e) = devtools::verify_after_first_navigation(webview.app_handle()) {
753 eprintln!("{e}");
754 webview.app_handle().exit(1);
755 }
756 }
757 });
758 if modules.contains(Module::NavigationPolicy) {
759 plugin = plugin.on_navigation(move |_webview, url| match nav_hook.decide(url) {
760 NavDecision::Allow => true,
761 NavDecision::External { url } => {
762 let _ = opener::open_checked(opener_hook.as_ref(), &url);
763 false
764 }
765 NavDecision::Block => false,
766 });
767 }
768 plugin = plugin
769 .on_webview_ready(move |webview| {
770 let decision = gate_for_views.decide(webview.label());
771 if devtools::apply_native(&webview, decision).is_err() {
775 return;
776 }
777 if harden_webviews {
778 harden::apply_native(&webview, &hardening_for_views, decision);
779 }
780 if let Err(e) = devtools::verify_not_remote_debuggable(webview.app_handle()) {
781 eprintln!("{e}");
782 webview.app_handle().exit(1);
783 return;
784 }
785 let app = webview.app_handle().clone();
786 let label = webview.label().to_owned();
787 lifecycle::enforce_never_show(&app, &ready_policy);
788 windows::with_placement_announced(&app, &label, || {
789 if restore_windows {
790 if let Some(state) = app.try_state::<ShellState>() {
791 let _ = state.windows.restore(&app, &label);
792 }
793 }
794 });
795 if label == main_label
796 && lifecycle::show_on_ready_decision(
797 show_on_ready_explicit,
798 show_on_ready_module,
799 lifecycle::launched_in_background(),
800 lifecycle::qa_hidden(),
801 )
802 {
803 if let Some(w) = app.get_webview_window(&label) {
804 let _ = crate::windows::show_window(
806 &app,
807 &w,
808 lifecycle::RevealSource::ShowOnReady,
809 windows::RevealStyle::ShowOnly,
810 );
811 }
812 }
813 })
814 .on_event(|app, event| {
815 if app.try_state::<ShellState>().is_some() {
816 lifecycle::run_event(app, event);
817 }
818 })
819 .setup(move |app, _api| {
820 let app = app.clone();
821 if let Err(e) = guard_result {
822 eprintln!("{e}");
823 return Err(Box::new(e));
824 }
825 match gate_result {
826 Ok(gate) => {
827 app.manage(gate);
828 }
829 Err(e) => {
830 eprintln!("{e}");
831 return Err(Box::new(e));
832 }
833 }
834 setup(&app, cfg, nav, opener)
835 });
836 if harden_webviews {
837 plugin = match script_scope {
838 HardeningScript::AllFrames => {
839 plugin.js_init_script_on_all_frames(hardening.script_for(&content_labels))
840 }
841 HardeningScript::TopFrameOnly => plugin.js_init_script(format!(
844 "if (window.top !== window.self) {{ return; }}\n{}",
845 hardening.script_for(&content_labels)
846 )),
847 HardeningScript::Off => plugin,
848 };
849 }
850 for (scheme, policy) in streams {
851 plugin = plugin.register_asynchronous_uri_scheme_protocol(
852 scheme,
853 move |ctx, request, responder| {
854 let state = ctx.app_handle().state::<ShellState>();
855 stream::serve(&state.broker, &policy, &request, move |response| {
856 responder.respond(response)
857 });
858 },
859 );
860 }
861 if let Some((scheme, policy)) = site {
862 plugin = plugin.register_asynchronous_uri_scheme_protocol(
863 scheme,
864 move |ctx, request, responder| {
865 let state = ctx.app_handle().state::<ShellState>();
866 site::serve_site_request(&state.broker, &policy, &request, move |response| {
867 responder.respond(response)
868 });
869 },
870 );
871 }
872 plugin.build()
873}
874
875struct PluginClaim(u64);
877
878fn unavailable_error(why: &str) -> Box<dyn std::error::Error> {
880 format!("rightkit-tauri-shell: single-instance could not be decided, refusing to start as a second primary: {why}").into()
881}
882
883fn invalid_app_id_error(e: &instance::InvalidAppId) -> Box<dyn std::error::Error> {
886 format!("rightkit-tauri-shell: single-instance cannot start, app id is not usable: {e}").into()
887}
888
889fn show_policy_of(cfg: &ShellBuilder) -> lifecycle::ShowPolicy {
891 lifecycle::ShowPolicy {
892 never_show: cfg.never_show,
893 never_show_env: cfg.never_show_env.clone(),
894 never_show_env_values: cfg.never_show_env_values.clone(),
895 reveal_on_reopen: cfg.reveal_on_reopen && cfg.modules.contains(Module::RevealOnReopen),
896 }
897}
898
899fn setup(
900 app: &AppHandle<Wry>,
901 cfg: ShellBuilder,
902 nav: Arc<NavPolicy>,
903 opener: Arc<dyn ExternalOpener>,
904) -> Result<(), Box<dyn std::error::Error>> {
905 let show_policy = show_policy_of(&cfg);
907 lifecycle::enforce_never_show(app, &show_policy);
908 let mut single = String::from("disabled");
910 let mode = lifecycle::instance_mode(
911 cfg.single_instance,
912 cfg.qa_instance_exempt,
913 lifecycle::qa_hidden(),
914 );
915 if mode == lifecycle::InstanceMode::QaExempt {
916 single = "qa-exempt".into();
917 }
918 if mode == lifecycle::InstanceMode::Arbitrate {
919 let for_cb = app.clone();
920 let custom = cfg.on_second_instance.clone();
921 let decider = cfg.on_second_instance_reveal.clone();
922 let version = app.package_info().version.to_string();
923 let identifier = app.config().identifier.clone();
924 match instance::arbitrate_with_legacy(
925 &cfg.app_id,
926 &version,
927 Some(identifier.as_str()),
928 Arc::new(move |s| {
929 lifecycle::route_second_instance(
930 &for_cb,
931 custom.as_ref(),
932 &s.argv,
933 &s.cwd,
934 |app, argv, cwd| {
935 lifecycle::on_second_instance(app, argv, cwd, decider.as_ref())
936 },
937 )
938 }),
939 ) {
940 instance::Arbitration::Primary(h) => {
941 let _claim = single_instance::hold(h);
943 single = "primary".into();
944 }
945 instance::Arbitration::Secondary => std::process::exit(0),
946 instance::Arbitration::Unavailable(why) => return Err(unavailable_error(&why)),
948 instance::Arbitration::InvalidAppId(e) => return Err(invalid_app_id_error(&e)),
949 }
950 }
951
952 let state_dir = match cfg.state_dir.clone() {
954 Some(d) => d,
955 None => app
956 .path()
957 .app_data_dir()
958 .map(|d| d.join("shell"))
959 .unwrap_or_else(|_| std::env::temp_dir().join(&cfg.app_id)),
960 };
961 let exe = std::env::current_exe().unwrap_or_default();
962 let tray_runtime = cfg.tray.as_ref().map(|t| tray::TrayRuntime {
963 icon: Mutex::new(None),
964 ids: Mutex::new(Default::default()),
965 show_label: t.show_label.clone(),
966 quit_label: t.quit_label.clone(),
967 });
968 #[cfg(feature = "engine")]
969 let engine = cfg
970 .engine
971 .clone()
972 .map(|s| Arc::new(engine::EngineHost::new(s)));
973 let cache_repair = if lifecycle::repair_cache_enabled(cfg.repair_cache, cfg.lifecycle_only) {
975 let dir = cfg
976 .cache_dir
977 .clone()
978 .or_else(|| app.path().app_cache_dir().ok());
979 dir.and_then(|d| recovery::ensure_cache_dir(&d).ok().map(|o| (d, o)))
980 } else {
981 None
982 };
983 let presence = Arc::new(presence::PresenceDnd::new());
984 if cfg.modules.contains(Module::Presence) {
985 let (handle, on_presence) = (app.clone(), cfg.on_presence.clone());
986 presence::start_loop(Arc::clone(&presence), cfg.app_name.clone(), move |s| {
987 use tauri::Emitter;
988 let _ = handle.emit(EVENT_PRESENCE, s);
989 if let Some(cb) = &on_presence {
990 cb(&handle, s);
991 }
992 });
993 }
994 let recovery = if cfg.modules.contains(Module::Recovery) {
995 cfg.recovery
996 .clone()
997 .map(|spec| recovery::RecoveryStore::new(spec, &state_dir))
998 } else {
999 None
1000 };
1001 let stream_schemes: Vec<String> = if cfg.modules.contains(Module::Stream) {
1002 cfg.streams.iter().map(|(s, _)| s.clone()).collect()
1003 } else {
1004 Vec::new()
1005 };
1006 let site_scheme = if cfg.modules.contains(Module::Stream) {
1007 cfg.site.as_ref().map(|(s, _)| s.clone())
1008 } else {
1009 None
1010 };
1011 let autostart_spec = autostart::AutostartSpec {
1012 app_id: cfg.app_id.clone(),
1013 app_name: cfg.app_name.clone(),
1014 executable: exe,
1015 options: cfg.autostart.clone(),
1016 legacy_plugin_name: cfg
1017 .autostart
1018 .migrate_from_plugin_entry
1019 .then(|| app.package_info().name.clone()),
1020 };
1021 if cfg.autostart.migrate_from_plugin_entry {
1022 let legacy = app.package_info().name.clone();
1023 match autostart::migrate_plugin_entry(&autostart_spec, &legacy) {
1024 Ok(_) => {}
1025 Err(e) => eprintln!(
1027 "rightkit-tauri-shell: could not migrate the tauri-plugin-autostart login entry: {e}"
1028 ),
1029 }
1030 }
1031 app.manage(ShellState {
1032 main_thread_id: std::thread::current().id(),
1033 app_id: cfg.app_id.clone(),
1034 app_name: cfg.app_name.clone(),
1035 main_label: cfg.main_label.clone(),
1036 nav,
1037 opener,
1038 frameless: cfg.frameless && cfg.modules.contains(Module::Frameless),
1039 close_to_tray: cfg.close_to_tray,
1040 hide_to_background: cfg.hide_to_background,
1041 on_visibility: cfg.on_visibility.clone(),
1042 activation: Default::default(),
1043 show_policy,
1044 quit: Arc::new(quit::QuitGate::new(cfg.quit_drain)),
1045 windows: Arc::new(window_state::WindowStateStore::new(state_dir)),
1046 deep: Default::default(),
1047 schemes: cfg.schemes.clone(),
1048 frontend_ready: AtomicBool::new(false),
1049 exiting: AtomicBool::new(false),
1050 traffic: Default::default(),
1051 autostart: autostart_spec,
1052 menu_items: Mutex::new(HashMap::new()),
1053 menu_rows: Mutex::new(Vec::new()),
1054 tray: tray_runtime,
1055 on_menu: cfg.on_menu.clone(),
1056 #[cfg(feature = "engine")]
1057 engine: engine.clone(),
1058 hosted: Mutex::new(None),
1059 single_instance: Mutex::new(single),
1060 broker: Arc::new(PathBroker::new()),
1061 notifications: notify::NotificationState::new(),
1062 presence,
1063 idle: cfg.idle_after.map(|d| {
1064 let mut options = cfg.idle_options.unwrap_or_default();
1066 options.suspend_after = d;
1067 idle::IdleHost::new(d)
1068 .with_options(options)
1069 .with_on_error(cfg.idle_on_error.clone())
1070 .with_on_transition(cfg.idle_on_transition.clone())
1071 }),
1072 recovery,
1073 cache_repair: Mutex::new(cache_repair),
1074 stream_scheme: stream_schemes.first().cloned(),
1075 stream_schemes,
1076 site_scheme,
1077 site_policy: cfg
1078 .site
1079 .as_ref()
1080 .map(|(_, p)| p.clone())
1081 .unwrap_or_default(),
1082 modules: cfg.modules,
1083 clipboard: cfg.clipboard,
1084 clipboard_options: cfg.clipboard_options,
1085 clipboard_dir: clipboard::clipboard_temp_dir(&cfg.app_id),
1086 });
1087
1088 if let Some(spec) = &cfg.menu {
1090 let built = menu::build(
1091 app,
1092 &MenuSpec {
1093 app_name: cfg.app_name.clone(),
1094 ..spec.clone()
1095 },
1096 )?;
1097 let state = app.state::<ShellState>();
1098 *state.menu_rows.lock().unwrap_or_else(|e| e.into_inner()) = menu::describe(&built.menu);
1099 *state.menu_items.lock().unwrap_or_else(|e| e.into_inner()) = built.items;
1100 app.set_menu(built.menu)?;
1101 app.on_menu_event(|app, event| lifecycle::on_menu_event(app, event.id().as_ref()));
1102 }
1103
1104 if let Some(spec) = &cfg.tray {
1106 let icon = tray::install(app, spec, &cfg.main_label)?;
1107 let state = app.state::<ShellState>();
1108 if let Some(rt) = &state.tray {
1109 *rt.icon.lock().unwrap_or_else(|e| e.into_inner()) = Some(icon);
1110 }
1111 }
1112
1113 #[cfg(feature = "engine")]
1115 if let Some(engine) = engine {
1116 let handle = app.clone();
1117 std::thread::Builder::new()
1118 .name("rk-shell-engine".into())
1119 .spawn(move || {
1120 use tauri::Emitter;
1121 let payload = match engine.start() {
1122 Ok(hello) => serde_json::json!({"state": "ready", "hello": hello}),
1123 Err(e) => {
1124 serde_json::json!({"state": "error", "code": e.code, "message": e.message})
1125 }
1126 };
1127 engine.set_lifecycle(payload.clone());
1128 let _ = handle.emit(
1129 EVENT_ENGINE,
1130 serde_json::json!({"call": "$lifecycle", "event": payload}),
1131 );
1132 })?;
1133 }
1134
1135 if cfg.modules.contains(Module::Watchdog) {
1137 if let Some(w) = cfg.watchdog.clone() {
1138 watchdog::install(app.clone(), w);
1139 }
1140 }
1141
1142 let argv: Vec<String> = std::env::args().skip(1).collect();
1144 let state = app.state::<ShellState>();
1145 state
1146 .deep
1147 .push_all(argv.iter().map(String::as_str), &state.schemes);
1148 notify::set_default_app_id(&app.config().identifier);
1149 open_url::deliver(&argv);
1150 Ok(())
1151}
1152
1153#[macro_export]
1156macro_rules! handler {
1157 ($($app_cmd:path),* $(,)?) => {
1158 ::tauri::generate_handler![
1159 $crate::commands::rk_shell_open_external,
1160 $crate::commands::rk_shell_ready,
1161 $crate::commands::rk_shell_status,
1162 $crate::commands::rk_shell_window,
1163 $crate::commands::rk_shell_open_window,
1164 $crate::commands::rk_shell_set_tray,
1165 $crate::commands::rk_shell_set_menu_item_enabled,
1166 $crate::commands::rk_shell_autostart,
1167 $crate::commands::rk_shell_clipboard_read,
1168 $crate::commands::rk_shell_clipboard_write,
1169 $crate::commands::rk_shell_engine_call,
1170 $crate::commands::rk_shell_traffic_lights,
1171 $crate::commands::rk_shell_quit,
1172 $crate::commands::rk_shell_finish_quit,
1173 $crate::commands::rk_shell_capabilities,
1174 $crate::commands::rk_shell_set_theme,
1175 $crate::commands::rk_shell_notify,
1176 $crate::commands::rk_shell_notify_status,
1177 $crate::commands::rk_shell_notify_clear,
1178 $crate::commands::rk_shell_badge,
1179 $crate::commands::rk_shell_set_dnd,
1180 $crate::commands::rk_shell_presence,
1181 $crate::commands::rk_shell_presence_prefs,
1182 $crate::commands::rk_shell_presence_probe,
1183 $crate::commands::rk_shell_presence_evaluate,
1184 $crate::commands::rk_shell_idle_status,
1185 $crate::commands::rk_shell_idle_note,
1186 $crate::commands::rk_shell_path_require,
1187 $crate::commands::rk_shell_path_revoke,
1188 $crate::commands::rk_shell_path_grant_recent,
1189 $crate::commands::rk_shell_path_regrant_saved,
1190 $crate::commands::rk_shell_stream_grant,
1191 $crate::commands::rk_shell_recovery_save,
1192 $crate::commands::rk_shell_recovery_load,
1193 $crate::commands::rk_shell_recovery_clear,
1194 $crate::commands::rk_shell_clipboard_item,
1195 $crate::commands::rk_shell_site_grant,
1196 $crate::commands::rk_shell_site_revoke
1197 $(, $app_cmd)*
1198 ]
1199 };
1200}