1use std::ffi::{OsStr, OsString};
20use std::sync::atomic::{AtomicBool, Ordering};
21
22use tauri::{AppHandle, Emitter, Manager, RunEvent, Runtime, WindowEvent, Wry};
23
24use crate::quit::QuitStep;
25use crate::{ShellState, EVENT_DEEP_LINK, EVENT_MENU, EVENT_QUIT_REQUESTED, EVENT_SECOND_INSTANCE};
26
27pub fn launched_in_background() -> bool {
29 std::env::args().any(|a| a == crate::autostart::BACKGROUND_ARG)
30}
31
32#[cfg(debug_assertions)]
37pub fn qa_hidden() -> bool {
38 std::env::var_os("RIGHTKIT_QA_HIDDEN").as_deref() == Some(std::ffi::OsStr::new("1"))
39}
40
41#[cfg(not(debug_assertions))]
42pub fn qa_hidden() -> bool {
43 false
44}
45
46#[cfg(debug_assertions)]
47pub fn qa_background() -> bool {
48 std::env::var_os("RIGHTKIT_QA_BACKGROUND").as_deref() == Some(std::ffi::OsStr::new("1"))
49}
50
51#[cfg(not(debug_assertions))]
52pub fn qa_background() -> bool {
53 false
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58pub enum RevealSource {
59 Reopen,
61 Opened,
63 SecondInstance,
65 ShowOnReady,
67 Tray,
69 WindowCommand,
71 OpenWindow,
73 Restore,
75 Api,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum RevealMode {
82 Forbidden,
84 Background,
86 Foreground,
88}
89
90pub const NEVER_SHOW_TRUTHY: [&str; 4] = ["1", "true", "yes", "on"];
93
94pub fn env_means_never_show(value: Option<&OsStr>) -> bool {
98 let Some(value) = value.and_then(|v| v.to_str()) else {
99 return false;
100 };
101 NEVER_SHOW_TRUTHY
102 .iter()
103 .any(|truthy| value.eq_ignore_ascii_case(truthy))
104}
105
106pub fn env_equals(value: Option<&OsStr>, expected: &str) -> bool {
108 value.is_some_and(|v| v == OsStr::new(expected))
109}
110
111#[derive(Debug, Clone)]
113pub struct ShowPolicy {
114 pub never_show: bool,
117 pub never_show_env: Vec<String>,
119 pub never_show_env_values: Vec<(String, String)>,
122 pub reveal_on_reopen: bool,
124}
125
126impl Default for ShowPolicy {
127 fn default() -> Self {
128 Self {
129 never_show: false,
130 never_show_env: Vec::new(),
131 never_show_env_values: Vec::new(),
132 reveal_on_reopen: true,
133 }
134 }
135}
136
137impl ShowPolicy {
138 pub fn is_never_show(&self, env: &dyn Fn(&str) -> Option<OsString>) -> bool {
140 self.never_show
141 || self
142 .never_show_env
143 .iter()
144 .any(|name| env_means_never_show(env(name).as_deref()))
145 || self
146 .never_show_env_values
147 .iter()
148 .any(|(name, expected)| env_equals(env(name).as_deref(), expected))
149 }
150
151 pub fn never_show_now(&self) -> bool {
153 self.is_never_show(&|name: &str| std::env::var_os(name))
154 }
155
156 pub fn mode(
158 &self,
159 source: RevealSource,
160 qa_background: bool,
161 env: &dyn Fn(&str) -> Option<OsString>,
162 ) -> RevealMode {
163 if self.is_never_show(env) {
164 return RevealMode::Forbidden;
165 }
166 if matches!(source, RevealSource::Reopen | RevealSource::Opened) && !self.reveal_on_reopen {
167 return RevealMode::Forbidden;
168 }
169 if qa_background {
170 RevealMode::Background
171 } else {
172 RevealMode::Foreground
173 }
174 }
175
176 pub fn mode_now(&self, source: RevealSource) -> RevealMode {
178 self.mode(source, qa_background(), &|name: &str| {
179 std::env::var_os(name)
180 })
181 }
182}
183
184pub fn reveal_mode<R: Runtime>(app: &AppHandle<R>, source: RevealSource) -> RevealMode {
189 match app.try_state::<ShellState>() {
190 Some(state) => state.show_policy.mode_now(source),
191 None => ShowPolicy::default().mode_now(source),
192 }
193}
194
195pub fn never_show<R: Runtime>(app: &AppHandle<R>) -> bool {
197 app.try_state::<ShellState>()
198 .is_some_and(|state| state.show_policy.never_show_now())
199}
200
201pub fn reopen_wants_reveal(has_visible_windows: bool) -> bool {
203 !has_visible_windows
204}
205
206#[cfg(target_os = "macos")]
209pub(crate) fn enforce_never_show(app: &AppHandle<Wry>, policy: &ShowPolicy) {
210 if policy.never_show_now() {
211 let _ = app.set_activation_policy(tauri::ActivationPolicy::Prohibited);
212 }
213}
214
215#[cfg(not(target_os = "macos"))]
216pub(crate) fn enforce_never_show(_app: &AppHandle<Wry>, _policy: &ShowPolicy) {}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum InstanceMode {
221 Disabled,
223 Arbitrate,
225 QaExempt,
229}
230
231pub fn instance_mode(single_instance: bool, qa_exempt: bool, qa_hidden_env: bool) -> InstanceMode {
233 match (single_instance, qa_exempt && qa_hidden_env) {
234 (false, _) => InstanceMode::Disabled,
235 (true, true) => InstanceMode::QaExempt,
236 (true, false) => InstanceMode::Arbitrate,
237 }
238}
239
240pub fn show_on_ready_decision(
246 explicit: Option<bool>,
247 module_enabled: bool,
248 background_arg: bool,
249 qa_hidden_env: bool,
250) -> bool {
251 if !module_enabled || background_arg {
252 return false;
253 }
254 explicit.unwrap_or(!qa_hidden_env)
255}
256
257pub fn show_on_ready_installed(
261 module_in_set: bool,
262 lifecycle_only: bool,
263 explicit: Option<bool>,
264) -> bool {
265 module_in_set || (lifecycle_only && explicit == Some(true))
266}
267
268pub fn repair_cache_enabled(explicit: Option<bool>, lifecycle_only: bool) -> bool {
271 explicit.unwrap_or(!lifecycle_only)
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
276pub enum Visibility {
277 Hidden,
278 Revealed,
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283pub enum Activation {
284 Regular,
286 Accessory,
288}
289
290pub fn activation_for(
293 hide_to_background: bool,
294 visibility: Visibility,
295 other_visible: bool,
296) -> Option<Activation> {
297 if !hide_to_background {
298 return None;
299 }
300 match visibility {
301 Visibility::Hidden if other_visible => None,
302 Visibility::Hidden => Some(Activation::Accessory),
303 Visibility::Revealed => Some(Activation::Regular),
304 }
305}
306
307#[derive(Debug, Default)]
309pub struct ActivationTracker {
310 accessory: AtomicBool,
311}
312
313impl ActivationTracker {
314 pub fn transition(&self, want: Option<Activation>) -> Option<Activation> {
316 match want? {
317 Activation::Accessory => {
318 (!self.accessory.swap(true, Ordering::SeqCst)).then_some(Activation::Accessory)
319 }
320 Activation::Regular => self
321 .accessory
322 .swap(false, Ordering::SeqCst)
323 .then_some(Activation::Regular),
324 }
325 }
326}
327
328pub(crate) fn note_visibility(app: &AppHandle<Wry>, visibility: Visibility) {
331 let state = app.state::<ShellState>();
332 let other_visible = app
333 .webview_windows()
334 .iter()
335 .any(|(label, w)| *label != state.main_label && w.is_visible().unwrap_or(false));
336 let want = if state.show_policy.never_show_now() {
338 None
339 } else {
340 activation_for(state.hide_to_background, visibility, other_visible)
341 };
342 if let Some(change) = state.activation.transition(want) {
343 apply_activation(app, change);
344 }
345 if let Some(cb) = &state.on_visibility {
346 cb(app, visibility);
347 }
348}
349
350#[cfg(target_os = "macos")]
351fn apply_activation(app: &AppHandle<Wry>, change: Activation) {
352 let policy = match change {
353 Activation::Regular => tauri::ActivationPolicy::Regular,
354 Activation::Accessory => tauri::ActivationPolicy::Accessory,
355 };
356 let _ = app.set_activation_policy(policy);
357}
358
359#[cfg(not(target_os = "macos"))]
360fn apply_activation(_app: &AppHandle<Wry>, _change: Activation) {}
361
362pub(crate) fn request_quit(app: &AppHandle<Wry>, source: &'static str) {
364 let state = app.state::<ShellState>();
365 let alive = !app.webview_windows().is_empty();
366 match state.quit.request(&state.exiting, alive) {
367 QuitStep::ExitNow => quit(app),
368 QuitStep::AlreadyDraining => {}
369 QuitStep::Drain => {
370 if !start_drain(app, source) {
371 state.quit.finish();
372 quit(app);
373 }
374 }
375 }
376}
377
378fn start_drain(app: &AppHandle<Wry>, source: &'static str) -> bool {
380 let state = app.state::<ShellState>();
381 let Some(bound) = state.quit.bound() else {
382 return false;
383 };
384 let payload = serde_json::json!({"source": source, "boundMs": bound.as_millis() as u64});
385 if app.emit(EVENT_QUIT_REQUESTED, payload).is_err() {
386 return false;
387 }
388 let handle = app.clone();
389 state.quit.arm_bound(move || quit(&handle));
390 true
391}
392
393pub(crate) fn finish_quit(app: &AppHandle<Wry>) {
395 let state = app.state::<ShellState>();
396 state.exiting.store(true, Ordering::SeqCst);
398 state.quit.finish();
399 quit(app);
400}
401
402pub fn quit(app: &AppHandle<Wry>) {
403 app.state::<ShellState>()
404 .exiting
405 .store(true, Ordering::SeqCst);
406 app.exit(0);
407}
408
409pub(crate) fn deliver_links<'a>(app: &AppHandle<Wry>, raw: impl IntoIterator<Item = &'a str>) {
410 let state = app.state::<ShellState>();
411 let raw: Vec<&str> = raw.into_iter().collect();
412 crate::open_url::deliver(&raw);
413 state.deep.push_all(raw, &state.schemes);
414 flush_links(app);
415}
416
417pub(crate) fn flush_links(app: &AppHandle<Wry>) {
418 let state = app.state::<ShellState>();
419 if !state.frontend_ready.load(Ordering::SeqCst) {
420 return;
421 }
422 for link in state.deep.drain() {
423 let _ = app.emit(EVENT_DEEP_LINK, link);
424 }
425}
426
427pub(crate) type SecondInstanceFn<A> = std::sync::Arc<dyn Fn(&A, &[String], &str) + Send + Sync>;
429
430pub(crate) fn route_second_instance<A>(
433 app: &A,
434 custom: Option<&SecondInstanceFn<A>>,
435 argv: &[String],
436 cwd: &str,
437 default: impl Fn(&A, &[String], &str),
438) {
439 crate::open_url::deliver_second_instance(argv);
440 match custom {
441 Some(f) => f(app, argv, cwd),
442 None => default(app, argv, cwd),
443 }
444}
445
446pub(crate) type RevealDecider<A> =
448 std::sync::Arc<dyn Fn(&A, &[String], &str) -> bool + Send + Sync>;
449
450pub(crate) fn second_instance_reveals<A>(
452 decider: Option<&RevealDecider<A>>,
453 app: &A,
454 argv: &[String],
455 cwd: &str,
456) -> bool {
457 decider.is_none_or(|f| f(app, argv, cwd))
458}
459
460pub(crate) fn on_second_instance(
461 app: &AppHandle<Wry>,
462 argv: &[String],
463 cwd: &str,
464 decider: Option<&RevealDecider<AppHandle<Wry>>>,
465) {
466 let _ = app.emit(
467 EVENT_SECOND_INSTANCE,
468 serde_json::json!({"argv": argv, "cwd": cwd}),
469 );
470 deliver_links(app, argv.iter().skip(1).map(String::as_str));
471 if second_instance_reveals(decider, app, argv, cwd) {
472 crate::windows::reveal_with(app, None, RevealSource::SecondInstance);
473 }
474}
475
476pub(crate) fn on_menu_event(app: &AppHandle<Wry>, id: &str) {
477 if id == crate::menu::ID_QUIT {
478 request_quit(app, "menu");
479 return;
480 }
481 let state = app.state::<ShellState>();
482 let _ = app.emit(EVENT_MENU, serde_json::json!({"id": id}));
483 if let Some(cb) = &state.on_menu {
484 cb(app, id);
485 }
486}
487
488fn note_minimized_for_idle(app: &AppHandle<Wry>, label: &str) {
491 let state = app.state::<ShellState>();
492 if label != state.main_label
493 || !state
494 .idle
495 .as_ref()
496 .is_some_and(|h| h.options.minimise_enabled())
497 {
498 return;
499 }
500 if let Some(w) = app.get_webview_window(label) {
501 crate::idle::note_minimized(app, w.is_minimized().unwrap_or(false));
502 }
503}
504
505pub(crate) fn run_event(app: &AppHandle<Wry>, event: &RunEvent) {
506 let state = app.state::<ShellState>();
507 match event {
508 RunEvent::WindowEvent { label, event, .. } => match event {
509 WindowEvent::Moved(_) | WindowEvent::Resized(_) => {
510 if state.modules.contains(crate::Module::WindowState) {
511 state.windows.schedule_save(app.clone(), label.clone());
512 }
513 if matches!(event, WindowEvent::Resized(_)) {
514 if let Some(w) = app.get_webview_window(label) {
515 if state.modules.contains(crate::Module::Frameless) {
516 state.traffic.reapply(&w);
517 }
518 }
519 note_minimized_for_idle(app, label);
520 }
521 }
522 WindowEvent::Focused(false) => note_minimized_for_idle(app, label),
523 WindowEvent::Focused(true) => {
524 if let Some(w) = app.get_webview_window(label) {
525 if state.modules.contains(crate::Module::Frameless) {
526 state.traffic.reapply(&w);
527 }
528 }
529 if *label == state.main_label {
530 crate::idle::note_visible(app);
531 }
532 }
533 WindowEvent::CloseRequested { api, .. } => {
534 if state.modules.contains(crate::Module::WindowState) {
535 let _ = state.windows.save_now(app, label);
536 }
537 if state.close_to_tray
538 && *label == state.main_label
539 && !state.exiting.load(Ordering::SeqCst)
540 {
541 api.prevent_close();
542 if let Some(w) = app.get_webview_window(label) {
543 let _ = w.hide();
544 }
545 crate::idle::note_hidden(app, true);
546 note_visibility(app, Visibility::Hidden);
547 }
548 }
549 _ => {}
550 },
551 RunEvent::ExitRequested { api, code, .. } => {
552 if code.is_none() && state.close_to_tray && !state.exiting.load(Ordering::SeqCst) {
554 api.prevent_exit();
555 return;
556 }
557 match state
560 .quit
561 .request(&state.exiting, !app.webview_windows().is_empty())
562 {
563 QuitStep::AlreadyDraining => {
564 api.prevent_exit();
565 return;
566 }
567 QuitStep::Drain => {
568 if start_drain(app, "exit-requested") {
569 api.prevent_exit();
570 return;
571 }
572 state.quit.finish();
573 }
574 QuitStep::ExitNow => {}
575 }
576 state.exiting.store(true, Ordering::SeqCst);
577 for w in app.webview_windows().keys() {
578 if state.modules.contains(crate::Module::WindowState) {
579 let _ = state.windows.save_now(app, w);
580 }
581 }
582 }
583 RunEvent::Exit => shutdown(app),
584 #[cfg(target_os = "macos")]
585 RunEvent::Reopen {
586 has_visible_windows,
587 ..
588 } => {
589 if reopen_wants_reveal(*has_visible_windows) {
590 crate::windows::reveal_with(app, None, RevealSource::Reopen);
591 }
592 }
593 #[cfg(any(target_os = "macos", target_os = "ios"))]
594 RunEvent::Opened { urls } => {
595 let raw: Vec<String> = urls.iter().map(|u| u.to_string()).collect();
596 deliver_links(app, raw.iter().map(String::as_str));
597 crate::windows::reveal_with(app, None, RevealSource::Opened);
598 }
599 _ => {}
600 }
601}
602
603fn shutdown(app: &AppHandle<Wry>) {
604 let state = app.state::<ShellState>();
605 state.exiting.store(true, Ordering::SeqCst);
606 #[cfg(feature = "engine")]
607 if let Some(engine) = &state.engine {
608 engine.shutdown();
609 }
610 if state.tray.is_some() {
611 let _ = app.remove_tray_by_id(crate::tray::TRAY_ID);
612 }
613 crate::single_instance::release_all();
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620 use std::cell::Cell;
621 use std::sync::{Arc, Mutex};
622
623 type Calls = Arc<Mutex<Vec<(Vec<String>, String)>>>;
624
625 #[test]
626 fn default_runs_without_a_callback() {
627 let ran = Cell::new(0);
628 let argv = vec!["app".to_owned()];
629 route_second_instance(&(), None, &argv, "/w", |_, a, c| {
630 assert_eq!((a, c), (&argv[..], "/w"));
631 ran.set(ran.get() + 1);
632 });
633 assert_eq!(ran.get(), 1);
634 }
635
636 #[test]
637 fn custom_callback_gets_argv_and_cwd_and_suppresses_default() {
638 let calls: Calls = Arc::default();
639 let c = calls.clone();
640 let cb: SecondInstanceFn<()> =
641 Arc::new(move |_, a, cwd| c.lock().unwrap().push((a.to_vec(), cwd.to_owned())));
642 let argv = vec!["app".to_owned(), "--x".to_owned()];
643 route_second_instance(&(), Some(&cb), &argv, "/work", |_, _, _| {
644 panic!("default focus behaviour must not run")
645 });
646 assert_eq!(*calls.lock().unwrap(), vec![(argv, "/work".to_owned())]);
647 }
648
649 #[test]
650 fn the_default_second_launch_reveals_unless_the_host_declines() {
651 let argv = vec!["app".to_owned()];
652 assert!(second_instance_reveals::<()>(None, &(), &argv, "/w"));
653 let yes: RevealDecider<()> = Arc::new(|_, _, _| true);
654 let no: RevealDecider<()> = Arc::new(|_, a, cwd| a.len() > 5 && cwd == "/w");
655 assert!(second_instance_reveals(Some(&yes), &(), &argv, "/w"));
656 assert!(!second_instance_reveals(Some(&no), &(), &argv, "/w"));
657 }
658}