use std::ffi::{OsStr, OsString};
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{AppHandle, Emitter, Manager, RunEvent, Runtime, WindowEvent, Wry};
use crate::quit::QuitStep;
use crate::{ShellState, EVENT_DEEP_LINK, EVENT_MENU, EVENT_QUIT_REQUESTED, EVENT_SECOND_INSTANCE};
pub fn launched_in_background() -> bool {
std::env::args().any(|a| a == crate::autostart::BACKGROUND_ARG)
}
#[cfg(debug_assertions)]
pub fn qa_hidden() -> bool {
std::env::var_os("RIGHTKIT_QA_HIDDEN").as_deref() == Some(std::ffi::OsStr::new("1"))
}
#[cfg(not(debug_assertions))]
pub fn qa_hidden() -> bool {
false
}
#[cfg(debug_assertions)]
pub fn qa_background() -> bool {
std::env::var_os("RIGHTKIT_QA_BACKGROUND").as_deref() == Some(std::ffi::OsStr::new("1"))
}
#[cfg(not(debug_assertions))]
pub fn qa_background() -> bool {
false
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RevealSource {
Reopen,
Opened,
SecondInstance,
ShowOnReady,
Tray,
WindowCommand,
OpenWindow,
Restore,
Api,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RevealMode {
Forbidden,
Background,
Foreground,
}
pub const NEVER_SHOW_TRUTHY: [&str; 4] = ["1", "true", "yes", "on"];
pub fn env_means_never_show(value: Option<&OsStr>) -> bool {
let Some(value) = value.and_then(|v| v.to_str()) else {
return false;
};
NEVER_SHOW_TRUTHY
.iter()
.any(|truthy| value.eq_ignore_ascii_case(truthy))
}
pub fn env_equals(value: Option<&OsStr>, expected: &str) -> bool {
value.is_some_and(|v| v == OsStr::new(expected))
}
#[derive(Debug, Clone)]
pub struct ShowPolicy {
pub never_show: bool,
pub never_show_env: Vec<String>,
pub never_show_env_values: Vec<(String, String)>,
pub reveal_on_reopen: bool,
}
impl Default for ShowPolicy {
fn default() -> Self {
Self {
never_show: false,
never_show_env: Vec::new(),
never_show_env_values: Vec::new(),
reveal_on_reopen: true,
}
}
}
impl ShowPolicy {
pub fn is_never_show(&self, env: &dyn Fn(&str) -> Option<OsString>) -> bool {
self.never_show
|| self
.never_show_env
.iter()
.any(|name| env_means_never_show(env(name).as_deref()))
|| self
.never_show_env_values
.iter()
.any(|(name, expected)| env_equals(env(name).as_deref(), expected))
}
pub fn never_show_now(&self) -> bool {
self.is_never_show(&|name: &str| std::env::var_os(name))
}
pub fn mode(
&self,
source: RevealSource,
qa_background: bool,
env: &dyn Fn(&str) -> Option<OsString>,
) -> RevealMode {
if self.is_never_show(env) {
return RevealMode::Forbidden;
}
if matches!(source, RevealSource::Reopen | RevealSource::Opened) && !self.reveal_on_reopen {
return RevealMode::Forbidden;
}
if qa_background {
RevealMode::Background
} else {
RevealMode::Foreground
}
}
pub fn mode_now(&self, source: RevealSource) -> RevealMode {
self.mode(source, qa_background(), &|name: &str| {
std::env::var_os(name)
})
}
}
pub fn reveal_mode<R: Runtime>(app: &AppHandle<R>, source: RevealSource) -> RevealMode {
match app.try_state::<ShellState>() {
Some(state) => state.show_policy.mode_now(source),
None => ShowPolicy::default().mode_now(source),
}
}
pub fn never_show<R: Runtime>(app: &AppHandle<R>) -> bool {
app.try_state::<ShellState>()
.is_some_and(|state| state.show_policy.never_show_now())
}
pub fn reopen_wants_reveal(has_visible_windows: bool) -> bool {
!has_visible_windows
}
#[cfg(target_os = "macos")]
pub(crate) fn enforce_never_show(app: &AppHandle<Wry>, policy: &ShowPolicy) {
if policy.never_show_now() {
let _ = app.set_activation_policy(tauri::ActivationPolicy::Prohibited);
}
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn enforce_never_show(_app: &AppHandle<Wry>, _policy: &ShowPolicy) {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstanceMode {
Disabled,
Arbitrate,
QaExempt,
}
pub fn instance_mode(single_instance: bool, qa_exempt: bool, qa_hidden_env: bool) -> InstanceMode {
match (single_instance, qa_exempt && qa_hidden_env) {
(false, _) => InstanceMode::Disabled,
(true, true) => InstanceMode::QaExempt,
(true, false) => InstanceMode::Arbitrate,
}
}
pub fn show_on_ready_decision(
explicit: Option<bool>,
module_enabled: bool,
background_arg: bool,
qa_hidden_env: bool,
) -> bool {
if !module_enabled || background_arg {
return false;
}
explicit.unwrap_or(!qa_hidden_env)
}
pub fn show_on_ready_installed(
module_in_set: bool,
lifecycle_only: bool,
explicit: Option<bool>,
) -> bool {
module_in_set || (lifecycle_only && explicit == Some(true))
}
pub fn repair_cache_enabled(explicit: Option<bool>, lifecycle_only: bool) -> bool {
explicit.unwrap_or(!lifecycle_only)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Visibility {
Hidden,
Revealed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Activation {
Regular,
Accessory,
}
pub fn activation_for(
hide_to_background: bool,
visibility: Visibility,
other_visible: bool,
) -> Option<Activation> {
if !hide_to_background {
return None;
}
match visibility {
Visibility::Hidden if other_visible => None,
Visibility::Hidden => Some(Activation::Accessory),
Visibility::Revealed => Some(Activation::Regular),
}
}
#[derive(Debug, Default)]
pub struct ActivationTracker {
accessory: AtomicBool,
}
impl ActivationTracker {
pub fn transition(&self, want: Option<Activation>) -> Option<Activation> {
match want? {
Activation::Accessory => {
(!self.accessory.swap(true, Ordering::SeqCst)).then_some(Activation::Accessory)
}
Activation::Regular => self
.accessory
.swap(false, Ordering::SeqCst)
.then_some(Activation::Regular),
}
}
}
pub(crate) fn note_visibility(app: &AppHandle<Wry>, visibility: Visibility) {
let state = app.state::<ShellState>();
let other_visible = app
.webview_windows()
.iter()
.any(|(label, w)| *label != state.main_label && w.is_visible().unwrap_or(false));
let want = if state.show_policy.never_show_now() {
None
} else {
activation_for(state.hide_to_background, visibility, other_visible)
};
if let Some(change) = state.activation.transition(want) {
apply_activation(app, change);
}
if let Some(cb) = &state.on_visibility {
cb(app, visibility);
}
}
#[cfg(target_os = "macos")]
fn apply_activation(app: &AppHandle<Wry>, change: Activation) {
let policy = match change {
Activation::Regular => tauri::ActivationPolicy::Regular,
Activation::Accessory => tauri::ActivationPolicy::Accessory,
};
let _ = app.set_activation_policy(policy);
}
#[cfg(not(target_os = "macos"))]
fn apply_activation(_app: &AppHandle<Wry>, _change: Activation) {}
pub(crate) fn request_quit(app: &AppHandle<Wry>, source: &'static str) {
let state = app.state::<ShellState>();
let alive = !app.webview_windows().is_empty();
match state.quit.request(&state.exiting, alive) {
QuitStep::ExitNow => quit(app),
QuitStep::AlreadyDraining => {}
QuitStep::Drain => {
if !start_drain(app, source) {
state.quit.finish();
quit(app);
}
}
}
}
fn start_drain(app: &AppHandle<Wry>, source: &'static str) -> bool {
let state = app.state::<ShellState>();
let Some(bound) = state.quit.bound() else {
return false;
};
let payload = serde_json::json!({"source": source, "boundMs": bound.as_millis() as u64});
if app.emit(EVENT_QUIT_REQUESTED, payload).is_err() {
return false;
}
let handle = app.clone();
state.quit.arm_bound(move || quit(&handle));
true
}
pub(crate) fn finish_quit(app: &AppHandle<Wry>) {
let state = app.state::<ShellState>();
state.exiting.store(true, Ordering::SeqCst);
state.quit.finish();
quit(app);
}
pub fn quit(app: &AppHandle<Wry>) {
app.state::<ShellState>()
.exiting
.store(true, Ordering::SeqCst);
app.exit(0);
}
pub(crate) fn deliver_links<'a>(app: &AppHandle<Wry>, raw: impl IntoIterator<Item = &'a str>) {
let state = app.state::<ShellState>();
let raw: Vec<&str> = raw.into_iter().collect();
crate::open_url::deliver(&raw);
state.deep.push_all(raw, &state.schemes);
flush_links(app);
}
pub(crate) fn flush_links(app: &AppHandle<Wry>) {
let state = app.state::<ShellState>();
if !state.frontend_ready.load(Ordering::SeqCst) {
return;
}
for link in state.deep.drain() {
let _ = app.emit(EVENT_DEEP_LINK, link);
}
}
pub(crate) type SecondInstanceFn<A> = std::sync::Arc<dyn Fn(&A, &[String], &str) + Send + Sync>;
pub(crate) fn route_second_instance<A>(
app: &A,
custom: Option<&SecondInstanceFn<A>>,
argv: &[String],
cwd: &str,
default: impl Fn(&A, &[String], &str),
) {
crate::open_url::deliver_second_instance(argv);
match custom {
Some(f) => f(app, argv, cwd),
None => default(app, argv, cwd),
}
}
pub(crate) type RevealDecider<A> =
std::sync::Arc<dyn Fn(&A, &[String], &str) -> bool + Send + Sync>;
pub(crate) fn second_instance_reveals<A>(
decider: Option<&RevealDecider<A>>,
app: &A,
argv: &[String],
cwd: &str,
) -> bool {
decider.is_none_or(|f| f(app, argv, cwd))
}
pub(crate) fn on_second_instance(
app: &AppHandle<Wry>,
argv: &[String],
cwd: &str,
decider: Option<&RevealDecider<AppHandle<Wry>>>,
) {
let _ = app.emit(
EVENT_SECOND_INSTANCE,
serde_json::json!({"argv": argv, "cwd": cwd}),
);
deliver_links(app, argv.iter().skip(1).map(String::as_str));
if second_instance_reveals(decider, app, argv, cwd) {
crate::windows::reveal_with(app, None, RevealSource::SecondInstance);
}
}
pub(crate) fn on_menu_event(app: &AppHandle<Wry>, id: &str) {
if id == crate::menu::ID_QUIT {
request_quit(app, "menu");
return;
}
let state = app.state::<ShellState>();
let _ = app.emit(EVENT_MENU, serde_json::json!({"id": id}));
if let Some(cb) = &state.on_menu {
cb(app, id);
}
}
fn note_minimized_for_idle(app: &AppHandle<Wry>, label: &str) {
let state = app.state::<ShellState>();
if label != state.main_label
|| !state
.idle
.as_ref()
.is_some_and(|h| h.options.minimise_enabled())
{
return;
}
if let Some(w) = app.get_webview_window(label) {
crate::idle::note_minimized(app, w.is_minimized().unwrap_or(false));
}
}
pub(crate) fn run_event(app: &AppHandle<Wry>, event: &RunEvent) {
let state = app.state::<ShellState>();
match event {
RunEvent::WindowEvent { label, event, .. } => match event {
WindowEvent::Moved(_) | WindowEvent::Resized(_) => {
if state.modules.contains(crate::Module::WindowState) {
state.windows.schedule_save(app.clone(), label.clone());
}
if matches!(event, WindowEvent::Resized(_)) {
if let Some(w) = app.get_webview_window(label) {
if state.modules.contains(crate::Module::Frameless) {
state.traffic.reapply(&w);
}
}
note_minimized_for_idle(app, label);
}
}
WindowEvent::Focused(false) => note_minimized_for_idle(app, label),
WindowEvent::Focused(true) => {
if let Some(w) = app.get_webview_window(label) {
if state.modules.contains(crate::Module::Frameless) {
state.traffic.reapply(&w);
}
}
if *label == state.main_label {
crate::idle::note_visible(app);
}
}
WindowEvent::CloseRequested { api, .. } => {
if state.modules.contains(crate::Module::WindowState) {
let _ = state.windows.save_now(app, label);
}
if state.close_to_tray
&& *label == state.main_label
&& !state.exiting.load(Ordering::SeqCst)
{
api.prevent_close();
if let Some(w) = app.get_webview_window(label) {
let _ = w.hide();
}
crate::idle::note_hidden(app, true);
note_visibility(app, Visibility::Hidden);
}
}
_ => {}
},
RunEvent::ExitRequested { api, code, .. } => {
if code.is_none() && state.close_to_tray && !state.exiting.load(Ordering::SeqCst) {
api.prevent_exit();
return;
}
match state
.quit
.request(&state.exiting, !app.webview_windows().is_empty())
{
QuitStep::AlreadyDraining => {
api.prevent_exit();
return;
}
QuitStep::Drain => {
if start_drain(app, "exit-requested") {
api.prevent_exit();
return;
}
state.quit.finish();
}
QuitStep::ExitNow => {}
}
state.exiting.store(true, Ordering::SeqCst);
for w in app.webview_windows().keys() {
if state.modules.contains(crate::Module::WindowState) {
let _ = state.windows.save_now(app, w);
}
}
}
RunEvent::Exit => shutdown(app),
#[cfg(target_os = "macos")]
RunEvent::Reopen {
has_visible_windows,
..
} => {
if reopen_wants_reveal(*has_visible_windows) {
crate::windows::reveal_with(app, None, RevealSource::Reopen);
}
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
RunEvent::Opened { urls } => {
let raw: Vec<String> = urls.iter().map(|u| u.to_string()).collect();
deliver_links(app, raw.iter().map(String::as_str));
crate::windows::reveal_with(app, None, RevealSource::Opened);
}
_ => {}
}
}
fn shutdown(app: &AppHandle<Wry>) {
let state = app.state::<ShellState>();
state.exiting.store(true, Ordering::SeqCst);
#[cfg(feature = "engine")]
if let Some(engine) = &state.engine {
engine.shutdown();
}
if state.tray.is_some() {
let _ = app.remove_tray_by_id(crate::tray::TRAY_ID);
}
crate::single_instance::release_all();
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::sync::{Arc, Mutex};
type Calls = Arc<Mutex<Vec<(Vec<String>, String)>>>;
#[test]
fn default_runs_without_a_callback() {
let ran = Cell::new(0);
let argv = vec!["app".to_owned()];
route_second_instance(&(), None, &argv, "/w", |_, a, c| {
assert_eq!((a, c), (&argv[..], "/w"));
ran.set(ran.get() + 1);
});
assert_eq!(ran.get(), 1);
}
#[test]
fn custom_callback_gets_argv_and_cwd_and_suppresses_default() {
let calls: Calls = Arc::default();
let c = calls.clone();
let cb: SecondInstanceFn<()> =
Arc::new(move |_, a, cwd| c.lock().unwrap().push((a.to_vec(), cwd.to_owned())));
let argv = vec!["app".to_owned(), "--x".to_owned()];
route_second_instance(&(), Some(&cb), &argv, "/work", |_, _, _| {
panic!("default focus behaviour must not run")
});
assert_eq!(*calls.lock().unwrap(), vec![(argv, "/work".to_owned())]);
}
#[test]
fn the_default_second_launch_reveals_unless_the_host_declines() {
let argv = vec!["app".to_owned()];
assert!(second_instance_reveals::<()>(None, &(), &argv, "/w"));
let yes: RevealDecider<()> = Arc::new(|_, _, _| true);
let no: RevealDecider<()> = Arc::new(|_, a, cwd| a.len() > 5 && cwd == "/w");
assert!(second_instance_reveals(Some(&yes), &(), &argv, "/w"));
assert!(!second_instance_reveals(Some(&no), &(), &argv, "/w"));
}
}