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")]
625 pub fn engine(mut self, spec: engine::EngineSpec) -> Self {
626 self.engine = Some(spec);
627 self.engine_policy = Default::default();
628 self
629 }
630 #[cfg(feature = "engine")]
632 pub fn engine_with_policy(
633 mut self,
634 spec: engine::EngineSpec,
635 policy: engine::EnginePolicy,
636 ) -> Self {
637 self.engine = Some(spec);
638 self.engine_policy = policy;
639 self
640 }
641 pub fn build(self) -> Shell {
642 assert!(
643 !self.app_id.is_empty()
644 && self.app_id.len() <= 40
645 && self
646 .app_id
647 .chars()
648 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
649 "rightkit-tauri-shell: app_id must be lowercase letters, digits and '-' (<= 40 chars)"
650 );
651 Shell { cfg: self }
652 }
653}
654
655impl Shell {
656 pub fn single_instance_plugin(
662 self,
663 on_second_instance: impl Fn(&AppHandle<Wry>, &[String], &str) + Send + Sync + 'static,
664 ) -> TauriPlugin<Wry> {
665 let app_id = self.cfg.app_id;
666 PluginBuilder::<Wry>::new("rightkit-single-instance")
667 .setup(move |app, _api| {
668 let handle = app.clone();
669 let version = app.package_info().version.to_string();
670 let identifier = app.config().identifier.clone();
671 let arbitration = instance::arbitrate_with_legacy(
672 &app_id,
673 &version,
674 Some(identifier.as_str()),
675 Arc::new(move |second| {
676 on_second_instance(&handle, &second.argv, &second.cwd);
677 }),
678 );
679 match arbitration {
680 instance::Arbitration::Primary(hosted) => {
681 app.manage(PluginClaim(single_instance::hold(hosted)));
682 }
683 instance::Arbitration::Secondary => std::process::exit(0),
684 instance::Arbitration::Unavailable(why) => {
686 return Err(unavailable_error(&why));
687 }
688 instance::Arbitration::InvalidAppId(e) => return Err(invalid_app_id_error(&e)),
689 }
690 Ok(())
691 })
692 .on_event(|app, event| {
693 if matches!(event, tauri::RunEvent::Exit) {
694 if let Some(claim) = app.try_state::<PluginClaim>() {
695 single_instance::release(claim.0);
696 }
697 }
698 })
699 .build()
700 }
701
702 pub fn lifecycle_plugin(self) -> TauriPlugin<Wry> {
708 ShellPlugin::builder().shell(self).lifecycle_only()
709 }
710
711 pub fn plugin(self) -> TauriPlugin<Wry> {
715 ShellPlugin::builder().shell(self).build()
716 }
717}
718
719pub(crate) fn install(cfg: ShellBuilder) -> TauriPlugin<Wry> {
722 let modules = cfg.modules;
723 let mut nav_policy = cfg.nav.clone();
724 for scheme in &cfg.extra_internal_schemes {
725 nav_policy = nav_policy
726 .with_internal_scheme(scheme)
727 .expect("rightkit-tauri-shell: internal_scheme");
728 }
729 let nav = Arc::new(nav_policy);
730 let opener = Arc::clone(&cfg.opener);
731 let hardening = cfg.hardening.clone();
732 let script_scope = cfg.hardening_script;
733 let (nav_hook, opener_hook) = (Arc::clone(&nav), Arc::clone(&opener));
734 let hardening_for_views = hardening.clone();
735 let guard_result = devtools::guard_process_start();
738 let gate_result = devtools::build_gate_with(
739 cfg.devtools_policy,
740 &cfg.main_label,
741 &cfg.content_webviews,
742 cfg!(feature = "content-devtools"),
743 devtools::DEV_INSPECT_COMPILED,
744 );
745 let gate_for_views = gate_result
746 .clone()
747 .unwrap_or_else(|_| DevToolsGate::denied(&cfg.main_label));
748 let content_labels = gate_for_views.content_labels();
749 let harden_webviews = modules.contains(Module::JsHardening);
750 let show_on_ready_explicit = cfg.show_on_ready;
751 let show_on_ready_module = lifecycle::show_on_ready_installed(
754 modules.contains(Module::ShowOnReady),
755 cfg.lifecycle_only,
756 show_on_ready_explicit,
757 );
758 let ready_policy = show_policy_of(&cfg);
759 let restore_windows = modules.contains(Module::WindowState);
760 let main_label = cfg.main_label.clone();
761 let (streams, site) = if modules.contains(Module::Stream) {
762 (cfg.streams.clone(), cfg.site.clone())
763 } else {
764 (Vec::new(), None)
765 };
766
767 let mut plugin = PluginBuilder::<Wry>::new("rightkit-shell");
768 plugin = plugin.on_page_load(|webview, payload| {
771 if matches!(payload.event(), tauri::webview::PageLoadEvent::Finished) {
772 if let Err(e) = devtools::verify_after_first_navigation(webview.app_handle()) {
773 eprintln!("{e}");
774 webview.app_handle().exit(1);
775 }
776 }
777 });
778 if modules.contains(Module::NavigationPolicy) {
779 plugin = plugin.on_navigation(move |_webview, url| match nav_hook.decide(url) {
780 NavDecision::Allow => true,
781 NavDecision::External { url } => {
782 let _ = opener::open_checked(opener_hook.as_ref(), &url);
783 false
784 }
785 NavDecision::Block => false,
786 });
787 }
788 plugin = plugin
789 .on_webview_ready(move |webview| {
790 let decision = gate_for_views.decide(webview.label());
791 if devtools::apply_native(&webview, decision).is_err() {
795 return;
796 }
797 if harden_webviews {
798 harden::apply_native(&webview, &hardening_for_views, decision);
799 }
800 if let Err(e) = devtools::verify_not_remote_debuggable(webview.app_handle()) {
801 eprintln!("{e}");
802 webview.app_handle().exit(1);
803 return;
804 }
805 let app = webview.app_handle().clone();
806 let label = webview.label().to_owned();
807 lifecycle::enforce_never_show(&app, &ready_policy);
808 windows::with_placement_announced(&app, &label, || {
809 if restore_windows {
810 if let Some(state) = app.try_state::<ShellState>() {
811 let _ = state.windows.restore(&app, &label);
812 }
813 }
814 });
815 if label == main_label
816 && lifecycle::show_on_ready_decision(
817 show_on_ready_explicit,
818 show_on_ready_module,
819 lifecycle::launched_in_background(),
820 lifecycle::qa_hidden(),
821 )
822 {
823 if let Some(w) = app.get_webview_window(&label) {
824 let _ = crate::windows::show_window(
826 &app,
827 &w,
828 lifecycle::RevealSource::ShowOnReady,
829 windows::RevealStyle::ShowOnly,
830 );
831 }
832 }
833 })
834 .on_event(|app, event| {
835 if app.try_state::<ShellState>().is_some() {
836 lifecycle::run_event(app, event);
837 }
838 })
839 .setup(move |app, _api| {
840 let app = app.clone();
841 if let Err(e) = guard_result {
842 eprintln!("{e}");
843 return Err(Box::new(e));
844 }
845 match gate_result {
846 Ok(gate) => {
847 app.manage(gate);
848 }
849 Err(e) => {
850 eprintln!("{e}");
851 return Err(Box::new(e));
852 }
853 }
854 setup(&app, cfg, nav, opener)
855 });
856 if harden_webviews {
857 plugin = match script_scope {
858 HardeningScript::AllFrames => {
859 plugin.js_init_script_on_all_frames(hardening.script_for(&content_labels))
860 }
861 HardeningScript::TopFrameOnly => plugin.js_init_script(format!(
864 "if (window.top !== window.self) {{ return; }}\n{}",
865 hardening.script_for(&content_labels)
866 )),
867 HardeningScript::Off => plugin,
868 };
869 }
870 for (scheme, policy) in streams {
871 plugin = plugin.register_asynchronous_uri_scheme_protocol(
872 scheme,
873 move |ctx, request, responder| {
874 let state = ctx.app_handle().state::<ShellState>();
875 stream::serve(&state.broker, &policy, &request, move |response| {
876 responder.respond(response)
877 });
878 },
879 );
880 }
881 if let Some((scheme, policy)) = site {
882 plugin = plugin.register_asynchronous_uri_scheme_protocol(
883 scheme,
884 move |ctx, request, responder| {
885 let state = ctx.app_handle().state::<ShellState>();
886 site::serve_site_request(&state.broker, &policy, &request, move |response| {
887 responder.respond(response)
888 });
889 },
890 );
891 }
892 plugin.build()
893}
894
895struct PluginClaim(u64);
897
898fn unavailable_error(why: &str) -> Box<dyn std::error::Error> {
900 format!("rightkit-tauri-shell: single-instance could not be decided, refusing to start as a second primary: {why}").into()
901}
902
903fn invalid_app_id_error(e: &instance::InvalidAppId) -> Box<dyn std::error::Error> {
906 format!("rightkit-tauri-shell: single-instance cannot start, app id is not usable: {e}").into()
907}
908
909fn show_policy_of(cfg: &ShellBuilder) -> lifecycle::ShowPolicy {
911 lifecycle::ShowPolicy {
912 never_show: cfg.never_show,
913 never_show_env: cfg.never_show_env.clone(),
914 never_show_env_values: cfg.never_show_env_values.clone(),
915 reveal_on_reopen: cfg.reveal_on_reopen && cfg.modules.contains(Module::RevealOnReopen),
916 }
917}
918
919fn setup(
920 app: &AppHandle<Wry>,
921 cfg: ShellBuilder,
922 nav: Arc<NavPolicy>,
923 opener: Arc<dyn ExternalOpener>,
924) -> Result<(), Box<dyn std::error::Error>> {
925 let show_policy = show_policy_of(&cfg);
927 lifecycle::enforce_never_show(app, &show_policy);
928 let mut single = String::from("disabled");
930 let mode = lifecycle::instance_mode(
931 cfg.single_instance,
932 cfg.qa_instance_exempt,
933 lifecycle::qa_hidden(),
934 );
935 if mode == lifecycle::InstanceMode::QaExempt {
936 single = "qa-exempt".into();
937 }
938 if mode == lifecycle::InstanceMode::Arbitrate {
939 let for_cb = app.clone();
940 let custom = cfg.on_second_instance.clone();
941 let decider = cfg.on_second_instance_reveal.clone();
942 let version = app.package_info().version.to_string();
943 let identifier = app.config().identifier.clone();
944 match instance::arbitrate_with_legacy(
945 &cfg.app_id,
946 &version,
947 Some(identifier.as_str()),
948 Arc::new(move |s| {
949 lifecycle::route_second_instance(
950 &for_cb,
951 custom.as_ref(),
952 &s.argv,
953 &s.cwd,
954 |app, argv, cwd| {
955 lifecycle::on_second_instance(app, argv, cwd, decider.as_ref())
956 },
957 )
958 }),
959 ) {
960 instance::Arbitration::Primary(h) => {
961 let _claim = single_instance::hold(h);
963 single = "primary".into();
964 }
965 instance::Arbitration::Secondary => std::process::exit(0),
966 instance::Arbitration::Unavailable(why) => return Err(unavailable_error(&why)),
968 instance::Arbitration::InvalidAppId(e) => return Err(invalid_app_id_error(&e)),
969 }
970 }
971
972 let state_dir = match cfg.state_dir.clone() {
974 Some(d) => d,
975 None => app
976 .path()
977 .app_data_dir()
978 .map(|d| d.join("shell"))
979 .unwrap_or_else(|_| std::env::temp_dir().join(&cfg.app_id)),
980 };
981 let exe = std::env::current_exe().unwrap_or_default();
982 let tray_runtime = cfg.tray.as_ref().map(|t| tray::TrayRuntime {
983 icon: Mutex::new(None),
984 ids: Mutex::new(Default::default()),
985 show_label: t.show_label.clone(),
986 quit_label: t.quit_label.clone(),
987 });
988 #[cfg(feature = "engine")]
989 let engine = cfg.engine.clone().map(|s| {
990 Arc::new(engine::EngineHost::with_policy(
991 s,
992 cfg.engine_policy.clone(),
993 ))
994 });
995 let cache_repair = if lifecycle::repair_cache_enabled(cfg.repair_cache, cfg.lifecycle_only) {
997 let dir = cfg
998 .cache_dir
999 .clone()
1000 .or_else(|| app.path().app_cache_dir().ok());
1001 dir.and_then(|d| recovery::ensure_cache_dir(&d).ok().map(|o| (d, o)))
1002 } else {
1003 None
1004 };
1005 let presence = Arc::new(presence::PresenceDnd::new());
1006 if cfg.modules.contains(Module::Presence) {
1007 let (handle, on_presence) = (app.clone(), cfg.on_presence.clone());
1008 presence::start_loop(Arc::clone(&presence), cfg.app_name.clone(), move |s| {
1009 use tauri::Emitter;
1010 let _ = handle.emit(EVENT_PRESENCE, s);
1011 if let Some(cb) = &on_presence {
1012 cb(&handle, s);
1013 }
1014 });
1015 }
1016 let recovery = if cfg.modules.contains(Module::Recovery) {
1017 cfg.recovery
1018 .clone()
1019 .map(|spec| recovery::RecoveryStore::new(spec, &state_dir))
1020 } else {
1021 None
1022 };
1023 let stream_schemes: Vec<String> = if cfg.modules.contains(Module::Stream) {
1024 cfg.streams.iter().map(|(s, _)| s.clone()).collect()
1025 } else {
1026 Vec::new()
1027 };
1028 let site_scheme = if cfg.modules.contains(Module::Stream) {
1029 cfg.site.as_ref().map(|(s, _)| s.clone())
1030 } else {
1031 None
1032 };
1033 let autostart_spec = autostart::AutostartSpec {
1034 app_id: cfg.app_id.clone(),
1035 app_name: cfg.app_name.clone(),
1036 executable: exe,
1037 options: cfg.autostart.clone(),
1038 legacy_plugin_name: cfg
1039 .autostart
1040 .migrate_from_plugin_entry
1041 .then(|| app.package_info().name.clone()),
1042 };
1043 if cfg.autostart.migrate_from_plugin_entry {
1044 let legacy = app.package_info().name.clone();
1045 match autostart::migrate_plugin_entry(&autostart_spec, &legacy) {
1046 Ok(_) => {}
1047 Err(e) => eprintln!(
1049 "rightkit-tauri-shell: could not migrate the tauri-plugin-autostart login entry: {e}"
1050 ),
1051 }
1052 }
1053 if cfg.autostart.macos_backend == autostart::MacosBackend::ServiceManagement {
1054 if let Err(e) = autostart::migrate_launch_agent(&autostart_spec) {
1056 eprintln!("rightkit-tauri-shell: could not move the LaunchAgent login item to SMAppService: {e}");
1057 }
1058 }
1059 app.manage(ShellState {
1060 main_thread_id: std::thread::current().id(),
1061 app_id: cfg.app_id.clone(),
1062 app_name: cfg.app_name.clone(),
1063 main_label: cfg.main_label.clone(),
1064 nav,
1065 opener,
1066 frameless: cfg.frameless && cfg.modules.contains(Module::Frameless),
1067 close_to_tray: cfg.close_to_tray,
1068 hide_to_background: cfg.hide_to_background,
1069 on_visibility: cfg.on_visibility.clone(),
1070 activation: Default::default(),
1071 show_policy,
1072 quit: Arc::new(quit::QuitGate::new(cfg.quit_drain)),
1073 windows: Arc::new(window_state::WindowStateStore::new(state_dir)),
1074 deep: Default::default(),
1075 schemes: cfg.schemes.clone(),
1076 frontend_ready: AtomicBool::new(false),
1077 exiting: AtomicBool::new(false),
1078 traffic: Default::default(),
1079 autostart: autostart_spec,
1080 menu_items: Mutex::new(HashMap::new()),
1081 menu_rows: Mutex::new(Vec::new()),
1082 tray: tray_runtime,
1083 on_menu: cfg.on_menu.clone(),
1084 #[cfg(feature = "engine")]
1085 engine: engine.clone(),
1086 hosted: Mutex::new(None),
1087 single_instance: Mutex::new(single),
1088 broker: Arc::new(PathBroker::with_app_name(&cfg.app_name)),
1089 notifications: notify::NotificationState::new(),
1090 presence,
1091 idle: cfg.idle_after.map(|d| {
1092 let mut options = cfg.idle_options.unwrap_or_default();
1094 options.suspend_after = d;
1095 idle::IdleHost::new(d)
1096 .with_options(options)
1097 .with_on_error(cfg.idle_on_error.clone())
1098 .with_on_transition(cfg.idle_on_transition.clone())
1099 }),
1100 recovery,
1101 cache_repair: Mutex::new(cache_repair),
1102 stream_scheme: stream_schemes.first().cloned(),
1103 stream_schemes,
1104 site_scheme,
1105 site_policy: cfg
1106 .site
1107 .as_ref()
1108 .map(|(_, p)| p.clone())
1109 .unwrap_or_default(),
1110 modules: cfg.modules,
1111 clipboard: cfg.clipboard,
1112 clipboard_options: cfg.clipboard_options,
1113 clipboard_dir: clipboard::clipboard_temp_dir(&cfg.app_id),
1114 });
1115
1116 if let Some(spec) = &cfg.menu {
1118 let built = menu::build(
1119 app,
1120 &MenuSpec {
1121 app_name: cfg.app_name.clone(),
1122 ..spec.clone()
1123 },
1124 )?;
1125 let state = app.state::<ShellState>();
1126 *state.menu_rows.lock().unwrap_or_else(|e| e.into_inner()) = menu::describe(&built.menu);
1127 *state.menu_items.lock().unwrap_or_else(|e| e.into_inner()) = built.items;
1128 app.set_menu(built.menu)?;
1129 app.on_menu_event(|app, event| lifecycle::on_menu_event(app, event.id().as_ref()));
1130 }
1131
1132 if let Some(spec) = &cfg.tray {
1134 let icon = tray::install(app, spec, &cfg.main_label)?;
1135 let state = app.state::<ShellState>();
1136 if let Some(rt) = &state.tray {
1137 *rt.icon.lock().unwrap_or_else(|e| e.into_inner()) = Some(icon);
1138 }
1139 }
1140
1141 #[cfg(feature = "engine")]
1143 if let Some(engine) = engine {
1144 let handle = app.clone();
1145 let lifecycle_events = engine.subscribe_lifecycle();
1146 let supervised = lifecycle_events.is_some();
1147 if let Some(events) = lifecycle_events {
1148 let handle = app.clone();
1149 let host = Arc::downgrade(&engine);
1150 std::thread::Builder::new()
1151 .name("rk-shell-engine-events".into())
1152 .spawn(move || {
1153 use tauri::Emitter;
1154 loop {
1155 let Some(host) = host.upgrade() else { break };
1156 if host.is_shutting_down() { break; }
1157 match events.recv_timeout(std::time::Duration::from_millis(250)) {
1158 Ok(event) => {
1159 let _ = handle.emit(EVENT_ENGINE, serde_json::json!({
1160 "call": "$lifecycle", "event": host.lifecycle(), "supervisor": event
1161 }));
1162 }
1163 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
1164 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
1165 }
1166 }
1167 })?;
1168 }
1169 if let Some(events) = engine.subscribe_events() {
1170 let handle = app.clone();
1171 let host = Arc::downgrade(&engine);
1172 std::thread::Builder::new()
1173 .name("rk-shell-engine-unsolicited".into())
1174 .spawn(move || {
1175 use tauri::Emitter;
1176 loop {
1177 let Some(host) = host.upgrade() else { break };
1178 if host.is_shutting_down() {
1179 break;
1180 }
1181 match events.recv_timeout(std::time::Duration::from_millis(250)) {
1182 Ok(frame) => {
1183 let _ = handle.emit(
1184 EVENT_ENGINE,
1185 engine::EngineEvent {
1186 call: "$unsolicited".into(),
1187 id: None,
1188 tag: None,
1189 event: frame["event"].clone(),
1190 },
1191 );
1192 }
1193 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
1194 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
1195 }
1196 }
1197 })?;
1198 }
1199 std::thread::Builder::new()
1200 .name("rk-shell-engine".into())
1201 .spawn(move || {
1202 use tauri::Emitter;
1203 let payload = match engine.start() {
1204 Ok(hello) => serde_json::json!({"state": "ready", "hello": hello}),
1205 Err(e) => {
1206 serde_json::json!({"state": "error", "code": e.code, "message": e.message})
1207 }
1208 };
1209 engine.set_lifecycle(payload.clone());
1210 if !supervised {
1211 let _ = handle.emit(
1212 EVENT_ENGINE,
1213 serde_json::json!({"call": "$lifecycle", "event": payload}),
1214 );
1215 }
1216 })?;
1217 }
1218
1219 if cfg.modules.contains(Module::Watchdog) {
1221 if let Some(w) = cfg.watchdog.clone() {
1222 watchdog::install(app.clone(), w);
1223 }
1224 }
1225
1226 let argv: Vec<String> = std::env::args().skip(1).collect();
1228 let state = app.state::<ShellState>();
1229 state
1230 .deep
1231 .push_all(argv.iter().map(String::as_str), &state.schemes);
1232 notify::set_default_app_id(&app.config().identifier);
1233 open_url::deliver(&argv);
1234 Ok(())
1235}
1236
1237#[macro_export]
1240macro_rules! handler {
1241 ($($app_cmd:path),* $(,)?) => {
1242 ::tauri::generate_handler![
1243 $crate::commands::rk_shell_open_external,
1244 $crate::commands::rk_shell_ready,
1245 $crate::commands::rk_shell_status,
1246 $crate::commands::rk_shell_window,
1247 $crate::commands::rk_shell_open_window,
1248 $crate::commands::rk_shell_set_tray,
1249 $crate::commands::rk_shell_set_menu_item_enabled,
1250 $crate::commands::rk_shell_autostart,
1251 $crate::commands::rk_shell_open_login_items_settings,
1252 $crate::commands::rk_shell_clipboard_read,
1253 $crate::commands::rk_shell_clipboard_write,
1254 $crate::commands::rk_shell_engine_call,
1255 $crate::commands::rk_shell_engine_call_tagged,
1256 $crate::commands::rk_shell_engine_status,
1257 $crate::commands::rk_shell_traffic_lights,
1258 $crate::commands::rk_shell_quit,
1259 $crate::commands::rk_shell_finish_quit,
1260 $crate::commands::rk_shell_capabilities,
1261 $crate::commands::rk_shell_set_theme,
1262 $crate::commands::rk_shell_notify,
1263 $crate::commands::rk_shell_notify_status,
1264 $crate::commands::rk_shell_notify_clear,
1265 $crate::commands::rk_shell_badge,
1266 $crate::commands::rk_shell_set_dnd,
1267 $crate::commands::rk_shell_presence,
1268 $crate::commands::rk_shell_presence_prefs,
1269 $crate::commands::rk_shell_presence_probe,
1270 $crate::commands::rk_shell_presence_evaluate,
1271 $crate::commands::rk_shell_idle_status,
1272 $crate::commands::rk_shell_idle_note,
1273 $crate::commands::rk_shell_path_require,
1274 $crate::commands::rk_shell_path_revoke,
1275 $crate::commands::rk_shell_path_grant_recent,
1276 $crate::commands::rk_shell_path_regrant_saved,
1277 $crate::commands::rk_shell_stream_grant,
1278 $crate::commands::rk_shell_recovery_save,
1279 $crate::commands::rk_shell_recovery_load,
1280 $crate::commands::rk_shell_recovery_clear,
1281 $crate::commands::rk_shell_clipboard_item,
1282 $crate::commands::rk_shell_site_grant,
1283 $crate::commands::rk_shell_site_revoke
1284 $(, $app_cmd)*
1285 ]
1286 };
1287}