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 login_item;
35pub mod menu;
36pub mod modules;
37pub mod nav;
38pub mod notify;
39pub mod open_url;
40pub mod opener;
41pub mod presence;
42pub mod quit;
43pub mod recovery;
44pub mod single_instance;
45pub mod site;
46pub mod startup_cache;
47pub mod stream;
48pub mod tray;
49pub mod watchdog;
50pub mod window_state;
51#[cfg(all(feature = "qa-probes", not(debug_assertions)))]
52compile_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");
53#[cfg(windows)]
55pub(crate) mod winapi {
56 #[cfg(not(any(feature = "wv2-0_38", feature = "wv2-0_39")))]
57 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\"]");
58 #[cfg(all(feature = "wv2-0_38", not(feature = "wv2-0_39")))]
59 pub use ::{webview2_com_038 as webview2_com, windows_061 as windows};
60 #[cfg(feature = "wv2-0_39")]
61 pub use ::{webview2_com_039 as webview2_com, windows_062 as windows};
62}
63pub mod windows;
64
65use std::collections::HashMap;
66use std::path::PathBuf;
67use std::sync::atomic::AtomicBool;
68use std::sync::{Arc, Mutex};
69
70use tauri::menu::MenuItem;
71use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
72use tauri::{AppHandle, Manager, Wry};
73
74pub use autostart::{AutostartMigration, AutostartOptions, LegacyAutostartNames, MacosBackend};
75pub use broker::PathBroker;
76pub use clipboard::{ClipboardItem, ClipboardOptions, ClipboardPolicy, ImagePreference};
77pub use devtools::{ContentWebview, DevToolsError, DevToolsGate, DevToolsPolicy, WebviewDecision};
78pub use error::ShellError;
79pub use harden::{Hardening, HardeningScript};
80pub use idle::{
81 IdleErrorCallback, IdleFailure, IdlePhase, IdleTransition, IdleTransitionCallback,
82 IdleTransitionKind, IdleTrigger,
83};
84pub use login_item::LoginItemStatus;
85pub use menu::{MenuEntry, MenuGroup, MenuSpec};
86pub use modules::{Module, ModuleSet, ShellPlugin, ShellPluginBuilder};
87pub use nav::{NavDecision, NavPolicy};
88pub use opener::{ExternalOpener, SystemOpener};
89pub use presence::{AutoPrefs, PresenceState};
90pub use recovery::{MigrationOutcome, RecoverySpec};
91pub use single_instance::{
92 ClaimFailure, ClaimOptions, ClaimOutcome, InstanceMessage, LegacySingleInstanceNames,
93 SingleInstance, SingleInstanceGuard,
94};
95pub use site::SitePolicy;
96pub use stream::{StreamEvent, StreamEventKind, StreamObserver, StreamPolicy};
97pub use tray::{LeftClick, TraySpec};
98pub use watchdog::{ShellRuntimeStatus, StallReport, WatchdogConfig, WatchdogStatus};
99
100pub const EVENT_MENU: &str = "rightkit-shell://menu";
101pub const EVENT_TRAY: &str = "rightkit-shell://tray";
102pub const EVENT_DEEP_LINK: &str = "rightkit-shell://deep-link";
103pub const EVENT_SECOND_INSTANCE: &str = "rightkit-shell://second-instance";
104pub const EVENT_STALL: &str = "rightkit-shell://stall";
105pub const EVENT_ENGINE: &str = "rightkit-shell://engine";
106pub const EVENT_WINDOW: &str = "rightkit-shell://window";
107pub const EVENT_IDLE: &str = "rightkit-shell://idle";
108pub const EVENT_PRESENCE: &str = "rightkit-shell://presence";
109pub const EVENT_QUIT_REQUESTED: &str = "rightkit-shell://quit-requested";
111
112type MenuCallback = Arc<dyn Fn(&AppHandle<Wry>, &str) + Send + Sync>;
113type PresenceCallback = Arc<dyn Fn(&AppHandle<Wry>, PresenceState) + Send + Sync>;
114type VisibilityCallback = Arc<dyn Fn(&AppHandle<Wry>, lifecycle::Visibility) + Send + Sync>;
115type RevealDecider = lifecycle::RevealDecider<AppHandle<Wry>>;
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
119pub enum CloseBehavior {
120 #[default]
122 Quit,
123 HideToTray,
125 HideToBackground,
128}
129
130pub struct ShellState {
132 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
134 pub(crate) main_thread_id: std::thread::ThreadId,
135 pub app_id: String,
136 pub app_name: String,
137 pub main_label: String,
138 pub nav: Arc<NavPolicy>,
139 pub opener: Arc<dyn ExternalOpener>,
140 pub frameless: bool,
141 pub close_to_tray: bool,
142 pub hide_to_background: bool,
144 pub on_visibility: Option<VisibilityCallback>,
145 pub(crate) activation: lifecycle::ActivationTracker,
146 pub show_policy: lifecycle::ShowPolicy,
148 pub quit: Arc<quit::QuitGate>,
150 pub windows: Arc<window_state::WindowStateStore>,
151 pub deep: deep_link::DeepLinkQueue,
152 pub schemes: Vec<String>,
153 pub frontend_ready: AtomicBool,
154 pub exiting: AtomicBool,
155 pub traffic: frameless::TrafficLights,
156 pub autostart: autostart::AutostartSpec,
157 pub menu_items: Mutex<HashMap<String, MenuItem<Wry>>>,
158 pub menu_rows: Mutex<Vec<(String, String)>>,
159 pub tray: Option<tray::TrayRuntime>,
160 pub on_menu: Option<MenuCallback>,
161 #[cfg(feature = "engine")]
162 pub engine: Option<Arc<engine::EngineHost>>,
163 pub hosted: Mutex<Option<rightkit_daemon::Hosted>>,
164 pub single_instance: Mutex<String>,
165 pub broker: Arc<PathBroker>,
167 pub notifications: notify::NotificationState,
168 pub presence: Arc<presence::PresenceDnd>,
169 pub idle: Option<idle::IdleHost>,
170 pub recovery: Option<recovery::RecoveryStore>,
171 pub cache_repair: Mutex<Option<(PathBuf, recovery::CacheDirOutcome)>>,
173 pub stream_scheme: Option<String>,
175 pub stream_schemes: Vec<String>,
177 pub site_scheme: Option<String>,
179 pub site_policy: site::SitePolicy,
180 pub modules: ModuleSet,
182 pub clipboard: clipboard::ClipboardPolicy,
183 pub clipboard_options: clipboard::ClipboardOptions,
185 pub clipboard_dir: PathBuf,
187}
188
189pub type SecondInstanceCallback = Arc<dyn Fn(&AppHandle<Wry>, &[String], &str) + Send + Sync>;
191
192pub struct ShellBuilder {
193 app_id: String,
194 app_name: String,
195 main_label: String,
196 hardening: Hardening,
197 hardening_script: HardeningScript,
198 devtools_policy: DevToolsPolicy,
199 content_webviews: Vec<ContentWebview>,
200 nav: NavPolicy,
201 extra_internal_schemes: Vec<String>,
202 opener: Arc<dyn ExternalOpener>,
203 single_instance: bool,
204 close_to_tray: bool,
205 hide_to_background: bool,
206 on_visibility: Option<VisibilityCallback>,
207 never_show: bool,
208 never_show_env: Vec<String>,
209 never_show_env_values: Vec<(String, String)>,
210 reveal_on_reopen: bool,
211 lifecycle_only: bool,
212 qa_instance_exempt: bool,
213 on_second_instance_reveal: Option<RevealDecider>,
214 quit_drain: Option<std::time::Duration>,
215 frameless: bool,
216 show_on_ready: Option<bool>,
217 state_dir: Option<PathBuf>,
218 schemes: Vec<String>,
219 menu: Option<MenuSpec>,
220 tray: Option<TraySpec>,
221 watchdog: Option<WatchdogConfig>,
222 on_menu: Option<MenuCallback>,
223 on_second_instance: Option<SecondInstanceCallback>,
224 streams: Vec<(String, StreamPolicy)>,
225 site: Option<(String, site::SitePolicy)>,
226 modules: ModuleSet,
227 clipboard: clipboard::ClipboardPolicy,
228 clipboard_options: clipboard::ClipboardOptions,
229 idle_on_error: Option<idle::IdleErrorCallback>,
230 idle_on_transition: Option<idle::IdleTransitionCallback>,
231 autostart: autostart::AutostartOptions,
232 recovery: Option<RecoverySpec>,
233 idle_after: Option<std::time::Duration>,
234 idle_options: Option<idle::IdleOptions>,
235 cache_dir: Option<PathBuf>,
236 repair_cache: Option<bool>,
237 on_presence: Option<PresenceCallback>,
238 #[cfg(feature = "engine")]
239 engine: Option<engine::EngineSpec>,
240 #[cfg(feature = "engine")]
241 engine_policy: engine::EnginePolicy,
242}
243
244pub struct Shell {
245 cfg: ShellBuilder,
246}
247
248impl Shell {
249 pub fn builder(app_id: &str) -> ShellBuilder {
252 ShellBuilder {
253 app_id: app_id.to_owned(),
254 app_name: app_id.to_owned(),
255 main_label: "main".into(),
256 hardening: Hardening::default(),
257 hardening_script: HardeningScript::default(),
258 devtools_policy: DevToolsPolicy::default(),
259 content_webviews: Vec::new(),
260 nav: NavPolicy::default(),
261 extra_internal_schemes: Vec::new(),
262 opener: Arc::new(SystemOpener),
263 single_instance: true,
264 close_to_tray: false,
265 hide_to_background: false,
266 on_visibility: None,
267 never_show: false,
268 never_show_env: Vec::new(),
269 never_show_env_values: Vec::new(),
270 reveal_on_reopen: true,
271 lifecycle_only: false,
272 qa_instance_exempt: false,
273 on_second_instance_reveal: None,
274 quit_drain: None,
275 frameless: false,
276 show_on_ready: None,
277 state_dir: None,
278 schemes: Vec::new(),
279 menu: None,
280 tray: None,
281 watchdog: Some(WatchdogConfig::default()),
282 on_menu: None,
283 on_second_instance: None,
284 streams: Vec::new(),
285 site: None,
286 modules: ModuleSet::all(),
287 clipboard: clipboard::ClipboardPolicy::default(),
288 clipboard_options: clipboard::ClipboardOptions::default(),
289 idle_on_error: None,
290 idle_on_transition: None,
291 autostart: autostart::AutostartOptions::default(),
292 recovery: None,
293 idle_after: None,
294 idle_options: None,
295 cache_dir: None,
296 repair_cache: None,
297 on_presence: None,
298 #[cfg(feature = "engine")]
299 engine: None,
300 #[cfg(feature = "engine")]
301 engine_policy: Default::default(),
302 }
303 }
304}
305
306impl ShellBuilder {
307 pub fn app_name(mut self, name: &str) -> Self {
308 self.app_name = name.to_owned();
309 self
310 }
311 pub fn main_label(mut self, label: &str) -> Self {
312 self.main_label = label.to_owned();
313 self
314 }
315 pub fn hardening(mut self, h: Hardening) -> Self {
316 self.hardening = h;
317 self
318 }
319 pub fn hardening_script(mut self, scope: HardeningScript) -> Self {
322 self.hardening_script = scope;
323 self
324 }
325 pub fn devtools_policy(mut self, p: DevToolsPolicy) -> Self {
330 self.devtools_policy = p;
331 self
332 }
333 pub fn content_webview(mut self, c: ContentWebview) -> Self {
337 self.content_webviews.push(c);
338 self
339 }
340 pub fn nav_policy(mut self, p: NavPolicy) -> Self {
341 self.nav = p;
342 self
343 }
344 pub fn internal_scheme(mut self, scheme: &str) -> Self {
351 nav::validate_internal_scheme(scheme).expect("rightkit-tauri-shell: internal_scheme");
352 self.extra_internal_schemes.push(scheme.to_owned());
353 self
354 }
355 pub fn opener(mut self, o: impl ExternalOpener) -> Self {
357 self.opener = Arc::new(o);
358 self
359 }
360 pub fn single_instance(mut self, on: bool) -> Self {
361 self.single_instance = on;
362 self
363 }
364 pub fn close_to_tray(mut self, on: bool) -> Self {
366 self.close_to_tray = on;
367 if !on {
368 self.hide_to_background = false;
369 }
370 self
371 }
372 pub fn close_behavior(mut self, behavior: CloseBehavior) -> Self {
377 self.close_to_tray = behavior != CloseBehavior::Quit;
378 self.hide_to_background = behavior == CloseBehavior::HideToBackground;
379 self
380 }
381 pub fn on_visibility(
386 mut self,
387 f: impl Fn(&AppHandle<Wry>, lifecycle::Visibility) + Send + Sync + 'static,
388 ) -> Self {
389 self.on_visibility = Some(Arc::new(f));
390 self
391 }
392 pub fn never_show(mut self, on: bool) -> Self {
399 self.never_show = on;
400 self
401 }
402 pub fn never_show_env(mut self, name: &str) -> Self {
410 assert!(
411 !name.is_empty() && !name.contains(['=', '\0']),
412 "rightkit-tauri-shell: never_show_env needs a plain environment variable name"
413 );
414 if !self.never_show_env.iter().any(|n| n == name) {
415 self.never_show_env.push(name.to_owned());
416 }
417 self
418 }
419 pub fn never_show_env_value(mut self, name: &str, expected: &str) -> Self {
426 assert!(
427 !name.is_empty() && !name.contains(['=', '\0']) && !expected.is_empty(),
428 "rightkit-tauri-shell: never_show_env_value needs a plain variable name and a non-empty expected value"
429 );
430 let pair = (name.to_owned(), expected.to_owned());
431 if !self.never_show_env_values.contains(&pair) {
432 self.never_show_env_values.push(pair);
433 }
434 self
435 }
436 pub fn reveal_on_reopen(mut self, on: bool) -> Self {
441 self.reveal_on_reopen = on;
442 self
443 }
444 pub fn qa_instance_exempt(mut self, on: bool) -> Self {
448 self.qa_instance_exempt = on;
449 self
450 }
451 pub fn second_instance_reveal(
455 mut self,
456 f: impl Fn(&AppHandle<Wry>, &[String], &str) -> bool + Send + Sync + 'static,
457 ) -> Self {
458 self.on_second_instance_reveal = Some(Arc::new(f));
459 self
460 }
461 pub fn quit_drain(mut self, on: bool) -> Self {
467 self.quit_drain = on.then_some(quit::DEFAULT_QUIT_DRAIN_BOUND);
468 self
469 }
470 pub fn quit_drain_bound(mut self, bound: std::time::Duration) -> Self {
474 self.quit_drain = Some(bound);
475 self
476 }
477 pub fn frameless(mut self, on: bool) -> Self {
479 self.frameless = on;
480 self
481 }
482 pub fn show_on_ready(mut self, on: bool) -> Self {
485 self.show_on_ready = Some(on);
486 self
487 }
488 pub fn state_dir(mut self, dir: PathBuf) -> Self {
490 self.state_dir = Some(dir);
491 self
492 }
493 pub fn deep_link_scheme(mut self, scheme: &str) -> Self {
495 self.schemes.push(scheme.to_ascii_lowercase());
496 self
497 }
498 pub fn menu(mut self, m: MenuSpec) -> Self {
499 self.menu = Some(m);
500 self
501 }
502 pub fn tray(mut self, t: TraySpec) -> Self {
503 self.tray = Some(t);
504 self
505 }
506 pub fn autostart_options(mut self, options: autostart::AutostartOptions) -> Self {
509 self.autostart = options;
510 self
511 }
512 pub fn on_stall(mut self, f: impl Fn(&watchdog::StallReport) + Send + Sync + 'static) -> Self {
516 if let Some(config) = self.watchdog.as_mut() {
517 config.on_stall = Some(Arc::new(f));
518 }
519 self
520 }
521 pub fn watchdog(mut self, w: Option<WatchdogConfig>) -> Self {
522 self.watchdog = w;
523 self
524 }
525 pub fn on_second_instance(
531 mut self,
532 f: impl Fn(&AppHandle<Wry>, &[String], &str) + Send + Sync + 'static,
533 ) -> Self {
534 self.on_second_instance = Some(Arc::new(f));
535 self
536 }
537 pub fn on_menu(mut self, f: impl Fn(&AppHandle<Wry>, &str) + Send + Sync + 'static) -> Self {
538 self.on_menu = Some(Arc::new(f));
539 self
540 }
541 pub fn stream_protocol(mut self, scheme: &str, policy: StreamPolicy) -> Self {
544 let scheme = scheme.to_ascii_lowercase();
545 self.streams.retain(|(s, _)| *s != scheme);
546 self.streams.push((scheme, policy));
547 self
548 }
549 pub fn site_protocol(mut self, scheme: &str, policy: site::SitePolicy) -> Self {
552 self.site = Some((scheme.to_ascii_lowercase(), policy));
553 self
554 }
555 pub fn clipboard_purge_after(mut self, age: std::time::Duration) -> Self {
557 self.clipboard.purge_after = age;
558 self
559 }
560 pub fn clipboard_options(mut self, options: clipboard::ClipboardOptions) -> Self {
564 self.clipboard_options = options;
565 self
566 }
567 pub fn recovery(mut self, spec: RecoverySpec) -> Self {
569 self.recovery = Some(spec);
570 self
571 }
572 pub fn webview_idle(mut self, suspend_after: std::time::Duration) -> Self {
574 self.idle_after = Some(suspend_after);
575 self
576 }
577 pub fn webview_idle_options(mut self, options: idle::IdleOptions) -> Self {
583 self.idle_after = Some(options.suspend_after);
584 self.idle_options = Some(options);
585 self
586 }
587 pub fn webview_idle_on_error(
590 mut self,
591 f: impl Fn(&idle::IdleFailure) + Send + Sync + 'static,
592 ) -> Self {
593 self.idle_on_error = Some(Arc::new(f));
594 self
595 }
596 pub fn webview_idle_on_transition(
599 mut self,
600 f: impl Fn(&idle::IdleTransition) + Send + Sync + 'static,
601 ) -> Self {
602 self.idle_on_transition = Some(Arc::new(f));
603 self
604 }
605 pub fn cache_dir(mut self, dir: PathBuf) -> Self {
607 self.cache_dir = Some(dir);
608 self
609 }
610 pub fn repair_cache(mut self, on: bool) -> Self {
613 self.repair_cache = Some(on);
614 self
615 }
616 pub fn on_presence(
618 mut self,
619 f: impl Fn(&AppHandle<Wry>, PresenceState) + Send + Sync + 'static,
620 ) -> Self {
621 self.on_presence = Some(Arc::new(f));
622 self
623 }
624 #[cfg(feature = "engine")]
627 pub fn engine_with_policy(
628 mut self,
629 spec: engine::EngineSpec,
630 policy: engine::EnginePolicy,
631 ) -> Self {
632 self.engine = Some(spec);
633 self.engine_policy = policy;
634 self
635 }
636 pub fn build(self) -> Shell {
637 assert!(
638 !self.app_id.is_empty()
639 && self.app_id.len() <= 40
640 && self
641 .app_id
642 .chars()
643 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
644 "rightkit-tauri-shell: app_id must be lowercase letters, digits and '-' (<= 40 chars)"
645 );
646 Shell { cfg: self }
647 }
648}
649
650impl Shell {
651 pub fn single_instance_plugin(
657 self,
658 on_second_instance: impl Fn(&AppHandle<Wry>, &[String], &str) + Send + Sync + 'static,
659 ) -> TauriPlugin<Wry> {
660 let app_id = self.cfg.app_id;
661 PluginBuilder::<Wry>::new("rightkit-single-instance")
662 .setup(move |app, _api| {
663 let handle = app.clone();
664 let version = app.package_info().version.to_string();
665 let identifier = app.config().identifier.clone();
666 let arbitration = instance::arbitrate_with_legacy(
667 &app_id,
668 &version,
669 Some(identifier.as_str()),
670 Arc::new(move |second| {
671 on_second_instance(&handle, &second.argv, &second.cwd);
672 }),
673 );
674 match arbitration {
675 instance::Arbitration::Primary(hosted) => {
676 app.manage(PluginClaim(single_instance::hold(hosted)));
677 }
678 instance::Arbitration::Secondary => std::process::exit(0),
679 instance::Arbitration::Unavailable(why) => {
681 return Err(unavailable_error(&why));
682 }
683 instance::Arbitration::InvalidAppId(e) => return Err(invalid_app_id_error(&e)),
684 }
685 Ok(())
686 })
687 .on_event(|app, event| {
688 if matches!(event, tauri::RunEvent::Exit) {
689 if let Some(claim) = app.try_state::<PluginClaim>() {
690 single_instance::release(claim.0);
691 }
692 }
693 })
694 .build()
695 }
696
697 pub fn lifecycle_plugin(self) -> TauriPlugin<Wry> {
703 ShellPlugin::builder().shell(self).lifecycle_only()
704 }
705
706 pub fn plugin(self) -> TauriPlugin<Wry> {
710 ShellPlugin::builder().shell(self).build()
711 }
712}
713
714pub(crate) fn install(cfg: ShellBuilder) -> TauriPlugin<Wry> {
717 let modules = cfg.modules;
718 let mut nav_policy = cfg.nav.clone();
719 for scheme in &cfg.extra_internal_schemes {
720 nav_policy = nav_policy
721 .with_internal_scheme(scheme)
722 .expect("rightkit-tauri-shell: internal_scheme");
723 }
724 let nav = Arc::new(nav_policy);
725 let opener = Arc::clone(&cfg.opener);
726 let hardening = cfg.hardening.clone();
727 let script_scope = cfg.hardening_script;
728 let (nav_hook, opener_hook) = (Arc::clone(&nav), Arc::clone(&opener));
729 let hardening_for_views = hardening.clone();
730 let guard_result = devtools::guard_process_start();
733 let gate_result = devtools::build_gate(
734 cfg.devtools_policy,
735 &cfg.main_label,
736 &cfg.content_webviews,
737 cfg!(feature = "content-devtools"),
738 );
739 let gate_for_views = gate_result
740 .clone()
741 .unwrap_or_else(|_| DevToolsGate::denied(&cfg.main_label));
742 let content_labels = gate_for_views.content_labels();
743 let harden_webviews = modules.contains(Module::JsHardening);
744 let show_on_ready_explicit = cfg.show_on_ready;
745 let show_on_ready_module = lifecycle::show_on_ready_installed(
748 modules.contains(Module::ShowOnReady),
749 cfg.lifecycle_only,
750 show_on_ready_explicit,
751 );
752 let ready_policy = show_policy_of(&cfg);
753 let restore_windows = modules.contains(Module::WindowState);
754 let main_label = cfg.main_label.clone();
755 let (streams, site) = if modules.contains(Module::Stream) {
756 (cfg.streams.clone(), cfg.site.clone())
757 } else {
758 (Vec::new(), None)
759 };
760
761 let mut plugin = PluginBuilder::<Wry>::new("rightkit-shell");
762 plugin = plugin.on_page_load(|webview, payload| {
765 if matches!(payload.event(), tauri::webview::PageLoadEvent::Finished) {
766 if let Err(e) = devtools::verify_after_first_navigation(webview.app_handle()) {
767 eprintln!("{e}");
768 webview.app_handle().exit(1);
769 }
770 }
771 });
772 if modules.contains(Module::NavigationPolicy) {
773 plugin = plugin.on_navigation(move |_webview, url| match nav_hook.decide(url) {
774 NavDecision::Allow => true,
775 NavDecision::External { url } => {
776 let _ = opener::open_checked(opener_hook.as_ref(), &url);
777 false
778 }
779 NavDecision::Block => false,
780 });
781 }
782 plugin = plugin
783 .on_webview_ready(move |webview| {
784 let decision = gate_for_views.decide(webview.label());
785 if devtools::apply_native(&webview, decision).is_err() {
789 return;
790 }
791 if harden_webviews {
792 harden::apply_native(&webview, &hardening_for_views, decision);
793 }
794 if let Err(e) = devtools::verify_not_remote_debuggable(webview.app_handle()) {
795 eprintln!("{e}");
796 webview.app_handle().exit(1);
797 return;
798 }
799 let app = webview.app_handle().clone();
800 let label = webview.label().to_owned();
801 lifecycle::enforce_never_show(&app, &ready_policy);
802 windows::with_placement_announced(&app, &label, || {
803 if restore_windows {
804 if let Some(state) = app.try_state::<ShellState>() {
805 let _ = state.windows.restore(&app, &label);
806 }
807 }
808 });
809 if label == main_label
810 && lifecycle::show_on_ready_decision(
811 show_on_ready_explicit,
812 show_on_ready_module,
813 lifecycle::launched_in_background(),
814 lifecycle::qa_hidden(),
815 )
816 {
817 if let Some(w) = app.get_webview_window(&label) {
818 let _ = crate::windows::show_window(
820 &app,
821 &w,
822 lifecycle::RevealSource::ShowOnReady,
823 windows::RevealStyle::ShowOnly,
824 );
825 }
826 }
827 })
828 .on_event(|app, event| {
829 if app.try_state::<ShellState>().is_some() {
830 lifecycle::run_event(app, event);
831 }
832 })
833 .setup(move |app, _api| {
834 let app = app.clone();
835 if let Err(e) = guard_result {
836 eprintln!("{e}");
837 return Err(Box::new(e));
838 }
839 match gate_result {
840 Ok(gate) => {
841 app.manage(gate);
842 }
843 Err(e) => {
844 eprintln!("{e}");
845 return Err(Box::new(e));
846 }
847 }
848 setup(&app, cfg, nav, opener)
849 });
850 if harden_webviews {
851 plugin = match script_scope {
852 HardeningScript::AllFrames => {
853 plugin.js_init_script_on_all_frames(hardening.script_for(&content_labels))
854 }
855 HardeningScript::TopFrameOnly => plugin.js_init_script(format!(
858 "if (window.top !== window.self) {{ return; }}\n{}",
859 hardening.script_for(&content_labels)
860 )),
861 HardeningScript::Off => plugin,
862 };
863 }
864 for (scheme, policy) in streams {
865 plugin = plugin.register_asynchronous_uri_scheme_protocol(
866 scheme,
867 move |ctx, request, responder| {
868 let state = ctx.app_handle().state::<ShellState>();
869 stream::serve(&state.broker, &policy, &request, move |response| {
870 responder.respond(response)
871 });
872 },
873 );
874 }
875 if let Some((scheme, policy)) = site {
876 plugin = plugin.register_asynchronous_uri_scheme_protocol(
877 scheme,
878 move |ctx, request, responder| {
879 let state = ctx.app_handle().state::<ShellState>();
880 site::serve_site_request(&state.broker, &policy, &request, move |response| {
881 responder.respond(response)
882 });
883 },
884 );
885 }
886 plugin.build()
887}
888
889struct PluginClaim(u64);
891
892fn unavailable_error(why: &str) -> Box<dyn std::error::Error> {
894 format!("rightkit-tauri-shell: single-instance could not be decided, refusing to start as a second primary: {why}").into()
895}
896
897fn invalid_app_id_error(e: &instance::InvalidAppId) -> Box<dyn std::error::Error> {
900 format!("rightkit-tauri-shell: single-instance cannot start, app id is not usable: {e}").into()
901}
902
903fn show_policy_of(cfg: &ShellBuilder) -> lifecycle::ShowPolicy {
905 lifecycle::ShowPolicy {
906 never_show: cfg.never_show,
907 never_show_env: cfg.never_show_env.clone(),
908 never_show_env_values: cfg.never_show_env_values.clone(),
909 reveal_on_reopen: cfg.reveal_on_reopen && cfg.modules.contains(Module::RevealOnReopen),
910 }
911}
912
913fn setup(
914 app: &AppHandle<Wry>,
915 cfg: ShellBuilder,
916 nav: Arc<NavPolicy>,
917 opener: Arc<dyn ExternalOpener>,
918) -> Result<(), Box<dyn std::error::Error>> {
919 let show_policy = show_policy_of(&cfg);
921 lifecycle::enforce_never_show(app, &show_policy);
922 let mut single = String::from("disabled");
924 let mode = lifecycle::instance_mode(
925 cfg.single_instance,
926 cfg.qa_instance_exempt,
927 lifecycle::qa_hidden(),
928 );
929 if mode == lifecycle::InstanceMode::QaExempt {
930 single = "qa-exempt".into();
931 }
932 if mode == lifecycle::InstanceMode::Arbitrate {
933 let for_cb = app.clone();
934 let custom = cfg.on_second_instance.clone();
935 let decider = cfg.on_second_instance_reveal.clone();
936 let version = app.package_info().version.to_string();
937 let identifier = app.config().identifier.clone();
938 match instance::arbitrate_with_legacy(
939 &cfg.app_id,
940 &version,
941 Some(identifier.as_str()),
942 Arc::new(move |s| {
943 lifecycle::route_second_instance(
944 &for_cb,
945 custom.as_ref(),
946 &s.argv,
947 &s.cwd,
948 |app, argv, cwd| {
949 lifecycle::on_second_instance(app, argv, cwd, decider.as_ref())
950 },
951 )
952 }),
953 ) {
954 instance::Arbitration::Primary(h) => {
955 let _claim = single_instance::hold(h);
957 single = "primary".into();
958 }
959 instance::Arbitration::Secondary => std::process::exit(0),
960 instance::Arbitration::Unavailable(why) => return Err(unavailable_error(&why)),
962 instance::Arbitration::InvalidAppId(e) => return Err(invalid_app_id_error(&e)),
963 }
964 }
965
966 let state_dir = match cfg.state_dir.clone() {
968 Some(d) => d,
969 None => app
970 .path()
971 .app_data_dir()
972 .map(|d| d.join("shell"))
973 .unwrap_or_else(|_| std::env::temp_dir().join(&cfg.app_id)),
974 };
975 let exe = std::env::current_exe().unwrap_or_default();
976 let tray_runtime = cfg.tray.as_ref().map(|t| tray::TrayRuntime {
977 icon: Mutex::new(None),
978 ids: Mutex::new(Default::default()),
979 show_label: t.show_label.clone(),
980 quit_label: t.quit_label.clone(),
981 });
982 #[cfg(feature = "engine")]
983 let engine = cfg.engine.clone().map(|s| {
984 Arc::new(engine::EngineHost::with_policy(
985 s,
986 cfg.engine_policy.clone(),
987 ))
988 });
989 let cache_repair = if lifecycle::repair_cache_enabled(cfg.repair_cache, cfg.lifecycle_only) {
991 let dir = cfg
992 .cache_dir
993 .clone()
994 .or_else(|| app.path().app_cache_dir().ok());
995 dir.and_then(|d| recovery::ensure_cache_dir(&d).ok().map(|o| (d, o)))
996 } else {
997 None
998 };
999 let presence = Arc::new(presence::PresenceDnd::new());
1000 if cfg.modules.contains(Module::Presence) {
1001 let (handle, on_presence) = (app.clone(), cfg.on_presence.clone());
1002 presence::start_loop(Arc::clone(&presence), cfg.app_name.clone(), move |s| {
1003 use tauri::Emitter;
1004 let _ = handle.emit(EVENT_PRESENCE, s);
1005 if let Some(cb) = &on_presence {
1006 cb(&handle, s);
1007 }
1008 });
1009 }
1010 let recovery = if cfg.modules.contains(Module::Recovery) {
1011 cfg.recovery
1012 .clone()
1013 .map(|spec| recovery::RecoveryStore::new(spec, &state_dir))
1014 } else {
1015 None
1016 };
1017 let stream_schemes: Vec<String> = if cfg.modules.contains(Module::Stream) {
1018 cfg.streams.iter().map(|(s, _)| s.clone()).collect()
1019 } else {
1020 Vec::new()
1021 };
1022 let site_scheme = if cfg.modules.contains(Module::Stream) {
1023 cfg.site.as_ref().map(|(s, _)| s.clone())
1024 } else {
1025 None
1026 };
1027 let autostart_spec = autostart::AutostartSpec {
1028 app_id: cfg.app_id.clone(),
1029 app_name: cfg.app_name.clone(),
1030 executable: exe,
1031 options: cfg.autostart.clone(),
1032 legacy_plugin_name: cfg
1033 .autostart
1034 .migrate_from_plugin_entry
1035 .then(|| app.package_info().name.clone()),
1036 };
1037 if cfg.autostart.migrate_from_plugin_entry {
1038 let legacy = app.package_info().name.clone();
1039 match autostart::migrate_plugin_entry(&autostart_spec, &legacy) {
1040 Ok(_) => {}
1041 Err(e) => eprintln!(
1043 "rightkit-tauri-shell: could not migrate the tauri-plugin-autostart login entry: {e}"
1044 ),
1045 }
1046 }
1047 if cfg.autostart.macos_backend == autostart::MacosBackend::ServiceManagement {
1048 if let Err(e) = autostart::migrate_launch_agent(&autostart_spec) {
1050 eprintln!("rightkit-tauri-shell: could not move the LaunchAgent login item to SMAppService: {e}");
1051 }
1052 }
1053 app.manage(ShellState {
1054 main_thread_id: std::thread::current().id(),
1055 app_id: cfg.app_id.clone(),
1056 app_name: cfg.app_name.clone(),
1057 main_label: cfg.main_label.clone(),
1058 nav,
1059 opener,
1060 frameless: cfg.frameless && cfg.modules.contains(Module::Frameless),
1061 close_to_tray: cfg.close_to_tray,
1062 hide_to_background: cfg.hide_to_background,
1063 on_visibility: cfg.on_visibility.clone(),
1064 activation: Default::default(),
1065 show_policy,
1066 quit: Arc::new(quit::QuitGate::new(cfg.quit_drain)),
1067 windows: Arc::new(window_state::WindowStateStore::new(state_dir)),
1068 deep: Default::default(),
1069 schemes: cfg.schemes.clone(),
1070 frontend_ready: AtomicBool::new(false),
1071 exiting: AtomicBool::new(false),
1072 traffic: Default::default(),
1073 autostart: autostart_spec,
1074 menu_items: Mutex::new(HashMap::new()),
1075 menu_rows: Mutex::new(Vec::new()),
1076 tray: tray_runtime,
1077 on_menu: cfg.on_menu.clone(),
1078 #[cfg(feature = "engine")]
1079 engine: engine.clone(),
1080 hosted: Mutex::new(None),
1081 single_instance: Mutex::new(single),
1082 broker: Arc::new(PathBroker::with_app_name(&cfg.app_name)),
1083 notifications: notify::NotificationState::new(),
1084 presence,
1085 idle: cfg.idle_after.map(|d| {
1086 let mut options = cfg.idle_options.unwrap_or_default();
1088 options.suspend_after = d;
1089 idle::IdleHost::new(d)
1090 .with_options(options)
1091 .with_on_error(cfg.idle_on_error.clone())
1092 .with_on_transition(cfg.idle_on_transition.clone())
1093 }),
1094 recovery,
1095 cache_repair: Mutex::new(cache_repair),
1096 stream_scheme: stream_schemes.first().cloned(),
1097 stream_schemes,
1098 site_scheme,
1099 site_policy: cfg
1100 .site
1101 .as_ref()
1102 .map(|(_, p)| p.clone())
1103 .unwrap_or_default(),
1104 modules: cfg.modules,
1105 clipboard: cfg.clipboard,
1106 clipboard_options: cfg.clipboard_options,
1107 clipboard_dir: clipboard::clipboard_temp_dir(&cfg.app_id),
1108 });
1109
1110 if let Some(spec) = &cfg.menu {
1112 let built = menu::build(
1113 app,
1114 &MenuSpec {
1115 app_name: cfg.app_name.clone(),
1116 ..spec.clone()
1117 },
1118 )?;
1119 let state = app.state::<ShellState>();
1120 *state.menu_rows.lock().unwrap_or_else(|e| e.into_inner()) = menu::describe(&built.menu);
1121 *state.menu_items.lock().unwrap_or_else(|e| e.into_inner()) = built.items;
1122 app.set_menu(built.menu)?;
1123 app.on_menu_event(|app, event| lifecycle::on_menu_event(app, event.id().as_ref()));
1124 }
1125
1126 if let Some(spec) = &cfg.tray {
1128 let icon = tray::install(app, spec, &cfg.main_label)?;
1129 let state = app.state::<ShellState>();
1130 if let Some(rt) = &state.tray {
1131 *rt.icon.lock().unwrap_or_else(|e| e.into_inner()) = Some(icon);
1132 }
1133 }
1134
1135 #[cfg(feature = "engine")]
1137 if let Some(engine) = engine {
1138 let handle = app.clone();
1139 let lifecycle_events = engine.subscribe_lifecycle();
1140 let supervised = lifecycle_events.is_some();
1141 if let Some(events) = lifecycle_events {
1142 let handle = app.clone();
1143 let host = Arc::downgrade(&engine);
1144 std::thread::Builder::new()
1145 .name("rk-shell-engine-events".into())
1146 .spawn(move || {
1147 use tauri::Emitter;
1148 loop {
1149 let Some(host) = host.upgrade() else { break };
1150 if host.is_shutting_down() { break; }
1151 match events.recv_timeout(std::time::Duration::from_millis(250)) {
1152 Ok(event) => {
1153 let _ = handle.emit(EVENT_ENGINE, serde_json::json!({
1154 "call": "$lifecycle", "event": host.lifecycle(), "supervisor": event
1155 }));
1156 }
1157 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
1158 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
1159 }
1160 }
1161 })?;
1162 }
1163 if let Some(events) = engine.subscribe_events() {
1164 let handle = app.clone();
1165 let host = Arc::downgrade(&engine);
1166 std::thread::Builder::new()
1167 .name("rk-shell-engine-unsolicited".into())
1168 .spawn(move || {
1169 use tauri::Emitter;
1170 loop {
1171 let Some(host) = host.upgrade() else { break };
1172 if host.is_shutting_down() {
1173 break;
1174 }
1175 match events.recv_timeout(std::time::Duration::from_millis(250)) {
1176 Ok(frame) => {
1177 let _ = handle.emit(
1178 EVENT_ENGINE,
1179 engine::EngineEvent {
1180 call: "$unsolicited".into(),
1181 id: None,
1182 tag: None,
1183 event: frame["event"].clone(),
1184 },
1185 );
1186 }
1187 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
1188 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
1189 }
1190 }
1191 })?;
1192 }
1193 std::thread::Builder::new()
1194 .name("rk-shell-engine".into())
1195 .spawn(move || {
1196 use tauri::Emitter;
1197 let payload = match engine.start() {
1198 Ok(hello) => serde_json::json!({"state": "ready", "hello": hello}),
1199 Err(e) => {
1200 serde_json::json!({"state": "error", "code": e.code, "message": e.message})
1201 }
1202 };
1203 engine.set_lifecycle(payload.clone());
1204 if !supervised {
1205 let _ = handle.emit(
1206 EVENT_ENGINE,
1207 serde_json::json!({"call": "$lifecycle", "event": payload}),
1208 );
1209 }
1210 })?;
1211 }
1212
1213 if cfg.modules.contains(Module::Watchdog) {
1215 if let Some(w) = cfg.watchdog.clone() {
1216 watchdog::install(app.clone(), w);
1217 }
1218 }
1219
1220 let argv: Vec<String> = std::env::args().skip(1).collect();
1222 let state = app.state::<ShellState>();
1223 state
1224 .deep
1225 .push_all(argv.iter().map(String::as_str), &state.schemes);
1226 notify::set_default_app_id(&app.config().identifier);
1227 open_url::deliver(&argv);
1228 Ok(())
1229}
1230
1231#[macro_export]
1234macro_rules! handler {
1235 ($($app_cmd:path),* $(,)?) => {
1236 ::tauri::generate_handler![
1237 $crate::commands::rk_shell_open_external,
1238 $crate::commands::rk_shell_ready,
1239 $crate::commands::rk_shell_status,
1240 $crate::commands::rk_shell_window,
1241 $crate::commands::rk_shell_open_window,
1242 $crate::commands::rk_shell_set_tray,
1243 $crate::commands::rk_shell_set_menu_item_enabled,
1244 $crate::commands::rk_shell_autostart,
1245 $crate::commands::rk_shell_open_login_items_settings,
1246 $crate::commands::rk_shell_clipboard_read,
1247 $crate::commands::rk_shell_clipboard_write,
1248 $crate::commands::rk_shell_engine_call,
1249 $crate::commands::rk_shell_engine_call_tagged,
1250 $crate::commands::rk_shell_engine_status,
1251 $crate::commands::rk_shell_traffic_lights,
1252 $crate::commands::rk_shell_quit,
1253 $crate::commands::rk_shell_finish_quit,
1254 $crate::commands::rk_shell_capabilities,
1255 $crate::commands::rk_shell_set_theme,
1256 $crate::commands::rk_shell_notify,
1257 $crate::commands::rk_shell_notify_status,
1258 $crate::commands::rk_shell_notify_clear,
1259 $crate::commands::rk_shell_badge,
1260 $crate::commands::rk_shell_set_dnd,
1261 $crate::commands::rk_shell_presence,
1262 $crate::commands::rk_shell_presence_prefs,
1263 $crate::commands::rk_shell_presence_probe,
1264 $crate::commands::rk_shell_presence_evaluate,
1265 $crate::commands::rk_shell_idle_status,
1266 $crate::commands::rk_shell_idle_note,
1267 $crate::commands::rk_shell_path_require,
1268 $crate::commands::rk_shell_path_revoke,
1269 $crate::commands::rk_shell_path_grant_recent,
1270 $crate::commands::rk_shell_path_regrant_saved,
1271 $crate::commands::rk_shell_stream_grant,
1272 $crate::commands::rk_shell_recovery_save,
1273 $crate::commands::rk_shell_recovery_load,
1274 $crate::commands::rk_shell_recovery_clear,
1275 $crate::commands::rk_shell_clipboard_item,
1276 $crate::commands::rk_shell_site_grant,
1277 $crate::commands::rk_shell_site_revoke
1278 $(, $app_cmd)*
1279 ]
1280 };
1281}