Files
dbx/src-tauri/src/lib.rs
T

2985 lines
139 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
mod background_backup;
mod commands;
mod data_dir;
pub use background_backup::run_if_requested as run_backup_worker_if_requested;
mod db;
#[cfg(target_os = "macos")]
mod macos_app_delegate;
#[cfg(target_os = "macos")]
mod macos_escape_guard;
mod migration_gate;
mod models;
mod plugin_ui_protocol;
#[cfg(any(target_os = "windows", test))]
mod startup_recovery;
#[cfg(all(not(target_os = "windows"), not(test)))]
#[path = "startup_recovery_noop.rs"]
mod startup_recovery;
#[cfg(any(target_os = "windows", test))]
mod webview2_recovery;
mod window_state_guard;
use commands::connection::AppState;
use dbx_core::persistence::secret_codec::SecretKeyPolicy;
use dbx_core::sql_dialect::dialect_loader::{register_core_dialects, DialectPluginLoader, DialectRegistry};
use dbx_core::sql_dialect::hot_reload::DialectHotReload;
use dbx_core::storage::{maybe_import_user_data_db, DesktopIconTheme, DesktopSettings, Storage};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
#[cfg(target_os = "macos")]
use tauri::menu::Menu;
#[cfg(target_os = "macos")]
use tauri::menu::{AboutMetadata, MenuItem, PredefinedMenuItem, Submenu};
use tauri::webview::PageLoadEvent;
use tauri::RunEvent;
use tauri::{
menu::MenuBuilder,
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
};
use tauri::{Emitter, Manager};
#[cfg(target_os = "macos")]
use tauri_plugin_clipboard_manager::ClipboardExt;
#[cfg(any(windows, target_os = "linux"))]
use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_opener::OpenerExt;
const DESKTOP_TRAY_ID: &str = "main-tray";
const APP_CLOSE_REQUESTED_EVENT: &str = "dbx-app-close-requested";
#[cfg(target_os = "macos")]
const APP_MENU_QUIT_ID: &str = "app-menu-quit";
#[cfg(target_os = "macos")]
const APP_MENU_COPY_SUPPORT_INFO_ID: &str = "app-menu-copy-support-info";
#[cfg(target_os = "macos")]
const APP_MENU_CLOSE_TAB_ID: &str = "app-menu-close-tab";
#[cfg(target_os = "macos")]
const APP_CLOSE_ACTIVE_TAB_EVENT: &str = "dbx-close-active-tab";
pub struct CloseBehaviorState {
confirmed_exit: AtomicBool,
frontend_ready: AtomicBool,
}
impl CloseBehaviorState {
fn new() -> Self {
Self { confirmed_exit: AtomicBool::new(false), frontend_ready: AtomicBool::new(false) }
}
pub(crate) fn allow_next_exit(&self) {
self.confirmed_exit.store(true, Ordering::Relaxed);
}
fn take_confirmed_exit(&self) -> bool {
self.confirmed_exit.swap(false, Ordering::Relaxed)
}
pub(crate) fn set_frontend_ready(&self, ready: bool) {
self.frontend_ready.store(ready, Ordering::Release);
}
fn is_frontend_ready(&self) -> bool {
self.frontend_ready.load(Ordering::Acquire)
}
}
/// UI language pushed from the frontend i18n layer; native menus follow it and
/// fall back to the OS locale until the first `set_app_locale` call arrives.
pub struct AppLocaleState {
locale: std::sync::Mutex<Option<String>>,
}
impl AppLocaleState {
fn new() -> Self {
Self { locale: std::sync::Mutex::new(None) }
}
pub(crate) fn set(&self, locale: String) {
*self.locale.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(locale);
}
fn get(&self) -> String {
self.locale
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
.unwrap_or_else(|| sys_locale::get_locale().unwrap_or_default())
}
}
#[cfg(target_os = "macos")]
const MACOS_TRAY_ICON: tauri::image::Image<'_> = tauri::include_image!("icons/tray-macos-template.png");
#[cfg(target_os = "macos")]
const ABOUT_APP_ICON: tauri::image::Image<'_> = tauri::include_image!("icons/icon.png");
#[cfg(not(target_os = "macos"))]
const BLACK_APP_ICON: tauri::image::Image<'_> = tauri::include_image!("icons/icon-black.png");
#[cfg(target_os = "macos")]
const MACOS_DEFAULT_APP_ICON: &[u8] = include_bytes!("../icons/icon.icns");
#[cfg(target_os = "macos")]
const MACOS_DARK_APP_ICON: &[u8] = include_bytes!("../icons/icon-macos-dark.icns");
pub(crate) fn apply_debug_log_level(debug_logging_enabled: bool) {
log::set_max_level(if debug_logging_enabled { log::LevelFilter::Debug } else { log::LevelFilter::Off });
}
fn should_hide_window_on_close(target_os: &str) -> bool {
matches!(target_os, "macos" | "windows")
}
/// How long to keep the app off-screen after hiding it and before the process
/// exits, so WindowServer has removed the window before WKWebView teardown.
#[cfg(target_os = "macos")]
pub(crate) const EXIT_HIDE_GRACE_MS: u64 = 250;
/// On macOS, tearing down WKWebView while the window is still on screen can
/// paint the window red for a frame before the process exits, so the app is
/// hidden first. Windows and Linux keep their existing exit behavior.
fn should_hide_window_before_exit(target_os: &str) -> bool {
target_os == "macos"
}
fn should_setup_desktop_tray(target_os: &str, show_tray_icon: bool, linux_appindicator_available: bool) -> bool {
show_tray_icon
&& (matches!(target_os, "macos" | "windows") || (target_os == "linux" && linux_appindicator_available))
}
fn should_enable_single_instance(debug_build: bool) -> bool {
!debug_build
}
fn startup_data_dir_mode(mode: &data_dir::DataDirMode) -> &'static str {
match mode {
data_dir::DataDirMode::Default => "default",
data_dir::DataDirMode::EnvOverride => "env_override",
data_dir::DataDirMode::Portable { .. } => "portable",
}
}
#[cfg(target_os = "macos")]
fn development_dock_badge_label(debug_build: bool) -> Option<&'static str> {
debug_build.then_some("DEV")
}
#[cfg(target_os = "linux")]
fn linux_appindicator_available() -> bool {
const APPINDICATOR_LIBRARIES: &[&str] = &["libayatana-appindicator3.so.1", "libappindicator3.so.1"];
APPINDICATOR_LIBRARIES.iter().any(|library| {
// tray-icon loads AppIndicator dynamically and panics when neither ABI is
// installed, so probe the same libraries before entering that code path.
unsafe { libloading::Library::new(library).is_ok() }
})
}
/// Without a compositing manager (XFCE with compositing off, i3, openbox, ...) a transparent
/// window renders black where it should be see-through.
#[cfg(target_os = "linux")]
pub(crate) fn linux_compositing_available() -> bool {
gtk::gdk::Screen::default().is_some_and(|screen| screen.is_composited())
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn should_enable_linux_floating_frame(compositing: bool, opt_in: Option<&std::ffi::OsStr>) -> bool {
compositing && opt_in == Some(std::ffi::OsStr::new("1"))
}
/// The Linux config declares the main window with `create: false` so its transparency can
/// be explicitly opted into at runtime when a compositor is available.
#[cfg(target_os = "linux")]
fn create_linux_main_window(app: &tauri::App) -> tauri::Result<()> {
let Some(config) = app.config().app.windows.iter().find(|window| window.label == "main").cloned() else {
return Ok(());
};
let compositing = linux_compositing_available();
let opt_in = std::env::var_os("DBX_LINUX_FLOATING_FRAME");
let floating_frame = should_enable_linux_floating_frame(compositing, opt_in.as_deref());
log::info!("[WINDOW] Linux main window compositing={compositing}; floating_frame={floating_frame}");
let mut builder = tauri::WebviewWindowBuilder::from_config(app.handle(), &config)?.transparent(floating_frame);
if floating_frame {
// Read by index.html before first paint to draw the rounded, shadowed frame.
builder = builder.initialization_script("window.__DBX_LINUX_FLOATING__ = true;");
}
builder.build()?;
Ok(())
}
#[cfg(not(target_os = "linux"))]
fn linux_appindicator_available() -> bool {
false
}
#[cfg(test)]
fn uses_application_level_icon(target_os: &str) -> bool {
target_os == "macos"
}
fn should_show_main_window_after_setup() -> bool {
true
}
fn should_show_main_window_before_setup_tasks() -> bool {
true
}
fn append_startup_probe(message: impl AsRef<str>) {
startup_recovery::record(message);
}
pub(crate) fn clear_startup_probe_after_frontend_ready(main_window_visible: bool) {
startup_recovery::mark_frontend_ready(main_window_visible);
}
fn should_confirm_app_exit_request(target_os: &str, exit_code: Option<i32>, confirmed_exit: bool) -> bool {
should_hide_window_on_close(target_os) && exit_code != Some(tauri::RESTART_EXIT_CODE) && !confirmed_exit
}
fn should_fallback_to_native_quit(target: &str, frontend_ready: bool) -> bool {
target == "quit" && !frontend_ready
}
fn native_window_decorations_override(target_os: &str) -> Option<bool> {
match target_os {
"windows" | "linux" => Some(false),
_ => None,
}
}
#[cfg(target_os = "macos")]
fn build_app_menu<R: tauri::Runtime>(app_handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<R>> {
let pkg_info = app_handle.package_info();
let app_name = pkg_info.name.clone();
let about_metadata = AboutMetadata {
name: Some(app_name.clone()),
version: Some(pkg_info.version.to_string()),
copyright: Some(commands::support_info::format_support_info_for_native_about()),
icon: Some(ABOUT_APP_ICON),
..Default::default()
};
let copy_support_info_item = MenuItem::with_id(
app_handle,
APP_MENU_COPY_SUPPORT_INFO_ID,
app_menu_copy_support_info_label(&current_app_locale(app_handle)),
true,
None::<&str>,
)?;
let quit_item = MenuItem::with_id(
app_handle,
APP_MENU_QUIT_ID,
app_menu_quit_label(&current_app_locale(app_handle), &app_name),
true,
Some("Cmd+Q"),
)?;
let close_tab_item = MenuItem::with_id(
app_handle,
APP_MENU_CLOSE_TAB_ID,
app_menu_close_tab_label(&current_app_locale(app_handle)),
true,
Some("Cmd+W"),
)?;
Menu::with_items(
app_handle,
&[
&Submenu::with_items(
app_handle,
app_name,
true,
&[
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?,
&copy_support_info_item,
&PredefinedMenuItem::separator(app_handle)?,
&PredefinedMenuItem::services(app_handle, None)?,
&PredefinedMenuItem::separator(app_handle)?,
&PredefinedMenuItem::hide(app_handle, None)?,
&PredefinedMenuItem::hide_others(app_handle, None)?,
&PredefinedMenuItem::separator(app_handle)?,
&quit_item,
],
)?,
&Submenu::with_items(app_handle, "File", true, &[&close_tab_item])?,
&Submenu::with_items(
app_handle,
"Edit",
true,
&[
&PredefinedMenuItem::undo(app_handle, None)?,
&PredefinedMenuItem::redo(app_handle, None)?,
&PredefinedMenuItem::separator(app_handle)?,
&PredefinedMenuItem::cut(app_handle, None)?,
&PredefinedMenuItem::copy(app_handle, None)?,
&PredefinedMenuItem::paste(app_handle, None)?,
&PredefinedMenuItem::select_all(app_handle, None)?,
],
)?,
&Submenu::with_items(app_handle, "View", true, &[&PredefinedMenuItem::fullscreen(app_handle, None)?])?,
&Submenu::with_items(
app_handle,
"Window",
true,
&[&PredefinedMenuItem::minimize(app_handle, None)?, &PredefinedMenuItem::maximize(app_handle, None)?],
)?,
&Submenu::with_items(app_handle, "Help", true, &[])?,
],
)
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum LinuxNvidiaDriver {
None,
Nouveau,
Proprietary,
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
#[derive(Clone, Debug, Eq, PartialEq)]
struct LinuxDrmRenderDevice {
device_file: std::path::PathBuf,
driver: Option<String>,
boot_vga: bool,
pci_id: Option<(u16, u16)>,
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct LinuxDmabufRendererPciQuirk {
vendor_id: u16,
device_id: u16,
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
const LINUX_DMABUF_RENDERER_PCI_QUIRKS: &[LinuxDmabufRendererPciQuirk] = &[LinuxDmabufRendererPciQuirk {
// Strix Halo can stop presenting new WebKitGTK DMABuf frames while the
// WebView remains interactive on native Wayland.
vendor_id: 0x1002,
device_id: 0x1586,
}];
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn linux_nvidia_driver_from_state(
proprietary_control_exists: bool,
proprietary_proc_exists: bool,
render_driver: Option<&str>,
) -> LinuxNvidiaDriver {
if proprietary_control_exists || proprietary_proc_exists {
LinuxNvidiaDriver::Proprietary
} else if render_driver.is_some_and(|driver| driver.eq_ignore_ascii_case("nouveau")) {
LinuxNvidiaDriver::Nouveau
} else {
LinuxNvidiaDriver::None
}
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn linux_selected_drm_render_device<'a>(
explicit_device_file: Option<&std::path::Path>,
devices: &'a [LinuxDrmRenderDevice],
) -> Option<&'a LinuxDrmRenderDevice> {
if let Some(explicit_device_file) = explicit_device_file {
// WebKit gives this environment override precedence over EGL/DRM discovery.
return devices.iter().find(|device| device.device_file.as_path() == explicit_device_file);
}
// Before WebKit initializes EGL, boot_vga is the best available default-display signal.
// The sorted first render node mirrors WebKit's final DRM-device fallback.
devices.iter().find(|device| device.boot_vga).or_else(|| devices.first())
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn linux_pci_id_from_sysfs_value(value: &str) -> Option<u16> {
let value = value.trim();
let value = value.strip_prefix("0x").or_else(|| value.strip_prefix("0X")).unwrap_or(value);
(!value.is_empty()).then(|| u16::from_str_radix(value, 16).ok()).flatten()
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn linux_drm_render_devices_from_paths(
sys_class_drm: &std::path::Path,
dev_dri: &std::path::Path,
) -> Vec<LinuxDrmRenderDevice> {
let Ok(entries) = std::fs::read_dir(sys_class_drm) else {
return Vec::new();
};
let mut devices = entries
.filter_map(Result::ok)
.filter_map(|entry| {
let node_name = entry.file_name();
let node_name = node_name.to_str()?;
let render_index = node_name.strip_prefix("renderD")?;
if render_index.is_empty() || !render_index.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
let device_file = dev_dri.join(node_name);
if std::fs::OpenOptions::new().read(true).write(true).open(&device_file).is_err() {
return None;
}
let device_path = entry.path().join("device");
let driver = std::fs::read_link(device_path.join("driver"))
.ok()
.and_then(|path| path.file_name().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase));
let boot_vga = std::fs::read_to_string(device_path.join("boot_vga")).is_ok_and(|value| value.trim() == "1");
let vendor_id = std::fs::read_to_string(device_path.join("vendor"))
.ok()
.and_then(|value| linux_pci_id_from_sysfs_value(&value));
let device_id = std::fs::read_to_string(device_path.join("device"))
.ok()
.and_then(|value| linux_pci_id_from_sysfs_value(&value));
Some(LinuxDrmRenderDevice { device_file, driver, boot_vga, pci_id: vendor_id.zip(device_id) })
})
.collect::<Vec<_>>();
devices.sort_by(|left, right| left.device_file.cmp(&right.device_file));
devices
}
#[cfg(target_os = "linux")]
fn linux_drm_render_devices() -> Vec<LinuxDrmRenderDevice> {
linux_drm_render_devices_from_paths(std::path::Path::new("/sys/class/drm"), std::path::Path::new("/dev/dri"))
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
const LINUX_SOFTWARE_ONLY_DRM_DRIVERS: &[&str] = &[
"virtio-pci", // QEMU/KVM virtio-gpu: 2D dumb-buffer only, GL falls back to llvmpipe
"virtio_gpu", // virtio-gpu on virtio-mmio/platform buses
"qxl", // QEMU/SPICE 2D display adapter
"bochs", // QEMU/BOCHS VGA (2D only)
"cirrus", // legacy Cirrus VGA (2D only)
"vmwgfx", // VMware SVGA
"vboxvideo", // VirtualBox graphics
"xen", // Xen virtual GPU
"udl", // DisplayLink 2D framebuffer
"mgag200", // Matrox server BMC
"ast", // ASPEED server BMC
"hibmc", // Huawei HiBMC server BMC
];
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn linux_drm_driver_is_software_only(driver: Option<&str>) -> bool {
// 2D-only/virtual drivers leave GL rendering to llvmpipe, where WebKitGTK's
// DMABuf compositing drives the gallivm LLVM JIT that can fail to
// materialize compositing shaders (blank window on GPU-less VMs).
driver.is_none_or(|driver| LINUX_SOFTWARE_ONLY_DRM_DRIVERS.contains(&driver))
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn linux_selected_device_has_dmabuf_quirk(
selected_device: Option<&LinuxDrmRenderDevice>,
uses_native_wayland: bool,
) -> bool {
uses_native_wayland
&& selected_device.is_some_and(|device| {
let Some((vendor_id, device_id)) = device.pci_id else {
return false;
};
LINUX_DMABUF_RENDERER_PCI_QUIRKS
.iter()
.any(|quirk| vendor_id == quirk.vendor_id && device_id == quirk.device_id)
})
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn linux_webkit_rendering_workarounds(
driver: LinuxNvidiaDriver,
has_hardware_render_device: bool,
selected_device: Option<&LinuxDrmRenderDevice>,
uses_native_wayland: bool,
) -> &'static [(&'static str, &'static str)] {
match driver {
LinuxNvidiaDriver::Proprietary => {
// NVIDIA's proprietary driver needs both DMABuf and explicit-sync
// workarounds to avoid blank windows and compositor failures.
&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1"), ("__NV_DISABLE_EXPLICIT_SYNC", "1")]
}
LinuxNvidiaDriver::Nouveau => {
// WebKitGTK's DMABuf renderer can produce a fully black WebView on
// Nouveau while the DOM remains interactive.
&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1")]
}
LinuxNvidiaDriver::None if !has_hardware_render_device => {
// No hardware render device means Mesa can only render through
// llvmpipe. WebKitGTK's DMABuf compositing then drives llvmpipe's
// gallivm LLVM JIT, which can fail to materialize the compositing
// shaders (blank/white window on GPU-less VMs and servers), so
// disable the DMABuf renderer there as well.
&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1")]
}
LinuxNvidiaDriver::None if linux_selected_device_has_dmabuf_quirk(selected_device, uses_native_wayland) => {
&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1")]
}
LinuxNvidiaDriver::None => {
// AMD / Intel and other Mesa drivers keep DMABuf enabled to avoid
// unnecessary CPU usage and UI lag on Wayland.
&[]
}
}
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn linux_webkit_environment_override<'a>(
existing_value: Option<&std::ffi::OsStr>,
workaround_value: &'a str,
) -> Option<&'a str> {
existing_value.is_none().then_some(workaround_value)
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn linux_appimage_requires_dmabuf_workaround(appimage: Option<&std::ffi::OsStr>) -> bool {
appimage.is_some_and(|value| !value.is_empty())
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn linux_uses_native_wayland(
wayland_display: Option<&std::ffi::OsStr>,
session_type: Option<&std::ffi::OsStr>,
gdk_backend: Option<&std::ffi::OsStr>,
) -> bool {
let has_wayland_display = wayland_display.is_some_and(|value| !value.is_empty());
let is_wayland_session =
session_type.and_then(std::ffi::OsStr::to_str).is_some_and(|value| value.eq_ignore_ascii_case("wayland"));
if !has_wayland_display || !is_wayland_session {
return false;
}
gdk_backend.is_none_or(|backends| {
backends
.to_string_lossy()
.split(',')
.next()
.is_some_and(|backend| backend.trim().eq_ignore_ascii_case("wayland"))
})
}
#[cfg(target_os = "linux")]
fn apply_linux_webkit_rendering_workarounds() {
let render_devices = linux_drm_render_devices();
let appimage = std::env::var_os("APPIMAGE");
let explicit_device_file = std::env::var_os("WEBKIT_WEB_RENDER_DEVICE_FILE")
.filter(|path| !path.is_empty())
.map(std::path::PathBuf::from)
// Resolve stable /dev/dri/by-path links to the renderD* node used by sysfs.
.map(|path| std::fs::canonicalize(&path).unwrap_or(path));
let selected_device = linux_selected_drm_render_device(explicit_device_file.as_deref(), &render_devices);
let nvidia_driver = linux_nvidia_driver_from_state(
std::path::Path::new("/dev/nvidiactl").exists(),
std::path::Path::new("/proc/driver/nvidia/version").exists(),
selected_device.and_then(|device| device.driver.as_deref()),
);
let has_hardware_render_device =
render_devices.iter().any(|device| !linux_drm_driver_is_software_only(device.driver.as_deref()));
let uses_native_wayland = linux_uses_native_wayland(
std::env::var_os("WAYLAND_DISPLAY").as_deref(),
std::env::var_os("XDG_SESSION_TYPE").as_deref(),
std::env::var_os("GDK_BACKEND").as_deref(),
);
// AppImages bundle WebKitGTK/GTK but use the host EGL/GL stack. On some
// combinations, WebKit's DMABUF initialization aborts the WebProcess
// before it can fall back to software rendering. Keep this opt-out
// user-overridable and use the stable shared-memory renderer instead.
if linux_appimage_requires_dmabuf_workaround(appimage.as_deref())
&& linux_webkit_environment_override(std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").as_deref(), "1")
.is_some()
{
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
}
for (key, value) in linux_webkit_rendering_workarounds(
nvidia_driver,
has_hardware_render_device,
selected_device,
uses_native_wayland,
) {
if let Some(value) = linux_webkit_environment_override(std::env::var_os(key).as_deref(), value) {
std::env::set_var(key, value);
}
}
}
/// Brings the main window to the foreground, reporting whether it ended up visible.
///
/// The individual calls used to be discarded with `let _ =`, which made a failed
/// reveal completely silent. That matters on macOS: an instance that has lost its
/// WindowServer connection stays alive and idle but can no longer present a window
/// or a tray icon, and the single-instance guard keeps handing later launches to it,
/// so the app looks like it simply does not start. Logging here is what makes that
/// state diagnosable at all.
fn show_main_window<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> bool {
let Some(window) = app.get_webview_window("main") else {
eprintln!("[WINDOW] show_main_window: no \"main\" webview window to reveal");
return false;
};
if let Err(err) = window.show() {
eprintln!("[WINDOW] show_main_window: show() failed: {err}");
}
if let Err(err) = window.unminimize() {
eprintln!("[WINDOW] show_main_window: unminimize() failed: {err}");
}
if let Err(err) = window.set_focus() {
eprintln!("[WINDOW] show_main_window: set_focus() failed: {err}");
}
match window.is_visible() {
Ok(visible) => visible,
Err(err) => {
eprintln!("[WINDOW] show_main_window: is_visible() failed: {err}");
false
}
}
}
fn main_window_probe_state<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> String {
let Some(window) = app.get_webview_window("main") else {
return "main_window=missing".to_string();
};
format!(
"main_window visible={:?} minimized={:?} maximized={:?} fullscreen={:?} position={:?} size={:?}",
window.is_visible(),
window.is_minimized(),
window.is_maximized(),
window.is_fullscreen(),
window.outer_position(),
window.outer_size()
)
}
fn prepare_main_window_for_display<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
if let Some(decorations) = native_window_decorations_override(std::env::consts::OS) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_decorations(decorations);
}
}
window_state_guard::enforce_main_window_bounds(app);
}
fn clear_main_webview_focus<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.eval(
r#"
(() => {
const active = document.activeElement;
if (active instanceof HTMLElement) active.blur();
if (document.body) {
if (!document.body.hasAttribute("tabindex")) {
document.body.setAttribute("tabindex", "-1");
}
document.body.focus({ preventScroll: true });
}
})();
"#,
);
}
}
pub(crate) fn hide_main_window_for_close<R: tauri::Runtime>(app: &tauri::AppHandle<R>, window: &tauri::Window<R>) {
clear_main_webview_focus(app);
#[cfg(target_os = "macos")]
{
if window.is_fullscreen().unwrap_or(false) {
let app = app.clone();
let window = window.clone();
let _ = window.set_fullscreen(false);
tauri::async_runtime::spawn(async move {
for _ in 0..40 {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
if !window.is_fullscreen().unwrap_or(false) {
tokio::time::sleep(std::time::Duration::from_millis(600)).await;
let app_to_hide = app.clone();
let window_to_hide = window.clone();
let _ = app.run_on_main_thread(move || {
let _ = window_to_hide.hide();
let _ = app_to_hide.hide();
});
return;
}
}
let app_to_hide = app.clone();
let window_to_hide = window.clone();
let _ = app.run_on_main_thread(move || {
let _ = window_to_hide.hide();
let _ = app_to_hide.hide();
});
});
return;
}
}
let _ = window.hide();
}
pub(crate) fn request_app_close<R: tauri::Runtime>(app: &tauri::AppHandle<R>, target: &str) {
let frontend_ready = app.try_state::<CloseBehaviorState>().is_some_and(|state| state.is_frontend_ready());
if should_fallback_to_native_quit(target, frontend_ready) {
// A missing WebView2 runtime can prevent the frontend listener from ever
// loading. Only the explicit tray Quit fallback bypasses the prompt.
if let Some(state) = app.try_state::<CloseBehaviorState>() {
state.allow_next_exit();
}
app.exit(0);
return;
}
show_main_window(app);
let _ = app.emit(APP_CLOSE_REQUESTED_EVENT, target);
}
fn open_connection_deep_links(app: &tauri::AppHandle, links: Vec<String>) {
if links.is_empty() {
return;
}
let should_emit = app
.try_state::<commands::deep_link::DeepLinkOpenState>()
.is_none_or(|state| state.route_connection_links(links.clone()));
if should_emit {
let _ = app.emit("dbx-open-connection-links", links);
}
show_main_window(app);
}
fn open_ai_config_deep_links(app: &tauri::AppHandle, links: Vec<String>) {
if links.is_empty() {
return;
}
let should_emit = app
.try_state::<commands::deep_link::DeepLinkOpenState>()
.is_none_or(|state| state.route_ai_config_links(links.clone()));
if should_emit {
let _ = app.emit("dbx-open-ai-config-links", links);
}
show_main_window(app);
}
fn open_plugin_install_deep_links(app: &tauri::AppHandle, links: Vec<String>) {
if links.is_empty() {
return;
}
let should_emit = app
.try_state::<commands::deep_link::DeepLinkOpenState>()
.is_none_or(|state| state.route_plugin_install_links(links.clone()));
if should_emit {
let _ = app.emit("dbx-open-plugin-install-links", links);
}
show_main_window(app);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LocaleFamily {
Azerbaijani,
English,
Thai,
SimplifiedChinese,
TraditionalChinese,
Japanese,
Korean,
Spanish,
Indonesian,
Italian,
Portuguese,
Russian,
Turkish,
}
// Mirrors the frontend language mapping in apps/desktop/src/i18n/index.ts
// (localeFromLanguageTag) so native menus agree with the UI language.
fn locale_family(locale: &str) -> LocaleFamily {
let normalized = locale.replace('_', "-").to_ascii_lowercase();
let is_language = |language: &str| normalized == language || normalized.starts_with(&format!("{language}-"));
if is_language("zh") {
if normalized.contains("hant")
|| normalized.starts_with("zh-tw")
|| normalized.starts_with("zh-hk")
|| normalized.starts_with("zh-mo")
{
LocaleFamily::TraditionalChinese
} else {
LocaleFamily::SimplifiedChinese
}
} else if is_language("ja") {
LocaleFamily::Japanese
} else if is_language("th") {
LocaleFamily::Thai
} else if is_language("ko") {
LocaleFamily::Korean
} else if is_language("az") {
LocaleFamily::Azerbaijani
} else if is_language("es") {
LocaleFamily::Spanish
} else if is_language("id") {
LocaleFamily::Indonesian
} else if is_language("tr") {
LocaleFamily::Turkish
} else if is_language("it") {
LocaleFamily::Italian
} else if is_language("pt") {
LocaleFamily::Portuguese
} else if is_language("ru") {
LocaleFamily::Russian
} else {
LocaleFamily::English
}
}
fn tray_menu_labels_for_locale(locale: &str) -> (&'static str, &'static str) {
match locale_family(locale) {
LocaleFamily::SimplifiedChinese => ("显示 DBX", "退出 DBX"),
LocaleFamily::TraditionalChinese => ("顯示 DBX", "退出 DBX"),
LocaleFamily::Thai => ("แสดง DBX", "ออกจาก DBX"),
LocaleFamily::Japanese => ("DBXを表示", "DBXを終了"),
LocaleFamily::Korean => ("DBX 표시", "DBX 종료"),
LocaleFamily::Azerbaijani => ("DBX-i göstər", "DBX-dən çıx"),
LocaleFamily::Spanish => ("Mostrar DBX", "Salir de DBX"),
LocaleFamily::Indonesian => ("Tampilkan DBX", "Keluar dari DBX"),
LocaleFamily::Italian => ("Mostra DBX", "Esci da DBX"),
LocaleFamily::Turkish => ("DBX'i Göster", "DBX'ten Çık"),
LocaleFamily::Portuguese => ("Mostrar DBX", "Sair do DBX"),
LocaleFamily::Russian => ("Показать DBX", "Выйти из DBX"),
LocaleFamily::English => ("Show DBX", "Quit DBX"),
}
}
// Matches the frontend supportInfoCopy translations in apps/desktop/src/i18n/locales/*.ts.
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
fn app_menu_copy_support_info_label(locale: &str) -> &'static str {
match locale_family(locale) {
LocaleFamily::SimplifiedChinese => "复制支持信息",
LocaleFamily::TraditionalChinese => "複製支援資訊",
LocaleFamily::Thai => "คัดลอกข้อมูลสำหรับขอความช่วยเหลือ",
LocaleFamily::Japanese => "サポート情報をコピー",
LocaleFamily::Korean => "지원 정보 복사",
LocaleFamily::Azerbaijani => "Dəstək məlumatlarını kopyala",
LocaleFamily::Spanish => "Copiar información",
LocaleFamily::Indonesian => "Salin Info Dukungan",
LocaleFamily::Italian => "Copia informazioni",
LocaleFamily::Turkish => "Destek bilgilerini kopyala",
LocaleFamily::Portuguese => "Copiar informações",
LocaleFamily::Russian => "Копировать сведения о поддержке",
LocaleFamily::English => "Copy Support Info",
}
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
fn app_menu_close_tab_label(locale: &str) -> &'static str {
match locale_family(locale) {
LocaleFamily::SimplifiedChinese => "关闭标签页",
LocaleFamily::TraditionalChinese => "關閉分頁",
LocaleFamily::Thai => "ปิดแท็บ",
LocaleFamily::Japanese => "タブを閉じる",
LocaleFamily::Korean => "탭 닫기",
LocaleFamily::Azerbaijani => "Vərəqi bağla",
LocaleFamily::Spanish => "Cerrar pestaña",
LocaleFamily::Indonesian => "Tutup Tab",
LocaleFamily::Italian => "Chiudi scheda",
LocaleFamily::Turkish => "Sekmeyi kapat",
LocaleFamily::Portuguese => "Fechar aba",
LocaleFamily::Russian => "Закрыть вкладку",
LocaleFamily::English => "Close Tab",
}
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
fn app_menu_quit_label(locale: &str, app_name: &str) -> String {
match locale_family(locale) {
LocaleFamily::SimplifiedChinese | LocaleFamily::TraditionalChinese => format!("退出 {app_name}"),
LocaleFamily::Thai => format!("ออกจาก {app_name}"),
LocaleFamily::Japanese => format!("{app_name}を終了"),
LocaleFamily::Korean => format!("{app_name} 종료"),
LocaleFamily::Azerbaijani => format!("{app_name}-dən çıx"),
LocaleFamily::Spanish => format!("Salir de {app_name}"),
LocaleFamily::Indonesian => format!("Keluar dari {app_name}"),
LocaleFamily::Italian => format!("Esci da {app_name}"),
LocaleFamily::Turkish => format!("{app_name} Uygulamasından Çık"),
LocaleFamily::Portuguese => format!("Sair do {app_name}"),
LocaleFamily::Russian => format!("Выйти из {app_name}"),
LocaleFamily::English => format!("Quit {app_name}"),
}
}
fn current_app_locale<R: tauri::Runtime, M: Manager<R>>(manager: &M) -> String {
match manager.try_state::<AppLocaleState>() {
Some(state) => state.get(),
None => sys_locale::get_locale().unwrap_or_default(),
}
}
fn build_tray_menu<R: tauri::Runtime, M: Manager<R>>(manager: &M) -> tauri::Result<tauri::menu::Menu<R>> {
let (show_label, quit_label) = tray_menu_labels_for_locale(&current_app_locale(manager));
MenuBuilder::new(manager).text("show", show_label).separator().text("quit", quit_label).build()
}
/// Rebuilds the tray menu (and the macOS app menu) so native labels follow the
/// UI language after the frontend reports a locale change.
pub(crate) fn refresh_native_menus(app: &tauri::AppHandle) -> tauri::Result<()> {
if let Some(tray) = app.tray_by_id(DESKTOP_TRAY_ID) {
tray.set_menu(Some(build_tray_menu(app)?))?;
}
#[cfg(target_os = "macos")]
{
let _ = app.set_menu(build_app_menu(app)?)?;
}
Ok(())
}
#[cfg_attr(not(any(target_os = "macos", target_os = "windows")), allow(dead_code))]
fn setup_desktop_tray<R: tauri::Runtime, M: Manager<R>>(
manager: &M,
_icon_theme: DesktopIconTheme,
) -> tauri::Result<()> {
let menu = build_tray_menu(manager)?;
let mut tray =
TrayIconBuilder::<R>::with_id(DESKTOP_TRAY_ID).tooltip("DBX").menu(&menu).show_menu_on_left_click(false);
#[cfg(target_os = "macos")]
{
tray = tray.icon(MACOS_TRAY_ICON).icon_as_template(true);
}
#[cfg(target_os = "windows")]
{
let icon = match _icon_theme {
DesktopIconTheme::Default => manager.app_handle().default_window_icon().cloned(),
DesktopIconTheme::Black => Some(BLACK_APP_ICON),
};
if let Some(icon) = icon {
tray = tray.icon(icon);
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
if let Some(icon) = manager.app_handle().default_window_icon().cloned() {
tray = tray.icon(icon);
}
}
tray.on_menu_event(|app, event| {
if event.id() == "show" {
show_main_window(app);
} else if event.id() == "quit" {
request_app_close(app, "quit");
}
})
.on_tray_icon_event(|tray, event| match event {
TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. }
| TrayIconEvent::DoubleClick { button: MouseButton::Left, .. } => {
show_main_window(tray.app_handle());
}
_ => {}
})
.build(manager)?;
Ok(())
}
#[cfg(target_os = "macos")]
fn apply_macos_app_icon_theme(
app: &tauri::AppHandle,
icon_theme: DesktopIconTheme,
persist_bundle_icon: bool,
) -> tauri::Result<()> {
use objc2::{AllocAnyThread, MainThreadMarker};
use objc2_app_kit::{NSApplication, NSImage, NSWorkspace, NSWorkspaceIconCreationOptions};
use objc2_foundation::{NSBundle, NSData, NSString};
let icon_bytes = match icon_theme {
DesktopIconTheme::Default => MACOS_DEFAULT_APP_ICON,
DesktopIconTheme::Black => MACOS_DARK_APP_ICON,
};
app.run_on_main_thread(move || {
// macOS has no per-window icon. Update NSApplication so the Dock and
// app switcher reflect the selected theme immediately.
let marker = unsafe { MainThreadMarker::new_unchecked() };
let application = NSApplication::sharedApplication(marker);
let workspace = NSWorkspace::sharedWorkspace();
let app_bundle_path = NSBundle::mainBundle().bundlePath();
let data = NSData::with_bytes(icon_bytes);
if let Some(icon) = NSImage::initWithData(NSImage::alloc(), &data) {
unsafe { application.setApplicationIconImage(Some(&icon)) };
// NSApplication only changes the icon for this process lifetime.
// Persist the selected icon on the app bundle so Finder and the
// Dock keep showing it after DBX exits. A nil image removes the
// custom icon and restores the bundle's packaged default.
if persist_bundle_icon {
let persistent_icon = match icon_theme {
DesktopIconTheme::Default => None,
DesktopIconTheme::Black => Some(&*icon),
};
if app_bundle_path.hasSuffix(&NSString::from_str(".app"))
&& !workspace.setIcon_forFile_options(
persistent_icon,
&app_bundle_path,
NSWorkspaceIconCreationOptions::empty(),
)
{
log::warn!("Failed to persist the selected macOS app icon; the app bundle may not be writable");
}
}
} else {
log::warn!("Failed to decode the selected macOS application icon");
}
})
}
#[cfg(target_os = "macos")]
fn apply_macos_development_dock_badge(app: &tauri::AppHandle) -> tauri::Result<()> {
use objc2::MainThreadMarker;
use objc2_app_kit::NSApplication;
use objc2_foundation::NSString;
let badge_label = development_dock_badge_label(cfg!(debug_assertions));
app.run_on_main_thread(move || {
let marker = unsafe { MainThreadMarker::new_unchecked() };
let application = NSApplication::sharedApplication(marker);
let badge_label = badge_label.map(NSString::from_str);
application.dockTile().setBadgeLabel(badge_label.as_deref());
})
}
pub(crate) fn apply_desktop_icon_theme(app: &tauri::AppHandle, icon_theme: DesktopIconTheme) -> tauri::Result<()> {
#[cfg(target_os = "macos")]
{
apply_macos_app_icon_theme(app, icon_theme, true)
}
#[cfg(not(target_os = "macos"))]
if let Some(window) = app.get_webview_window("main") {
match icon_theme {
DesktopIconTheme::Default => {
if let Some(icon) = app.default_window_icon().cloned() {
window.set_icon(icon)?;
}
}
DesktopIconTheme::Black => window.set_icon(BLACK_APP_ICON)?,
}
}
#[cfg(not(target_os = "macos"))]
Ok(())
}
fn apply_desktop_tray_icon_theme(app: &tauri::AppHandle, _icon_theme: DesktopIconTheme) -> tauri::Result<()> {
if let Some(_tray) = app.tray_by_id(DESKTOP_TRAY_ID) {
#[cfg(target_os = "windows")]
{
let icon = match _icon_theme {
DesktopIconTheme::Default => app.default_window_icon().cloned(),
DesktopIconTheme::Black => Some(BLACK_APP_ICON),
};
_tray.set_icon(icon)?;
}
#[cfg(target_os = "linux")]
{
let icon = match _icon_theme {
DesktopIconTheme::Default => app.default_window_icon().cloned(),
DesktopIconTheme::Black => Some(BLACK_APP_ICON),
};
_tray.set_icon(icon)?;
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
let _ = (_tray, _icon_theme);
}
}
Ok(())
}
pub(crate) fn apply_desktop_settings(app: &tauri::AppHandle, desktop_settings: &DesktopSettings) -> tauri::Result<()> {
apply_debug_log_level(desktop_settings.debug_logging_enabled);
apply_desktop_icon_theme(app, desktop_settings.icon_theme)?;
if should_setup_desktop_tray(std::env::consts::OS, desktop_settings.show_tray_icon, linux_appindicator_available())
{
if let Some(tray) = app.tray_by_id(DESKTOP_TRAY_ID) {
tray.set_visible(desktop_settings.show_tray_icon)?;
apply_desktop_tray_icon_theme(app, desktop_settings.icon_theme)?;
} else if desktop_settings.show_tray_icon {
setup_desktop_tray(app, desktop_settings.icon_theme)?;
}
}
Ok(())
}
async fn shutdown_runtime_resources(app_handle: &tauri::AppHandle) {
if let Some(backups) = app_handle.try_state::<background_backup::BackgroundBackup>() {
backups.shutdown().await;
}
if let Some(server) = app_handle.try_state::<commands::redis_pubsub_server::PubSubServerState>() {
server.shutdown(Duration::from_secs(1)).await;
}
if let Some(state) = app_handle.try_state::<Arc<AppState>>() {
state.shutdown(Duration::from_secs(3)).await;
}
}
fn run_shutdown_on_dedicated_thread(shutdown: impl FnOnce() + Send + 'static) -> Result<(), String> {
std::thread::Builder::new()
.name("dbx-runtime-shutdown".into())
.spawn(shutdown)
.map_err(|error| format!("failed to start runtime shutdown: {error}"))?
.join()
.map_err(|_| "runtime shutdown thread panicked".to_string())
}
pub(crate) fn cleanup_before_updater_forced_exit(app_handle: &tauri::AppHandle) {
let shutdown_app = app_handle.clone();
if let Err(error) = run_shutdown_on_dedicated_thread(move || {
tauri::async_runtime::block_on(shutdown_runtime_resources(&shutdown_app));
}) {
log::warn!("Failed to shut down DBX runtime resources before updater exit: {error}");
}
app_handle.cleanup_before_exit();
}
#[cfg(test)]
#[allow(clippy::items_after_test_module)]
mod tests {
use super::{
app_menu_close_tab_label, app_menu_copy_support_info_label, app_menu_quit_label,
linux_appimage_requires_dmabuf_workaround, linux_drm_driver_is_software_only,
linux_drm_render_devices_from_paths, linux_nvidia_driver_from_state, linux_pci_id_from_sysfs_value,
linux_selected_drm_render_device, linux_uses_native_wayland, linux_webkit_environment_override,
linux_webkit_rendering_workarounds, native_window_decorations_override, run_shutdown_on_dedicated_thread,
should_confirm_app_exit_request, should_enable_linux_floating_frame, should_enable_single_instance,
should_fallback_to_native_quit, should_hide_window_before_exit, should_hide_window_on_close,
should_setup_desktop_tray, should_show_main_window_after_setup, should_show_main_window_before_setup_tasks,
startup_data_dir_mode, tray_menu_labels_for_locale, uses_application_level_icon, LinuxDrmRenderDevice,
LinuxNvidiaDriver,
};
use crate::data_dir::DataDirMode;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
#[test]
fn forced_exit_shutdown_waits_for_a_dedicated_cleanup_thread() {
let caller = std::thread::current().id();
let cleanup_thread = Arc::new(Mutex::new(None));
let observed_thread = cleanup_thread.clone();
run_shutdown_on_dedicated_thread(move || {
*observed_thread.lock().unwrap() = Some(std::thread::current().id());
})
.unwrap();
let cleanup_thread = cleanup_thread.lock().unwrap().expect("cleanup thread id");
assert_ne!(cleanup_thread, caller);
}
#[test]
fn linux_floating_frame_is_disabled_by_default() {
assert!(!should_enable_linux_floating_frame(true, None));
assert!(!should_enable_linux_floating_frame(false, None));
}
#[test]
fn linux_floating_frame_requires_explicit_opt_in() {
assert!(should_enable_linux_floating_frame(true, Some(OsStr::new("1"))));
for value in ["", "0", "false", "true", "auto", "yes", "01"] {
assert!(!should_enable_linux_floating_frame(true, Some(OsStr::new(value))));
}
}
#[test]
fn linux_floating_frame_requires_compositing_even_when_opted_in() {
assert!(!should_enable_linux_floating_frame(false, Some(OsStr::new("1"))));
}
#[test]
fn tray_menu_labels_follow_locale() {
assert_eq!(tray_menu_labels_for_locale("zh-CN"), ("显示 DBX", "退出 DBX"));
assert_eq!(tray_menu_labels_for_locale("zh_CN"), ("显示 DBX", "退出 DBX"));
assert_eq!(tray_menu_labels_for_locale("zh-Hans-CN"), ("显示 DBX", "退出 DBX"));
assert_eq!(tray_menu_labels_for_locale("zh"), ("显示 DBX", "退出 DBX"));
assert_eq!(tray_menu_labels_for_locale("zh-TW"), ("顯示 DBX", "退出 DBX"));
assert_eq!(tray_menu_labels_for_locale("zh-Hant-HK"), ("顯示 DBX", "退出 DBX"));
assert_eq!(tray_menu_labels_for_locale("zh-MO"), ("顯示 DBX", "退出 DBX"));
assert_eq!(tray_menu_labels_for_locale("ja-JP"), ("DBXを表示", "DBXを終了"));
assert_eq!(tray_menu_labels_for_locale("th-TH"), ("แสดง DBX", "ออกจาก DBX"));
assert_eq!(tray_menu_labels_for_locale("th_TH"), ("แสดง DBX", "ออกจาก DBX"));
assert_eq!(tray_menu_labels_for_locale("ko-KR"), ("DBX 표시", "DBX 종료"));
assert_eq!(tray_menu_labels_for_locale("az-AZ"), ("DBX-i göstər", "DBX-dən çıx"));
assert_eq!(tray_menu_labels_for_locale("es-ES"), ("Mostrar DBX", "Salir de DBX"));
assert_eq!(tray_menu_labels_for_locale("id-ID"), ("Tampilkan DBX", "Keluar dari DBX"));
assert_eq!(tray_menu_labels_for_locale("it-IT"), ("Mostra DBX", "Esci da DBX"));
assert_eq!(tray_menu_labels_for_locale("pt-BR"), ("Mostrar DBX", "Sair do DBX"));
assert_eq!(tray_menu_labels_for_locale("tr-TR"), ("DBX'i Göster", "DBX'ten Çık"));
assert_eq!(tray_menu_labels_for_locale("ru-RU"), ("Показать DBX", "Выйти из DBX"));
assert_eq!(tray_menu_labels_for_locale("en-US"), ("Show DBX", "Quit DBX"));
// Unknown and empty locales fall back to English; "ita" must not match "it".
assert_eq!(tray_menu_labels_for_locale("ita"), ("Show DBX", "Quit DBX"));
assert_eq!(tray_menu_labels_for_locale(""), ("Show DBX", "Quit DBX"));
}
#[test]
fn app_menu_labels_follow_locale() {
assert_eq!(app_menu_quit_label("zh-CN", "DBX"), "退出 DBX");
assert_eq!(app_menu_quit_label("zh-TW", "DBX"), "退出 DBX");
assert_eq!(app_menu_quit_label("ja-JP", "DBX"), "DBXを終了");
assert_eq!(app_menu_quit_label("th-TH", "DBX"), "ออกจาก DBX");
assert_eq!(app_menu_quit_label("th_TH", "DBX"), "ออกจาก DBX");
assert_eq!(app_menu_quit_label("ko-KR", "DBX"), "DBX 종료");
assert_eq!(app_menu_quit_label("tr-TR", "DBX"), "DBX Uygulamasından Çık");
assert_eq!(app_menu_quit_label("ru-RU", "DBX"), "Выйти из DBX");
assert_eq!(app_menu_quit_label("az-AZ", "DBX"), "DBX-dən çıx");
assert_eq!(app_menu_quit_label("id-ID", "DBX"), "Keluar dari DBX");
assert_eq!(app_menu_quit_label("en-US", "DBX"), "Quit DBX");
assert_eq!(app_menu_quit_label("", "DBX"), "Quit DBX");
assert_eq!(app_menu_copy_support_info_label("zh-CN"), "复制支持信息");
assert_eq!(app_menu_copy_support_info_label("zh-TW"), "複製支援資訊");
assert_eq!(app_menu_copy_support_info_label("th-TH"), "คัดลอกข้อมูลสำหรับขอความช่วยเหลือ");
assert_eq!(app_menu_copy_support_info_label("ko-KR"), "지원 정보 복사");
assert_eq!(app_menu_copy_support_info_label("tr-TR"), "Destek bilgilerini kopyala");
assert_eq!(app_menu_copy_support_info_label("ru-RU"), "Копировать сведения о поддержке");
assert_eq!(app_menu_copy_support_info_label("az-AZ"), "Dəstək məlumatlarını kopyala");
assert_eq!(app_menu_copy_support_info_label("id-ID"), "Salin Info Dukungan");
assert_eq!(app_menu_copy_support_info_label("en-US"), "Copy Support Info");
assert_eq!(app_menu_close_tab_label("zh-CN"), "关闭标签页");
assert_eq!(app_menu_close_tab_label("zh-TW"), "關閉分頁");
assert_eq!(app_menu_close_tab_label("ja-JP"), "タブを閉じる");
assert_eq!(app_menu_close_tab_label("th-TH"), "ปิดแท็บ");
assert_eq!(app_menu_close_tab_label("en-US"), "Close Tab");
}
#[test]
fn hides_window_on_close_for_windows_and_macos() {
assert!(should_hide_window_on_close("windows"));
assert!(should_hide_window_on_close("macos"));
}
#[test]
fn does_not_hide_window_on_close_for_other_platforms() {
assert!(!should_hide_window_on_close("linux"));
}
#[test]
fn hides_window_before_exit_only_on_macos() {
assert!(should_hide_window_before_exit("macos"));
assert!(!should_hide_window_before_exit("windows"));
assert!(!should_hide_window_before_exit("linux"));
}
#[test]
fn sets_up_desktop_tray_for_windows_macos_and_linux() {
assert!(should_setup_desktop_tray("windows", true, false));
assert!(should_setup_desktop_tray("macos", true, false));
assert!(should_setup_desktop_tray("linux", true, true));
assert!(!should_setup_desktop_tray("linux", true, false));
assert!(!should_setup_desktop_tray("windows", false, true));
assert!(!should_setup_desktop_tray("macos", false, true));
assert!(!should_setup_desktop_tray("linux", false, true));
}
#[test]
fn keeps_single_instance_for_release_builds_only() {
assert!(!should_enable_single_instance(true));
assert!(should_enable_single_instance(false));
}
#[test]
fn startup_data_dir_diagnostics_never_include_paths() {
let private_path = PathBuf::from(r"C:\Users\private-user\DBXData");
assert_eq!(startup_data_dir_mode(&DataDirMode::Default), "default");
assert_eq!(startup_data_dir_mode(&DataDirMode::EnvOverride), "env_override");
let label = startup_data_dir_mode(&DataDirMode::Portable { exe_dir: private_path });
assert_eq!(label, "portable");
assert!(!label.contains("private-user"));
}
#[cfg(target_os = "macos")]
#[test]
fn labels_debug_builds_in_the_macos_dock() {
assert_eq!(super::development_dock_badge_label(true), Some("DEV"));
assert_eq!(super::development_dock_badge_label(false), None);
}
#[cfg(target_os = "macos")]
#[test]
fn macos_tray_icon_remains_a_system_template() {
// Menu bar template images are intentionally independent from the app
// icon theme so macOS can recolor them for light and dark menu bars.
assert_eq!(super::MACOS_TRAY_ICON.width(), 36);
assert_eq!(super::MACOS_TRAY_ICON.height(), 36);
}
#[cfg(target_os = "macos")]
#[test]
fn macos_icon_themes_use_packaged_dock_assets() {
use objc2::AllocAnyThread;
use objc2_app_kit::NSImage;
use objc2_foundation::NSData;
assert!(super::MACOS_DEFAULT_APP_ICON.starts_with(b"icns"));
assert!(super::MACOS_DARK_APP_ICON.starts_with(b"icns"));
for bytes in [super::MACOS_DEFAULT_APP_ICON, super::MACOS_DARK_APP_ICON] {
let data = NSData::with_bytes(bytes);
assert!(NSImage::initWithData(NSImage::alloc(), &data).is_some());
}
}
#[test]
fn macos_icon_theme_targets_the_application_instead_of_a_window() {
assert!(uses_application_level_icon("macos"));
assert!(!uses_application_level_icon("windows"));
assert!(!uses_application_level_icon("linux"));
}
#[test]
fn shows_main_window_after_regular_startup_setup() {
assert!(should_show_main_window_after_setup());
}
#[test]
fn shows_main_window_while_startup_setup_continues() {
assert!(should_show_main_window_before_setup_tasks());
}
#[test]
fn only_user_requested_app_exit_needs_frontend_confirmation() {
assert!(should_confirm_app_exit_request("windows", None, false));
assert!(should_confirm_app_exit_request("macos", Some(0), false));
assert!(!should_confirm_app_exit_request("windows", Some(0), true));
assert!(!should_confirm_app_exit_request("windows", Some(tauri::RESTART_EXIT_CODE), false));
assert!(!should_confirm_app_exit_request("linux", Some(0), false));
}
#[test]
fn only_quit_uses_native_fallback_before_frontend_ready() {
assert!(should_fallback_to_native_quit("quit", false));
assert!(!should_fallback_to_native_quit("quit", true));
assert!(!should_fallback_to_native_quit("settings", false));
}
#[test]
fn overrides_native_window_decorations_for_desktop_platforms() {
assert_eq!(native_window_decorations_override("windows"), Some(false));
assert_eq!(native_window_decorations_override("linux"), Some(false));
assert_eq!(native_window_decorations_override("macos"), None);
}
#[test]
fn classifies_linux_nvidia_driver_from_selected_renderer() {
assert_eq!(linux_nvidia_driver_from_state(true, false, None), LinuxNvidiaDriver::Proprietary);
assert_eq!(linux_nvidia_driver_from_state(false, true, None), LinuxNvidiaDriver::Proprietary);
assert_eq!(linux_nvidia_driver_from_state(true, false, Some("nouveau")), LinuxNvidiaDriver::Proprietary);
assert_eq!(linux_nvidia_driver_from_state(false, false, Some("nouveau")), LinuxNvidiaDriver::Nouveau);
assert_eq!(linux_nvidia_driver_from_state(false, false, Some("i915")), LinuxNvidiaDriver::None);
assert_eq!(linux_nvidia_driver_from_state(false, false, Some("amdgpu")), LinuxNvidiaDriver::None);
assert_eq!(linux_nvidia_driver_from_state(false, false, None), LinuxNvidiaDriver::None);
}
fn drm_render_device(path: &str, driver: &str, boot_vga: bool) -> LinuxDrmRenderDevice {
LinuxDrmRenderDevice {
device_file: PathBuf::from(path),
driver: Some(driver.to_string()),
boot_vga,
pci_id: None,
}
}
fn drm_pci_render_device(
path: &str,
driver: &str,
boot_vga: bool,
vendor_id: u16,
device_id: u16,
) -> LinuxDrmRenderDevice {
LinuxDrmRenderDevice {
device_file: PathBuf::from(path),
driver: Some(driver.to_string()),
boot_vga,
pci_id: Some((vendor_id, device_id)),
}
}
#[test]
fn discovers_only_usable_linux_drm_render_device_files() {
let root = std::env::temp_dir().join(format!("dbx-drm-render-devices-{}", uuid::Uuid::new_v4()));
let sys_class_drm = root.join("sys/class/drm");
let dev_dri = root.join("dev/dri");
std::fs::create_dir_all(sys_class_drm.join("renderD128/device")).unwrap();
std::fs::create_dir_all(sys_class_drm.join("renderD129/device")).unwrap();
std::fs::create_dir_all(&dev_dri).unwrap();
assert!(linux_drm_render_devices_from_paths(&sys_class_drm, &dev_dri).is_empty());
std::fs::write(dev_dri.join("renderD129"), []).unwrap();
std::fs::write(sys_class_drm.join("renderD129/device/vendor"), "0x1002\n").unwrap();
std::fs::write(sys_class_drm.join("renderD129/device/device"), "0x1586\n").unwrap();
let devices = linux_drm_render_devices_from_paths(&sys_class_drm, &dev_dri);
assert_eq!(devices.len(), 1);
assert_eq!(devices[0].device_file, dev_dri.join("renderD129"));
assert_eq!(devices[0].pci_id, Some((0x1002, 0x1586)));
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn parses_linux_drm_pci_ids_without_accepting_malformed_values() {
assert_eq!(linux_pci_id_from_sysfs_value("0x1002\n"), Some(0x1002));
assert_eq!(linux_pci_id_from_sysfs_value("0X1586"), Some(0x1586));
assert_eq!(linux_pci_id_from_sysfs_value("8086"), Some(0x8086));
assert_eq!(linux_pci_id_from_sysfs_value(""), None);
assert_eq!(linux_pci_id_from_sysfs_value("0x"), None);
assert_eq!(linux_pci_id_from_sysfs_value("0x10000"), None);
assert_eq!(linux_pci_id_from_sysfs_value("not-a-device"), None);
let root = std::env::temp_dir().join(format!("dbx-drm-pci-ids-{}", uuid::Uuid::new_v4()));
let sys_class_drm = root.join("sys/class/drm");
let dev_dri = root.join("dev/dri");
for node in ["renderD128", "renderD129"] {
std::fs::create_dir_all(sys_class_drm.join(node).join("device")).unwrap();
std::fs::create_dir_all(&dev_dri).unwrap();
std::fs::write(dev_dri.join(node), []).unwrap();
}
std::fs::write(sys_class_drm.join("renderD128/device/vendor"), "malformed").unwrap();
std::fs::write(sys_class_drm.join("renderD128/device/device"), "0x1586").unwrap();
std::fs::write(sys_class_drm.join("renderD129/device/vendor"), "0x1002").unwrap();
let devices = linux_drm_render_devices_from_paths(&sys_class_drm, &dev_dri);
assert_eq!(devices.len(), 2);
assert_eq!(devices[0].pci_id, None);
assert_eq!(devices[1].pci_id, None);
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn keeps_linux_dmabuf_when_nouveau_is_loaded_but_not_the_default_renderer() {
let devices = [
drm_render_device("/dev/dri/renderD128", "i915", true),
drm_render_device("/dev/dri/renderD129", "nouveau", false),
];
let selected = linux_selected_drm_render_device(None, &devices).unwrap();
assert_eq!(selected.driver.as_deref(), Some("i915"));
assert_eq!(linux_nvidia_driver_from_state(false, false, selected.driver.as_deref()), LinuxNvidiaDriver::None);
}
#[test]
fn honors_explicit_webkit_linux_render_device_on_hybrid_gpus() {
let devices = [
drm_render_device("/dev/dri/renderD128", "i915", true),
drm_render_device("/dev/dri/renderD129", "nouveau", false),
];
let selected = linux_selected_drm_render_device(Some(Path::new("/dev/dri/renderD129")), &devices).unwrap();
assert_eq!(selected.driver.as_deref(), Some("nouveau"));
assert_eq!(
linux_nvidia_driver_from_state(false, false, selected.driver.as_deref()),
LinuxNvidiaDriver::Nouveau
);
let devices = [
drm_render_device("/dev/dri/renderD128", "i915", false),
drm_render_device("/dev/dri/renderD129", "nouveau", true),
];
let selected = linux_selected_drm_render_device(Some(Path::new("/dev/dri/renderD128")), &devices).unwrap();
assert_eq!(selected.driver.as_deref(), Some("i915"));
assert_eq!(linux_nvidia_driver_from_state(false, false, selected.driver.as_deref()), LinuxNvidiaDriver::None);
}
#[test]
fn uses_nouveau_workaround_for_the_default_linux_renderer() {
let devices = [
drm_render_device("/dev/dri/renderD128", "amdgpu", false),
drm_render_device("/dev/dri/renderD129", "nouveau", true),
];
let selected = linux_selected_drm_render_device(None, &devices).unwrap();
assert_eq!(selected.driver.as_deref(), Some("nouveau"));
assert_eq!(
linux_nvidia_driver_from_state(false, false, selected.driver.as_deref()),
LinuxNvidiaDriver::Nouveau
);
}
#[test]
fn applies_driver_specific_linux_webkit_rendering_workarounds() {
assert_eq!(
linux_webkit_rendering_workarounds(LinuxNvidiaDriver::Proprietary, true, None, false),
&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1"), ("__NV_DISABLE_EXPLICIT_SYNC", "1")]
);
assert_eq!(
linux_webkit_rendering_workarounds(LinuxNvidiaDriver::Nouveau, true, None, false),
&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1")]
);
assert_eq!(linux_webkit_rendering_workarounds(LinuxNvidiaDriver::None, true, None, false), &[]);
// Without any hardware render device (GPU-less VM / server) Mesa falls
// back to llvmpipe, whose DMABuf compositing path can crash the WebKit
// process.
assert_eq!(
linux_webkit_rendering_workarounds(LinuxNvidiaDriver::None, false, None, false),
&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1")]
);
}
#[test]
fn enables_appimage_dmabuf_workaround_only_for_real_appimage_values() {
assert!(linux_appimage_requires_dmabuf_workaround(Some(OsStr::new("/opt/DBX.AppImage"))));
assert!(!linux_appimage_requires_dmabuf_workaround(Some(OsStr::new(""))));
assert!(!linux_appimage_requires_dmabuf_workaround(None));
}
#[test]
fn disables_linux_webkit_dmabuf_only_for_strix_halo_on_native_wayland() {
let strix_halo = drm_pci_render_device("/dev/dri/renderD128", "amdgpu", true, 0x1002, 0x1586);
let mut strix_halo_without_driver = strix_halo.clone();
strix_halo_without_driver.driver = None;
let adjacent_amd = drm_pci_render_device("/dev/dri/renderD128", "amdgpu", true, 0x1002, 0x1587);
let native_wayland =
linux_uses_native_wayland(Some(OsStr::new("wayland-0")), Some(OsStr::new("wayland")), None);
assert!(native_wayland);
assert_eq!(
linux_webkit_rendering_workarounds(LinuxNvidiaDriver::None, true, Some(&strix_halo), native_wayland),
&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1")]
);
assert_eq!(
linux_webkit_rendering_workarounds(
LinuxNvidiaDriver::None,
true,
Some(&strix_halo_without_driver),
native_wayland,
),
&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1")]
);
assert_eq!(
linux_webkit_rendering_workarounds(LinuxNvidiaDriver::None, true, Some(&adjacent_amd), native_wayland),
&[]
);
for native_wayland in [
linux_uses_native_wayland(
Some(OsStr::new("wayland-0")),
Some(OsStr::new("wayland")),
Some(OsStr::new("x11")),
),
linux_uses_native_wayland(None, Some(OsStr::new("wayland")), None),
linux_uses_native_wayland(Some(OsStr::new("wayland-0")), None, None),
linux_uses_native_wayland(Some(OsStr::new("wayland-0")), Some(OsStr::new("x11")), None),
] {
assert!(!native_wayland);
assert_eq!(
linux_webkit_rendering_workarounds(LinuxNvidiaDriver::None, true, Some(&strix_halo), native_wayland),
&[]
);
}
}
#[test]
fn linux_webkit_strix_quirk_follows_the_selected_hybrid_render_node() {
let devices = [
drm_pci_render_device("/dev/dri/renderD128", "i915", true, 0x8086, 0x46a6),
drm_pci_render_device("/dev/dri/renderD129", "amdgpu", false, 0x1002, 0x1586),
];
let default_device = linux_selected_drm_render_device(None, &devices).unwrap();
assert_eq!(linux_webkit_rendering_workarounds(LinuxNvidiaDriver::None, true, Some(default_device), true), &[]);
let explicit_device = linux_selected_drm_render_device(Some(Path::new("/dev/dri/renderD129")), &devices);
assert_eq!(
linux_webkit_rendering_workarounds(LinuxNvidiaDriver::None, true, explicit_device, true),
&[("WEBKIT_DISABLE_DMABUF_RENDERER", "1")]
);
let unmatched_device = linux_selected_drm_render_device(Some(Path::new("/dev/dri/renderD130")), &devices);
assert!(unmatched_device.is_none());
assert_eq!(linux_webkit_rendering_workarounds(LinuxNvidiaDriver::None, true, unmatched_device, true), &[]);
}
#[test]
fn linux_webkit_quirk_respects_explicit_gdk_backend() {
let display = Some(OsStr::new("wayland-0"));
let session = Some(OsStr::new("wayland"));
assert!(linux_uses_native_wayland(display, session, None));
assert!(linux_uses_native_wayland(display, session, Some(OsStr::new("wayland"))));
assert!(!linux_uses_native_wayland(display, session, Some(OsStr::new("x11,wayland,*"))));
}
#[test]
fn linux_webkit_workarounds_preserve_user_environment_values() {
assert_eq!(linux_webkit_environment_override(None, "1"), Some("1"));
for value in [OsStr::new(""), OsStr::new("0"), OsStr::new("1")] {
assert_eq!(linux_webkit_environment_override(Some(value), "1"), None);
}
}
#[test]
fn treats_virtual_and_2d_drm_drivers_as_software_rendering() {
assert!(linux_drm_driver_is_software_only(Some("virtio-pci")));
assert!(linux_drm_driver_is_software_only(Some("virtio_gpu")));
assert!(linux_drm_driver_is_software_only(Some("qxl")));
assert!(linux_drm_driver_is_software_only(Some("bochs")));
assert!(linux_drm_driver_is_software_only(None));
assert!(!linux_drm_driver_is_software_only(Some("amdgpu")));
assert!(!linux_drm_driver_is_software_only(Some("i915")));
assert!(!linux_drm_driver_is_software_only(Some("nouveau")));
}
}
fn route_external_commands(
main_handler: impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static,
) -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
dbx_tauri_consul::route(dbx_tauri_schema::route(main_handler))
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// Metadata/completion command chains nest very large async futures and can
// exhaust tokio's default 2 MiB worker stack, which aborts the process with
// STATUS_STACK_OVERFLOW. Share the roomier stack the backup worker and Web
// server runtimes use as well.
let runtime = dbx_core::scheduled_backup::worker_runtime().expect("Failed to build tokio runtime");
let runtime_handle = runtime.handle().clone();
let _runtime = Box::leak(Box::new(runtime));
tauri::async_runtime::set(runtime_handle);
startup_recovery::initialize();
rustls::crypto::aws_lc_rs::default_provider().install_default().expect("Failed to install rustls crypto provider");
append_startup_probe("runtime prerequisites configured");
#[cfg(target_os = "linux")]
apply_linux_webkit_rendering_workarounds();
let startup_begin = Instant::now();
let builder = tauri::Builder::default()
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
// Plugin workbench sandbox documents lazy-load their code-split chunks
// through this scheme; see plugin_ui_protocol.rs.
.register_asynchronous_uri_scheme_protocol(plugin_ui_protocol::PLUGIN_UI_SCHEME, plugin_ui_protocol::handle);
let builder = if should_enable_single_instance(cfg!(debug_assertions)) {
builder.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
let app_open_requested = args.iter().any(|arg| commands::deep_link::is_app_open_deep_link(arg));
let links = commands::deep_link::connection_deep_links_from_args(args.clone());
open_connection_deep_links(app, links);
let ai_config_links = commands::deep_link::ai_config_deep_links_from_args(args.clone());
open_ai_config_deep_links(app, ai_config_links);
let plugin_install_links = commands::deep_link::plugin_install_deep_links_from_args(args.clone());
open_plugin_install_deep_links(app, plugin_install_links);
let paths = commands::external_sql::sql_file_paths_from_args(args.clone(), std::path::Path::new(&cwd));
if !paths.is_empty() {
if let Some(state) = app.try_state::<commands::external_sql::ExternalSqlOpenState>() {
state.push(paths.clone());
}
let _ = app.emit("dbx-open-sql-files", paths);
}
let db_paths = commands::external_db::db_file_paths_from_args(args, std::path::Path::new(&cwd));
if !db_paths.is_empty() {
if let Some(state) = app.try_state::<commands::external_db::ExternalDbOpenState>() {
state.push(db_paths.clone());
}
let _ = app.emit("dbx-open-db-files", db_paths);
}
// This runs inside the *existing* instance: a second launch has already
// handed over its arguments and exited. If we cannot reveal a window here
// the user is left with no feedback whatsoever - the app they clicked
// simply vanished - so make the reason recoverable from the logs.
if !show_main_window(app) {
eprintln!(
"[WINDOW] single-instance handoff could not reveal the main window; app_open_requested={app_open_requested}; {}",
main_window_probe_state(app)
);
}
}))
} else {
builder
};
let builder = builder
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
.plugin(
tauri_plugin_window_state::Builder::default()
.with_state_flags(window_state_guard::persisted_main_window_state_flags())
.skip_initial_state("main")
.build(),
);
// macOS app menu (Cmd+Q / Dock Quit). Skip on Linux/Windows so an empty menu bar
// is not installed where there was none before.
#[cfg(target_os = "macos")]
let builder = builder.menu(build_app_menu).on_menu_event(|app, event| {
if event.id() == APP_MENU_QUIT_ID {
request_app_close(app, "quit");
} else if event.id() == APP_MENU_COPY_SUPPORT_INFO_ID {
if let Err(err) = app.clipboard().write_text(commands::support_info::format_support_info_for_clipboard()) {
log::warn!("Failed to copy support info from app menu: {err}");
}
} else if event.id() == APP_MENU_CLOSE_TAB_ID {
let _ = app.emit(APP_CLOSE_ACTIVE_TAB_EVENT, ());
}
});
builder
.manage(CloseBehaviorState::new())
.manage(commands::plugin_file::PluginFileState::new())
.manage(commands::plugin_media::PluginMediaState::new())
.manage(commands::plugin_storage::PluginUiStorageState::new())
.manage(AppLocaleState::new())
.on_page_load(|webview, payload| {
if payload.event() == PageLoadEvent::Started {
if let Some(state) = webview.app_handle().try_state::<CloseBehaviorState>() {
state.set_frontend_ready(false);
}
}
})
.setup(move |app| {
let setup_start = Instant::now();
#[cfg(target_os = "linux")]
create_linux_main_window(app)?;
eprintln!("[STARTUP] plugins registered in {:?}", startup_begin.elapsed());
append_startup_probe(format!("setup entered after {:?}", startup_begin.elapsed()));
window_state_guard::restore_main_window_state(app.handle());
if should_show_main_window_before_setup_tasks() {
prepare_main_window_for_display(app.handle());
show_main_window(app.handle());
append_startup_probe(format!(
"early main window show requested; {}",
main_window_probe_state(app.handle())
));
}
append_startup_probe("resolving app data dir");
let default_data_dir =
app.path().app_data_dir().map_err(|e| e.to_string()).expect("Failed to resolve app data dir");
let data_dir_resolution = data_dir::resolve_data_dir_with_mode(default_data_dir);
let data_dir = data_dir_resolution.data_dir.clone();
let portable_mode = data_dir_resolution.is_portable_mode();
std::fs::create_dir_all(&data_dir).expect("Failed to create data dir");
let data_dir_mode = startup_data_dir_mode(&data_dir_resolution.mode);
append_startup_probe(format!("data dir ready mode={data_dir_mode}"));
let alternative_data_dir = data_dir::alternative_data_dir(&data_dir_resolution);
match maybe_import_user_data_db(&data_dir, alternative_data_dir.as_deref()) {
Ok(result) => eprintln!("[STARTUP] data db fallback import: {result:?}"),
Err(err) => eprintln!("[STARTUP] data db fallback import failed: {err}"),
}
let db_path = data_dir.join("dbx.db");
// Failure logs from previous runs are kept only within their retention
// limits; prune once at startup so a killed process cannot leave the
// directory growing forever.
commands::sql_file::prune_desktop_sql_file_failure_logs(&data_dir);
let t = Instant::now();
append_startup_probe(format!("opening storage file=dbx.db data_dir_mode={data_dir_mode}"));
let storage = tauri::async_runtime::block_on(async {
let mut s = Storage::open_unmigrated(&db_path).await.expect("Failed to open storage");
if portable_mode {
s = s.with_secret_key_policy(SecretKeyPolicy::ManagedDataDir);
match s.prepare_portable_secret_key().await {
Ok(true) => append_startup_probe("portable key copied from the platform credential store"),
Ok(false) => {}
Err(error) => {
log::warn!("[STARTUP] portable key preparation failed: {error}");
append_startup_probe(format!("portable key preparation failed: {error}"));
}
}
}
eprintln!("[STARTUP] Storage::open in {:?}", t.elapsed());
append_startup_probe(format!("storage opened in {:?}", t.elapsed()));
let t2 = Instant::now();
eprintln!("[STARTUP] migration wizard deferred in {:?}", t2.elapsed());
append_startup_probe(format!("migration deferred to security wizard in {:?}", t2.elapsed()));
s
});
let migration_ready = tauri::async_runtime::block_on(storage.inspect_data_migration())
.map(|status| status.is_ready())
.unwrap_or(false);
let migration_gate = Arc::new(migration_gate::MigrationGate::new(migration_ready));
app.manage(migration_gate.clone());
let desktop_settings = tauri::async_runtime::block_on(storage.load_desktop_settings()).unwrap_or_default();
app.handle().plugin(
tauri_plugin_log::Builder::default()
.timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)
.format(|out, message, record| {
out.finish(format_args!(
"[{}][{}][{}] {}",
chrono::Local::now().format("%Y-%m-%d][%H:%M:%S%.3f"),
record.level(),
record.target(),
message
));
})
.level(log::LevelFilter::Debug)
.build(),
)?;
apply_debug_log_level(desktop_settings.debug_logging_enabled);
eprintln!("[STARTUP] storage ready in {:?}", t.elapsed());
append_startup_probe(format!("storage ready in {:?}", t.elapsed()));
// Initialize core dialect registry and load external plugin dialects
let dialect_init_start = Instant::now();
register_core_dialects();
let registry = DialectRegistry::global();
let plugin_dirs = vec![data_dir.join("plugins").join("dialects")];
let load_result = DialectPluginLoader::scan_and_load(registry, &plugin_dirs);
eprintln!(
"[STARTUP] dialect plugins loaded: {} success, {} errors, {} skipped in {:?}",
load_result.loaded.len(),
load_result.errors.len(),
load_result.skipped.len(),
dialect_init_start.elapsed()
);
append_startup_probe(format!(
"dialect plugins loaded: {} success, {} errors, {} skipped in {:?}",
load_result.loaded.len(),
load_result.errors.len(),
load_result.skipped.len(),
dialect_init_start.elapsed()
));
// Start dialect YAML hot-reload watcher
let watch_dirs = plugin_dirs.clone();
tauri::async_runtime::spawn(async move {
if let Err(e) = DialectHotReload::run_forever(watch_dirs, DialectRegistry::global()).await {
log::error!("[STARTUP] dialect hot-reload watcher exited: {e}");
}
});
eprintln!("[STARTUP] dialect hot-reload watcher started");
let default_agent_dir = data_dir_resolution.uses_custom_data_dir().then(|| data_dir.join("agents"));
let (plugin_dir, agent_dir) = commands::app_settings::resolve_driver_store_dirs_from_settings(
&desktop_settings,
&data_dir,
default_agent_dir,
);
let state = if let Some(agent_dir) = agent_dir {
AppState::new_with_plugin_and_agent_dir_and_app_version(
storage,
plugin_dir,
agent_dir,
env!("CARGO_PKG_VERSION"),
)
} else {
AppState::new_with_plugin_dir_and_app_version(storage, plugin_dir, env!("CARGO_PKG_VERSION"))
};
dbx_core::db::sqlite_worker::enable_sqlite_ssh_runtime(env!("CARGO_PKG_VERSION"));
state.set_duckdb_worker_process_isolation_enabled(desktop_settings.duckdb_worker_process_isolation);
state.set_duckdb_worker_max_processes(desktop_settings.duckdb_worker_max_processes);
let oidc_app_handle = app.handle().clone();
state.set_mongo_oidc_browser_opener(Arc::new(move |url| {
oidc_app_handle
.opener()
.open_url(url, None::<&str>)
.map_err(|err| format!("Failed to open the system browser: {err}"))
}));
let sf_app_handle = app.handle().clone();
state.set_salesforce_browser_opener(Arc::new(move |url| {
sf_app_handle
.opener()
.open_url(url, None::<&str>)
.map_err(|err| format!("Failed to open the system browser: {err}"))
}));
let state = Arc::new(state);
app.manage(state.clone());
commands::plugins::install_plugin_event_bridge(app.handle(), state.clone());
commands::connection::install_connection_liveness_bridge(app.handle(), state.clone());
let app_identifier = app.config().identifier.clone();
let backups = tauri::async_runtime::block_on(async {
background_backup::BackgroundBackup::new(state.clone(), data_dir.clone(), app_identifier)
});
match backups {
Ok(backups) => {
if let Err(error) = backups.resume() {
log::error!("[database-backup] background registration failed: {error}");
}
app.manage(backups);
}
Err(error) => log::error!("[database-backup] worker startup failed: {error}"),
}
let mcp_http_server = Arc::new(commands::mcp_http_server::McpHttpServerState::new(data_dir.clone()));
app.manage(mcp_http_server.clone());
let mcp_http_state = state.clone();
let mcp_gate = migration_gate.clone();
tauri::async_runtime::spawn(async move {
mcp_gate.wait().await;
commands::mcp_http_server::start_if_enabled(mcp_http_state, mcp_http_server).await;
});
app.manage(commands::redis_pubsub_server::start_pubsub_server(state.clone()));
app.manage(commands::saved_sql::SavedSqlStorageState { data_dir: data_dir.clone() });
app.manage(commands::external_sql::ExternalSqlOpenState::default());
app.manage(commands::external_db::ExternalDbOpenState::default());
app.manage(commands::deep_link::DeepLinkOpenState::default());
app.manage(commands::update::PendingUpdateState::default());
app.manage(commands::ssh_prompt::SshPromptState::new());
commands::ssh_prompt::install_ssh_prompt_bridge(app.handle());
commands::ssh_prompt::install_ssh_notice_bridge(app.handle());
#[cfg(target_os = "macos")]
macos_app_delegate::install_dock_quit_handler(app.handle());
#[cfg(target_os = "macos")]
macos_escape_guard::install_escape_fullscreen_guard();
#[cfg(target_os = "windows")]
webview2_recovery::install(app.handle());
let startup_args: Vec<String> = std::env::args().skip(1).collect();
let startup_links = commands::deep_link::connection_deep_links_from_args(&startup_args);
open_connection_deep_links(app.handle(), startup_links);
let startup_ai_config_links = commands::deep_link::ai_config_deep_links_from_args(&startup_args);
open_ai_config_deep_links(app.handle(), startup_ai_config_links);
let startup_plugin_install_links = commands::deep_link::plugin_install_deep_links_from_args(&startup_args);
open_plugin_install_deep_links(app.handle(), startup_plugin_install_links);
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
migration_gate.wait().await;
commands::mcp_bridge::start(app_handle, state, data_dir);
});
eprintln!("[STARTUP] setup complete in {:?} (total {:?})", setup_start.elapsed(), startup_begin.elapsed());
append_startup_probe(format!(
"setup tasks complete in {:?} total {:?}",
setup_start.elapsed(),
startup_begin.elapsed()
));
prepare_main_window_for_display(app.handle());
if should_setup_desktop_tray(
std::env::consts::OS,
desktop_settings.show_tray_icon,
linux_appindicator_available(),
) {
setup_desktop_tray(app, desktop_settings.icon_theme)?;
}
apply_desktop_icon_theme(app.handle(), desktop_settings.icon_theme)?;
#[cfg(target_os = "macos")]
apply_macos_development_dock_badge(app.handle())?;
if should_show_main_window_after_setup() {
show_main_window(app.handle());
append_startup_probe(format!(
"final main window show requested; {}",
main_window_probe_state(app.handle())
));
}
#[cfg(any(windows, target_os = "linux"))]
let _ = app.deep_link().register_all();
append_startup_probe("setup finished");
Ok(())
})
.on_window_event(|window, event| {
// Anchor the plugin-file drop consent on the Rust side: paths are
// registered for the webview that physically received the drop,
// and plugin_file_open_dropped consumes them there. Folders are
// granted as themselves and expand to their contained files at
// open time. Renderer-side drop events stay display-only; they
// cannot mint file access.
if let tauri::WindowEvent::DragDrop(tauri::DragDropEvent::Drop { paths, .. }) = event {
let dropped: Vec<String> = paths
.iter()
.filter(|path| path.is_file() || path.is_dir())
.map(|path| path.to_string_lossy().into_owned())
.collect();
if !dropped.is_empty() {
if let Some(state) = window.try_state::<commands::plugin_file::PluginFileState>() {
state.register_dropped_paths(window.label(), dropped);
}
}
return;
}
if let tauri::WindowEvent::Destroyed = event {
// A webview that no longer exists must not leave drop grants
// behind for a future renderer to claim.
if let Some(state) = window.try_state::<commands::plugin_file::PluginFileState>() {
state.clear_dropped_paths(window.label());
}
if let Some(tab_id) = window.label().strip_prefix("detached-tab-") {
let _ = window.emit("dbx:detached-tab-lost", serde_json::json!({ "tabId": tab_id }));
}
return;
}
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
// Floating plugin widgets (label prefix shared with the frontend's
// floatingWindowLabel) close like ordinary windows. Hiding them to
// the tray would keep their plugin instance — audio included —
// running invisibly, and the app-wide close prompt belongs to the
// main shell, not to a widget the user dismissed.
if window.label().starts_with("plugin-floating-") {
return;
}
if let Some(tab_id) = window.label().strip_prefix("detached-tab-") {
if commands::app_settings::take_approved_detached_window_close(window.label()) {
return;
}
api.prevent_close();
// Broadcast with the tabId payload: JS listeners registered
// with the default `listen()` target receive events emitted
// to any window, so the frontend must filter by tabId.
let _ = window.emit("dbx:detached-tab-close-requested", serde_json::json!({ "tabId": tab_id }));
return;
}
if !should_hide_window_on_close(std::env::consts::OS) {
return;
}
let app = window.app_handle();
if app.try_state::<CloseBehaviorState>().is_none() {
api.prevent_close();
hide_main_window_for_close(app, window);
return;
}
api.prevent_close();
request_app_close(app, "settings");
}
})
.invoke_handler(migration_gate::guard_handler(route_external_commands(tauri::generate_handler![
commands::ai::ai_complete,
commands::ai::ai_stream,
commands::ai::ai_agent_stream,
commands::ai::ai_cancel_stream,
commands::ai::ai_resolve_tool_approval,
commands::ai::get_ai_plugin_tool_plugins,
commands::ai::set_ai_plugin_tool_plugin_enabled,
commands::ai::preview_plugin_ai_tools,
commands::ai::ai_test_connection,
commands::ai::ai_list_models,
commands::ai::ai_resolve_model_effort,
commands::ai::save_ai_config,
commands::ai::load_ai_config,
commands::ai::save_ai_provider_config,
commands::ai::load_ai_provider_configs,
commands::ai::save_ai_chat_selection,
commands::ai::load_ai_chat_selection,
commands::ai::save_ai_conversation,
commands::ai::load_ai_conversations,
commands::ai::delete_ai_conversation,
commands::ai::save_ai_run,
commands::ai::save_ai_run_state,
commands::ai::load_ai_runs,
commands::ai_multi_config::save_ai_configs,
commands::ai_multi_config::load_ai_configs,
commands::ai_multi_config::set_default_ai_config,
commands::ai_multi_config::save_ai_config_item,
commands::ai_multi_config::delete_ai_config,
commands::cc_switch::load_cc_switch_ai_configs,
commands::prompt_template::load_prompt_templates,
commands::prompt_template::save_prompt_template,
commands::prompt_template::delete_prompt_template,
commands::prompt_template::get_ai_global_custom_instructions,
commands::prompt_template::set_ai_global_custom_instructions,
commands::user_skills::list_user_skills,
commands::user_skills::read_user_skills,
commands::app_settings::load_desktop_settings,
commands::app_settings::save_desktop_settings,
commands::app_settings::load_max_agent_turns,
commands::app_settings::save_max_agent_turns,
commands::app_settings::load_history_retention_limit,
commands::app_settings::save_history_retention_limit,
commands::app_settings::load_mcp_history_retention_limit,
commands::app_settings::save_mcp_history_retention_limit,
commands::app_settings::load_max_retries,
commands::app_settings::save_max_retries,
commands::app_settings::load_app_appearance_settings,
commands::app_settings::update_app_appearance_settings,
commands::app_settings::set_app_locale,
commands::app_settings::complete_app_close,
commands::app_settings::mark_frontend_ready,
commands::app_settings::request_app_close_from_window_controls,
commands::window_controls::set_macos_traffic_light_position,
commands::window_controls::get_linux_window_control_icons,
commands::app_settings::set_driver_store_dir,
commands::app_settings::set_plugin_store_dir,
commands::app_settings::set_agent_store_dir,
commands::app_settings::get_driver_store_path,
commands::app_settings::load_pinned_tree_node_ids,
commands::app_settings::save_pinned_tree_node_ids,
commands::app_settings::load_mcp_global_policy,
commands::app_settings::save_mcp_global_policy,
commands::mcp_bridge::respond_mcp_sql_approval,
commands::background_image::save_background_image,
commands::background_image::clear_background_image,
commands::background_image::read_background_image,
commands::background_image::check_background_image,
commands::mcp_http_server::load_mcp_http_server_settings,
commands::mcp_http_server::save_mcp_http_server_settings,
commands::mcp_http_server::mcp_http_server_status,
commands::mcp_http_server::rotate_mcp_http_server_token,
commands::app_settings::load_editor_settings,
commands::app_settings::save_editor_settings,
commands::app_settings::load_global_search_settings,
commands::app_settings::save_global_search_settings,
commands::app_settings::load_open_tabs_state,
commands::app_settings::save_open_tabs_state,
commands::app_settings::save_detached_tab_handoff,
commands::app_settings::load_detached_tab_handoff,
commands::app_settings::list_detached_tab_handoffs,
commands::app_settings::delete_detached_tab_handoff,
commands::app_settings::approve_detached_window_close,
commands::app_settings::load_saved_sql_editor_positions,
commands::app_settings::save_saved_sql_editor_positions,
commands::app_settings::load_transfer_task_library,
commands::app_settings::save_transfer_task_library,
commands::app_settings::load_native_debug_logs,
commands::diagnostics::get_process_memory_info,
commands::support_info::get_app_support_info,
commands::cloud_sync::webdav_sync_test,
commands::cloud_sync::migration_status,
commands::cloud_sync::migration_start,
commands::cloud_sync::migration_retry,
commands::cloud_sync::migration_cleanup_backups,
commands::cloud_sync::webdav_password_status,
commands::cloud_sync::save_webdav_saved_password,
commands::cloud_sync::forget_webdav_saved_password,
commands::cloud_sync::webdav_sync_secrets_status,
commands::cloud_sync::save_webdav_sync_secrets_preference,
commands::cloud_sync::forget_webdav_sync_secrets_passphrase,
commands::cloud_sync::webdav_sync_upload,
commands::cloud_sync::webdav_sync_download,
commands::cloud_sync::webdav_sync_inspect,
commands::cloud_sync::cloud_sync_local_catalog,
commands::cloud_sync::snippet_sync_test,
commands::cloud_sync::snippet_token_status,
commands::cloud_sync::save_snippet_saved_token,
commands::cloud_sync::forget_snippet_saved_token,
commands::cloud_sync::snippet_sync_settings,
commands::cloud_sync::save_snippet_sync_id,
commands::cloud_sync::retry_snippet_legacy_cleanup,
commands::cloud_sync::snippet_sync_upload,
commands::cloud_sync::snippet_sync_download,
commands::cloud_sync::snippet_sync_inspect,
commands::local_backup::local_backup_export,
commands::local_backup::local_backup_inspect,
commands::local_backup::local_backup_import,
commands::connection::test_connection,
commands::connection::test_connection_with_info,
commands::connection::test_ssh_tunnel,
commands::salesforce_oauth::salesforce_oauth_browser_authorize,
commands::salesforce_oauth::salesforce_oauth_device_start,
commands::salesforce_oauth::salesforce_oauth_device_poll,
commands::salesforce_oauth::salesforce_oauth_refresh,
commands::salesforce_oauth::salesforce_oauth_password_login,
commands::salesforce_oauth::salesforce_current_user,
commands::connection::connect_db,
commands::connection::connection_final_proxy_port,
commands::connection::disconnect_db,
commands::connection::close_database_connection,
commands::connection::session_credential_status,
commands::connection::forget_session_credential,
commands::connection::replace_nacos_session_credential,
commands::connection::clear_all_session_credentials,
commands::connection::refresh_connections,
commands::connection::check_connection_health,
commands::connection::connection_is_open,
commands::connection::prewarm_connection,
commands::connection::connection_identifier_quote,
commands::connection::connection_database_info,
commands::connection::save_connection_database_info,
commands::connection::unlock_connection_writes,
commands::connection::lock_connection_writes,
commands::connection::connection_write_unlock_state,
commands::connection::save_connections,
commands::connection::load_connections,
commands::connection::save_sidebar_layout,
commands::connection::load_sidebar_layout,
commands::connection::save_table_vgroups,
commands::connection::load_table_vgroups,
commands::connection::delete_table_vgroups_for_connection,
commands::plugin_file::plugin_file_open_dropped,
commands::plugin_file::plugin_file_pick_files,
commands::plugin_file::plugin_file_save_as,
commands::plugin_file::plugin_file_read,
commands::plugin_file::plugin_file_write,
commands::plugin_file::plugin_file_close,
commands::plugin_media::plugin_media_open,
commands::plugin_media::plugin_media_close,
commands::plugin_storage::plugin_ui_storage_get,
commands::plugin_storage::plugin_ui_storage_set,
commands::plugin_storage::plugin_ui_storage_delete,
commands::plugins::list_plugins,
commands::plugins::list_plugin_trusted_keys,
commands::plugins::save_plugin_trusted_key,
commands::plugins::remove_plugin_trusted_key,
commands::plugins::list_plugin_repositories,
commands::plugins::save_plugin_repository,
commands::plugins::remove_plugin_repository,
commands::plugins::fetch_plugin_marketplace_catalogs,
commands::plugins::install_marketplace_plugin,
commands::plugins::install_plugin_package,
commands::plugins::install_plugin_package_from_url,
commands::plugins::rollback_plugin,
commands::plugins::uninstall_plugin,
commands::plugins::activate_plugin,
commands::plugins::list_active_plugins,
commands::plugins::stop_plugin,
commands::plugins::invoke_plugin,
commands::plugin_download::download_plugin_file,
commands::plugin_download::cancel_plugin_download,
commands::plugins::invoke_plugin_connection_action,
commands::plugins::notify_plugin,
commands::plugins::send_plugin_binary,
commands::plugins::list_plugin_filesystem_entries,
commands::plugins::read_plugin_filesystem_file,
commands::plugins::write_plugin_filesystem_file,
commands::plugins::create_plugin_filesystem_directory,
commands::plugins::delete_plugin_filesystem_entry,
commands::plugins::rename_plugin_filesystem_entry,
commands::plugins::read_plugin_ui_entry,
commands::plugins::read_plugin_asset,
commands::plugins::read_plugin_ui_asset,
commands::plugins::list_jdbc_drivers,
commands::plugins::list_jdbc_maven_bundles,
commands::plugins::list_jdbc_local_bundles,
commands::plugins::import_jdbc_drivers,
commands::plugins::install_jdbc_driver_from_maven,
commands::plugins::install_prestosql_jdbc_driver,
commands::plugins::delete_jdbc_driver,
commands::plugins::delete_jdbc_maven_bundle,
commands::plugins::delete_jdbc_local_bundle,
commands::plugins::jdbc_plugin_status,
commands::plugins::install_jdbc_plugin,
commands::plugins::install_jdbc_plugin_local,
commands::plugins::uninstall_jdbc_plugin,
commands::schema_diff::prepare_schema_diff,
commands::schema_diff::generate_schema_sync_sql,
commands::schema_diff::generate_schema_sync_plan,
commands::dialect_cmd::list_dialect_data_types,
commands::schema_cache::save_schema_cache,
commands::schema_cache::load_schema_cache,
commands::schema_cache::delete_schema_cache_prefix,
commands::tab_runtime_cache::save_tab_runtime_cache,
commands::tab_runtime_cache::load_tab_runtime_cache,
commands::tab_runtime_cache::list_tab_runtime_cache_metadata,
commands::tab_runtime_cache::prune_tab_runtime_cache,
commands::tab_runtime_cache::delete_tab_runtime_cache_owner,
commands::tab_runtime_cache::delete_tab_runtime_cache,
commands::query::execute_query,
commands::query::execute_conditional_update,
commands::query::execute_multi,
commands::query::cancel_query,
commands::query::cancel_conditional_update,
commands::query::close_query_session,
commands::query::read_large_value_chunk,
commands::query::release_large_value,
commands::query::download_large_value,
commands::query::export_snapshot_result,
commands::query::close_client_connection_session,
commands::query::execute_batch,
commands::query::execute_script,
commands::query::execute_in_transaction,
commands::query::execute_script_with_2pc,
commands::query::begin_manual_transaction,
commands::query::execute_in_manual_transaction,
commands::query::commit_manual_transaction,
commands::query::rollback_manual_transaction,
commands::query::analyze_sql_references,
commands::query::find_statement_at_cursor,
commands::query::prepare_query_pagination_execution_plan,
commands::query::build_sorted_query_sql,
commands::query::build_explain_sql,
commands::query::get_explain_info,
commands::query::get_plugin_plan_capabilities,
commands::query::get_plugin_estimated_plan,
commands::query::query_plugin_data,
commands::query::get_plugin_data_grants,
commands::query::set_plugin_data_grant,
commands::query::build_create_user_sql,
commands::query::build_dropped_file_preview_sql,
commands::query::build_table_select_sql,
commands::query::build_database_search_sql,
commands::query::build_database_structure_search_sql,
commands::query::supports_database_structure_search,
commands::query::build_search_result_where,
commands::query::build_rename_object_sql,
commands::query::build_rename_database_sql,
commands::query::build_rename_database_preflight_sql,
commands::query::build_create_database_sql,
#[cfg(feature = "duckdb-sidecar")]
commands::query::build_duckdb_attach_database_sql,
commands::query::build_sqlite_attach_database_sql,
commands::query::build_drop_object_sql,
commands::query::build_drop_table_sql,
commands::query::build_drop_table_child_object_sql,
commands::query::build_empty_table_sql,
commands::query::build_truncate_table_sql,
commands::query::build_vacuum_table_sql,
commands::query::build_mysql_auto_increment_sql,
commands::query::build_drop_database_sql,
commands::query::build_create_schema_sql,
commands::query::build_update_database_properties_sql,
commands::query::build_drop_schema_sql,
commands::query::build_duplicate_table_structure_sql,
commands::query::build_copy_table_data_sql,
commands::query::build_executable_object_source_statements,
commands::query::validate_oracle_executable_object_source,
commands::query::fingerprint_oracle_routine_object_source,
commands::query::build_executable_object_source_sql,
commands::query::build_editable_object_source,
commands::query::build_routine_rename_object_source_statements,
commands::query::rewrite_oracle_routine_references,
commands::query::rewrite_oracle_table_references,
commands::query::parse_oracle_object_name,
commands::query::build_oracle_view_rename_source,
commands::query::review_oracle_view_rename_source,
commands::query::build_view_ddl_sql,
commands::query::build_table_structure_change_sql,
commands::query::build_table_owner_change_sql,
commands::query::preview_sqlite_table_structure_change,
commands::query::apply_sqlite_table_structure_change,
commands::query::build_create_table_sql,
commands::query::build_create_partitioned_table_sql,
commands::query::build_table_partition_operation_sql,
commands::query::build_single_column_alter_sql,
commands::query::analyze_editable_query_editability,
commands::query::prepare_data_grid_save,
commands::query::extract_data_grid_selection,
commands::query::build_data_grid_copy_update_statements,
commands::query::build_data_grid_copy_insert_statement,
commands::query::build_dml_change_preview_sql,
commands::query::build_data_grid_context_filter_condition,
commands::query::build_data_grid_column_value_filter_condition,
commands::query::build_data_grid_column_values_filter_condition,
commands::query::build_data_grid_column_distinct_values_sql,
commands::query::build_data_grid_count_sql,
commands::query::build_data_grid_conditional_update_sql,
commands::query::build_hive_table_properties_sql,
commands::query::build_export_insert_statements,
commands::query::build_export_sql_insert,
commands::query::build_database_sql_export,
commands::data_compare::prepare_data_compare,
commands::data_compare::prepare_data_compare_from_tables,
commands::data_compare::prepare_data_compare_missing_target,
commands::data_compare::build_data_compare_sync_plan,
commands::sql_file::preview_sql_file,
commands::sql_file::inspect_sql_file_tables,
commands::sql_file::execute_sql_file,
commands::sql_file::execute_sql_files,
commands::sql_file::cancel_sql_file_execution,
commands::sql_file::sql_file_failure_log_info,
commands::sql_file::export_sql_file_failure_log,
commands::sql_file::read_sql_file_failure_log_text,
commands::external_sql::pending_open_sql_files,
commands::external_sql::read_external_sql_file,
commands::external_sql::inspect_external_sql_file,
commands::external_sql::write_external_sql_file,
commands::external_sql::save_external_sql_file,
commands::list_sql_files::list_sql_files_in_folder,
commands::list_sql_files::create_sql_file_in_folder,
commands::list_sql_files::rename_sql_file_in_folder,
commands::list_sql_files::delete_sql_file_in_folder,
commands::list_sql_files::create_sql_folder_in_folder,
commands::list_sql_files::rename_sql_folder_in_folder,
commands::list_sql_files::delete_sql_folder_in_folder,
commands::global_search::global_search,
commands::external_db::pending_open_db_files,
commands::keychain::read_keychain_password,
commands::keychain::read_keychain_passwords,
commands::deep_link::pending_open_connection_links,
commands::deep_link::pending_open_ai_config_links,
commands::deep_link::pending_open_plugin_install_links,
commands::table_import::preview_table_import_file,
commands::table_import::import_table_file,
commands::table_import::cancel_table_import,
commands::mongodb_import_export::preview_mongodb_import_file,
commands::mongodb_import_export::import_mongodb_file,
commands::mongodb_import_export::cancel_mongodb_import,
commands::mongodb_import_export::export_mongodb_query,
commands::mongodb_import_export::cancel_mongodb_export,
commands::mongodb_dump::inspect_mongodb_database_dump,
commands::mongodb_dump::prepare_mongodb_restore_source,
commands::mongodb_dump::release_mongodb_restore_source,
commands::mongodb_dump::dump_mongodb_database,
commands::mongodb_dump::restore_mongodb_database,
commands::mongodb_dump::cancel_mongodb_database_dump,
commands::redis_cmd::redis_list_databases,
commands::redis_cmd::redis_scan_keys,
commands::redis_cmd::redis_scan_keys_batch,
commands::redis_cmd::redis_scan_values,
commands::redis_cmd::redis_get_value,
commands::redis_cmd::redis_get_raw_value,
commands::redis_cmd::redis_get_ttl,
commands::redis_cmd::redis_get_stream_entries,
commands::redis_cmd::redis_get_stream_groups,
commands::redis_cmd::redis_get_stream_consumers,
commands::redis_cmd::redis_get_stream_pending,
commands::redis_cmd::redis_set_string,
commands::redis_cmd::redis_delete_key,
commands::redis_cmd::redis_rename_key,
commands::redis_cmd::redis_hash_set,
commands::redis_cmd::redis_hash_del,
commands::redis_cmd::redis_hash_field_update,
commands::redis_cmd::redis_hash_field_set_ttl,
commands::redis_cmd::redis_hash_field_set_expire_at,
commands::redis_cmd::redis_list_push,
commands::redis_cmd::redis_list_set,
commands::redis_cmd::redis_list_remove,
commands::redis_cmd::redis_set_add,
commands::redis_cmd::redis_set_remove,
commands::redis_cmd::redis_zadd,
commands::redis_cmd::redis_zrem,
commands::redis_cmd::redis_zset_update,
commands::redis_cmd::redis_stream_add,
commands::redis_cmd::redis_json_set,
commands::redis_cmd::redis_check_json_module,
commands::redis_cmd::redis_set_ttl,
commands::redis_cmd::redis_set_expire_at,
commands::redis_cmd::redis_set_keys_ttl,
commands::redis_cmd::redis_set_keys_expire_at,
commands::redis_cmd::redis_delete_keys,
commands::redis_cmd::redis_delete_keys_by_pattern,
commands::redis_cmd::redis_flush_db,
commands::redis_cmd::redis_execute_command,
commands::redis_cmd::redis_load_more,
commands::redis_cmd::redis_pubsub_publish,
commands::redis_pubsub_server::redis_pubsub_server_port,
commands::redis_cmd::redis_slowlog_get,
commands::redis_cmd::redis_cluster_master_nodes,
commands::etcd_cmd::etcd_supports_ttl,
commands::etcd_cmd::etcd_list_prefix,
commands::etcd_cmd::etcd_get,
commands::etcd_cmd::etcd_put,
commands::etcd_cmd::etcd_delete,
commands::etcd_cmd::etcd_rename,
commands::etcd_cmd::etcd_history,
commands::etcd_cmd::etcd_status,
commands::etcd_cmd::etcd_preflight,
commands::etcd_cmd::etcd_compact,
commands::etcd_cmd::etcd_defrag,
commands::etcd_cmd::etcd_watch_start,
commands::etcd_cmd::etcd_watch_poll,
commands::etcd_cmd::etcd_watch_stop,
commands::etcd_cmd::etcd_lease_list,
commands::etcd_cmd::etcd_lease_call,
commands::etcd_cmd::etcd_auth_call,
commands::zookeeper_cmd::zookeeper_list_prefix,
commands::zookeeper_cmd::zookeeper_get,
commands::zookeeper_cmd::zookeeper_put,
commands::zookeeper_cmd::zookeeper_delete,
commands::nacos_cmd::nacos_test_connection,
commands::nacos_cmd::nacos_list_namespaces,
commands::nacos_cmd::nacos_sidebar_snapshot,
commands::nacos_cmd::nacos_create_namespace,
commands::nacos_cmd::nacos_update_namespace,
commands::nacos_cmd::nacos_delete_namespace,
commands::nacos_cmd::nacos_list_configs,
commands::nacos_cmd::nacos_get_config,
commands::nacos_cmd::nacos_publish_config,
commands::nacos_cmd::nacos_delete_config,
commands::nacos_cmd::nacos_list_config_history,
commands::nacos_cmd::nacos_get_config_history,
commands::nacos_cmd::nacos_rollback_config,
commands::nacos_cmd::nacos_get_rnacos_console_captcha,
commands::nacos_cmd::nacos_login_rnacos_console,
commands::nacos_cmd::nacos_list_users,
commands::nacos_cmd::nacos_create_user,
commands::nacos_cmd::nacos_update_user,
commands::nacos_cmd::nacos_delete_user,
commands::nacos_cmd::nacos_list_role_bindings,
commands::nacos_cmd::nacos_assign_role,
commands::nacos_cmd::nacos_remove_role,
commands::nacos_cmd::nacos_access_snapshot,
commands::nacos_cmd::nacos_start_access_operation,
commands::nacos_cmd::nacos_get_access_operation,
commands::nacos_cmd::nacos_retry_access_operation,
commands::nacos_cmd::nacos_undo_access_operation,
commands::nacos_cmd::nacos_list_services,
commands::nacos_cmd::nacos_get_service,
commands::nacos_cmd::nacos_create_service,
commands::nacos_cmd::nacos_update_service,
commands::nacos_cmd::nacos_delete_service,
commands::nacos_cmd::nacos_list_instances,
commands::nacos_cmd::nacos_update_instance,
commands::nacos_cmd::nacos_register_instance,
commands::nacos_cmd::nacos_deregister_instance,
commands::nacos_cmd::nacos_get_dashboard,
commands::nacos_cmd::nacos_raw_request,
commands::nacos_cmd::nacos_search_config_content,
commands::nacos_cmd::nacos_cancel_operation,
commands::nacos_cmd::nacos_export_configs,
commands::nacos_cmd::nacos_preview_config_import,
commands::nacos_cmd::nacos_apply_config_import,
commands::nacos_cmd::nacos_preview_config_transfer,
commands::nacos_cmd::nacos_apply_config_transfer,
commands::saved_sql::load_saved_sql_library,
commands::saved_sql::load_saved_sql_files_for_sync,
commands::saved_sql::load_saved_sql_file,
commands::saved_sql::save_saved_sql_folder,
commands::saved_sql::delete_saved_sql_folder,
commands::saved_sql::save_saved_sql_file,
commands::saved_sql::delete_saved_sql_file,
commands::saved_sql::saved_sql_storage_dir,
commands::saved_sql::open_saved_sql_storage_dir,
commands::saved_sql::sync_saved_sql_directory,
commands::shortcut_recording::set_shortcut_recording,
commands::fs_open::reveal_path_in_file_manager,
commands::fs_open::is_sqlite_database_file,
commands::fs_open::delete_database_backup_files,
background_backup::database_backup_command,
background_backup::database_backup_background,
commands::sqlite_backup::backup_sqlite_database,
commands::sqlite_backup::restore_sqlite_database,
commands::mongo_cmd::mongo_list_databases,
commands::mongo_cmd::mongo_list_collections,
commands::vector_cmd::vector_collection_detail,
commands::mongo_cmd::mongo_create_database,
commands::mongo_cmd::mongo_drop_database,
commands::mongo_cmd::mongo_drop_collection,
commands::vector_cmd::vector_drop_database,
commands::vector_cmd::vector_drop_collection,
commands::vector_cmd::vector_rename_collection,
commands::mongo_cmd::mongo_rename_collection,
commands::mongo_cmd::mongo_clone_collection,
commands::docs::docs_collect_snapshot,
commands::docs::docs_collect_snapshot_for_export,
commands::docs::docs_load_annotations,
commands::docs::docs_apply_annotations,
commands::docs::docs_save_annotations,
commands::docs::docs_export_html,
commands::document_cmd::document_list_databases,
commands::document_cmd::document_list_collections,
commands::document_cmd::document_find_documents,
commands::document_cmd::document_count_documents,
commands::document_cmd::dynamodb_describe_table,
commands::document_cmd::elasticsearch_count_documents,
commands::document_cmd::elasticsearch_get_index_metadata,
commands::document_cmd::elasticsearch_delete_all_documents,
commands::document_cmd::document_list_gridfs_buckets,
commands::document_cmd::document_create_gridfs_bucket,
commands::document_cmd::document_delete_gridfs_bucket,
commands::document_cmd::document_list_gridfs_files,
commands::document_cmd::document_download_gridfs_file,
commands::document_cmd::document_upload_gridfs_file,
commands::document_cmd::document_delete_gridfs_file,
commands::mongo_cmd::mongo_find_documents,
commands::mongo_cmd::mongo_parse_shell_command,
commands::mongo_cmd::mongo_find_one,
commands::mongo_cmd::mongo_explain_find,
commands::mongo_cmd::mongo_count_documents,
commands::mongo_cmd::mongo_server_version,
commands::mongo_cmd::mongo_collection_stats,
commands::mongo_cmd::mongo_aggregate_documents,
commands::mongo_cmd::mongo_distinct,
commands::mongo_cmd::mongo_list_index_specs,
commands::mongo_cmd::mongo_create_index,
commands::mongo_cmd::mongo_create_user,
commands::mongo_cmd::mongo_run_command,
commands::mongo_cmd::mongo_drop_indexes,
commands::document_cmd::document_insert_document,
commands::mongo_cmd::mongo_insert_document,
commands::mongo_cmd::mongo_insert_documents,
commands::document_cmd::document_update_document,
commands::mongo_cmd::mongo_update_document,
commands::mongo_cmd::mongo_update_documents,
commands::mongo_cmd::mongo_replace_document,
commands::mongo_cmd::mongo_bulk_write,
commands::document_cmd::document_delete_document,
commands::document_cmd::document_save_meilisearch_batch,
commands::document_cmd::meilisearch_search_documents,
commands::document_cmd::meilisearch_fetch_documents,
commands::document_cmd::meilisearch_get_document,
commands::document_cmd::meilisearch_get_index_settings,
commands::document_cmd::meilisearch_update_index_settings,
commands::document_cmd::meilisearch_get_index_stats,
commands::document_cmd::meilisearch_get_index_overview,
commands::document_cmd::meilisearch_create_index,
commands::document_cmd::meilisearch_delete_index,
commands::document_cmd::meilisearch_delete_all_documents,
commands::document_cmd::meilisearch_get_system_overview,
commands::document_cmd::meilisearch_list_keys,
commands::document_cmd::meilisearch_get_key,
commands::document_cmd::meilisearch_create_key,
commands::document_cmd::meilisearch_update_key,
commands::document_cmd::meilisearch_delete_key,
commands::document_cmd::meilisearch_get_tasks,
commands::document_cmd::meilisearch_get_task,
commands::document_cmd::meilisearch_cancel_tasks,
commands::document_cmd::meilisearch_delete_tasks,
commands::hbase_cmd::hbase_get_table_schema,
commands::hbase_cmd::hbase_scan_rows,
commands::hbase_cmd::hbase_get_row,
commands::hbase_cmd::hbase_put_row,
commands::hbase_cmd::hbase_delete_row,
commands::hbase_cmd::hbase_create_table,
commands::hbase_cmd::hbase_delete_table,
commands::mongo_cmd::mongo_delete_document,
commands::mongo_cmd::mongo_delete_documents,
commands::mongo_cmd::mongo_find_one_and_update,
commands::mongo_cmd::mongo_find_one_and_replace,
commands::mongo_cmd::mongo_find_one_and_delete,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_test_connection,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_tenants,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_get_tenant,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_create_tenant,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_update_tenant,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_delete_tenant,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_namespaces,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_create_namespace,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_delete_namespace,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_get_namespace_policies,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_topics,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_topics_page,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_create_topic,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_delete_topic,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_update_partitions,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_get_topic_stats,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_get_topic_internal_stats,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_exchanges,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_exchanges_page,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_create_exchange,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_delete_exchange,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_bindings,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_bind,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_unbind,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_subscriptions,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_enrich_subscriptions,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_get_kafka_consumer_group_snapshot,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_create_subscription,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_delete_subscription,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_skip_messages,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_reset_cursor,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_clear_backlog,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_get_consumer_group_config,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_alter_consumer_group_config,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_peek_messages,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_expire_messages,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_producers,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_consumers,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_unload_topic,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_client_connections,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_client_channels,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_close_client_connection,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_set_publish_rate,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_set_dispatch_rate,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_set_subscribe_rate,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_set_backlog_quota,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_set_retention,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_get_effective_policies,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_grant_permission,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_revoke_permission,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_permissions,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_users,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_create_user,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_delete_user,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_user_permissions,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_grant_user_permission,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_revoke_user_permission,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_policies,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_set_policy,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_delete_policy,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_get_overview,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_nodes,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_issue_token,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_list_token_records,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_get_backlog,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_get_cluster_info,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_get_topic_route,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_alter_topic_config,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_skip_topic_accumulation,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_view_message,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_query_messages_by_key,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_query_messages_by_topic,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_query_message_trace,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_raw_request,
#[cfg(feature = "mq-admin")]
commands::mq_cmd::mq_send_message,
#[cfg(feature = "mq-admin")]
commands::mqtt_cmd::mqtt_get_broker_info,
#[cfg(feature = "mq-admin")]
commands::mqtt_cmd::mqtt_subscribe,
#[cfg(feature = "mq-admin")]
commands::mqtt_cmd::mqtt_save_topic_config,
#[cfg(feature = "mq-admin")]
commands::mqtt_cmd::mqtt_unsubscribe,
#[cfg(feature = "mq-admin")]
commands::mqtt_cmd::mqtt_delete_topic_config,
#[cfg(feature = "mq-admin")]
commands::mqtt_cmd::mqtt_publish,
#[cfg(feature = "mq-admin")]
commands::mqtt_cmd::mqtt_list_topics,
#[cfg(feature = "mq-admin")]
commands::mqtt_cmd::mqtt_list_saved_topic_configs,
#[cfg(feature = "mq-admin")]
commands::mqtt_cmd::mqtt_get_topic_tree,
#[cfg(feature = "mq-admin")]
commands::mqtt_cmd::mqtt_get_messages,
#[cfg(feature = "mq-admin")]
commands::mqtt_cmd::mqtt_clear_messages,
#[cfg(feature = "mq-admin")]
commands::mqtt_cmd::mqtt_delete_message,
commands::history::save_history,
commands::history::load_history,
commands::history::search_history,
commands::history::load_history_connection_options,
commands::history::clear_history,
commands::history::clear_history_by_source,
commands::history::cleanup_mcp_history_retention,
commands::history::delete_history_entry,
commands::task_history::load_task_runs,
commands::task_history::load_task_run,
commands::task_history::load_task_run_items,
commands::mcp::check_mcp_server_status,
commands::mcp::install_mcp_server,
commands::mcp::install_native_mcp_server,
commands::mcp::uninstall_mcp_server,
commands::mcp::uninstall_npm_mcp_server,
commands::update::check_for_updates,
commands::update::fetch_changelog,
commands::update::get_system_proxy_url,
commands::update::get_downloaded_update,
commands::update::discard_downloaded_update,
commands::update::download_update,
commands::update::cancel_update_download,
commands::update::install_downloaded_update,
commands::transfer::start_transfer,
commands::transfer::preview_transfer_ownership,
commands::transfer::cancel_transfer,
commands::database_export::begin_database_backup_snapshot,
commands::database_export::export_database_sql,
commands::database_export::cancel_database_export,
commands::database_export::clear_database_export_cancellation,
commands::database_export::database_export_destination_needs_confirmation,
commands::database_export::record_database_export_destination,
commands::table_export::start_table_export,
commands::table_export::cancel_table_export,
commands::dameng_dump::start_dameng_dump,
commands::dameng_dump::cancel_dameng_dump,
commands::dameng_dump::resolve_dameng_dump_tool,
commands::query_result_export::start_query_result_export,
commands::query_result_export::create_query_result_temp_file,
commands::query_result_export::open_query_result_temp_file,
commands::query_result_export::cancel_query_result_export,
commands::csv_export::export_query_result_csv,
commands::csv_export::export_table_data_csv,
commands::xlsx_export::export_query_result_xlsx,
commands::xlsx_export::export_query_results_xlsx,
commands::text_export::export_query_result_json,
commands::text_export::export_query_result_markdown,
commands::text_export::export_query_result_html,
commands::agents::list_installed_agents,
commands::agents::list_installed_agents_local,
commands::agents::is_agent_installed,
commands::agents::get_driver_store_usage,
commands::agents::clear_driver_download_cache,
commands::agents::get_driver_runtime_summary,
commands::agents::stop_driver_runtime,
commands::agents::restart_driver_runtime,
commands::agents::install_agent,
commands::agents::cancel_agent_install,
commands::agents::upgrade_all_agents,
commands::agents::cancel_agent_upgrade_all,
commands::agents::check_agent_update_blockers,
commands::agents::uninstall_agent,
commands::agents::check_jre_installed,
commands::agents::get_agent_java_runtime_config,
commands::agents::set_agent_java_runtime_config,
commands::agents::uninstall_jre,
commands::agents::reinstall_jre,
commands::agents::invalidate_agent_registry_cache,
commands::agents::import_agents_from_zip,
commands::agents::preview_agent_offline_export,
commands::agents::export_agents_offline,
commands::agents::import_agent_driver_cmd,
commands::agents::import_agent_jar_cmd,
commands::system_fonts::list_system_fonts,
commands::ssh_config::list_ssh_config_hosts,
commands::ssh_keys::list_local_ssh_keys,
commands::ssh_prompt::ssh_prompt_ready,
commands::ssh_prompt::ssh_prompt_not_ready,
commands::ssh_prompt::resolve_ssh_prompt,
commands::tunnel_profiles::load_tunnel_profiles,
commands::tunnel_profiles::save_tunnel_profiles,
commands::tunnel_profiles::test_tunnel_profile,
])))
.build(tauri::generate_context!())
.inspect(|app| {
append_startup_probe(format!("tauri application built after {:?}", startup_begin.elapsed()));
startup_recovery::start_watchdog(app.handle());
})
.unwrap_or_else(|error| {
append_startup_probe(format!("tauri application build failed: {error}"));
panic!("error while building tauri application: {error}");
})
.run(|app_handle, event| {
startup_recovery::record_run_event();
#[cfg(not(target_os = "macos"))]
let _ = (&app_handle, &event);
if let RunEvent::ExitRequested { code, api, .. } = &event {
let confirmed_exit = app_handle
.try_state::<CloseBehaviorState>()
.map(|state| state.take_confirmed_exit())
.unwrap_or(false);
if should_confirm_app_exit_request(std::env::consts::OS, *code, confirmed_exit) {
api.prevent_exit();
request_app_close(app_handle, "quit");
} else {
// Restart exits and the no-frontend native quit bypass
// `complete_app_close`, so hide the window here too; the
// shutdown below gives WindowServer time to remove it.
if should_hide_window_before_exit(std::env::consts::OS) {
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.hide();
}
}
tauri::async_runtime::block_on(shutdown_runtime_resources(app_handle));
}
}
#[cfg(target_os = "macos")]
if let RunEvent::Opened { urls } = &event {
if urls.iter().any(|url| commands::deep_link::is_app_open_deep_link(url.as_str())) {
show_main_window(app_handle);
}
let links: Vec<String> = urls
.iter()
.map(|url| url.to_string())
.filter_map(|url| commands::deep_link::connection_deep_link_from_arg(&url))
.collect();
open_connection_deep_links(app_handle, links);
let ai_config_links: Vec<String> = urls
.iter()
.map(|url| url.to_string())
.filter_map(|url| commands::deep_link::ai_config_deep_link_from_arg(&url))
.collect();
open_ai_config_deep_links(app_handle, ai_config_links);
let plugin_install_links: Vec<String> = urls
.iter()
.map(|url| url.to_string())
.filter_map(|url| commands::deep_link::plugin_install_deep_link_from_arg(&url))
.collect();
open_plugin_install_deep_links(app_handle, plugin_install_links);
let paths: Vec<String> = urls
.iter()
.filter_map(|url| url.to_file_path().ok())
.filter(|path| commands::external_sql::is_sql_file_path(path))
.map(|path| path.to_string_lossy().to_string())
.collect();
if !paths.is_empty() {
if let Some(state) = app_handle.try_state::<commands::external_sql::ExternalSqlOpenState>() {
state.push(paths.clone());
}
let _ = app_handle.emit("dbx-open-sql-files", paths);
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
let db_paths: Vec<String> = urls
.iter()
.filter_map(|url| url.to_file_path().ok())
.filter(|path| commands::external_db::is_db_file_path(path))
.map(|path| path.to_string_lossy().to_string())
.collect();
if !db_paths.is_empty() {
if let Some(state) = app_handle.try_state::<commands::external_db::ExternalDbOpenState>() {
state.push(db_paths.clone());
}
let _ = app_handle.emit("dbx-open-db-files", db_paths);
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
}
#[cfg(target_os = "macos")]
if let RunEvent::Reopen { has_visible_windows, .. } = &event {
if !has_visible_windows && !show_main_window(app_handle) {
// Dock / Finder reopen is the other way back into a hidden
// instance, and it fails silently for the same reason.
eprintln!(
"[WINDOW] reopen could not reveal the main window; {}",
main_window_probe_state(app_handle)
);
}
let app_handle = app_handle.clone();
let migration_gate =
app_handle.try_state::<Arc<migration_gate::MigrationGate>>().map(|state| state.inner().clone());
tauri::async_runtime::spawn(async move {
let Some(migration_gate) = migration_gate else {
return;
};
migration_gate.wait().await;
if let Some(state) = app_handle.try_state::<AppState>() {
state.refresh_connections().await;
}
});
}
if let RunEvent::Resumed = &event {
let app_handle = app_handle.clone();
let migration_gate =
app_handle.try_state::<Arc<migration_gate::MigrationGate>>().map(|state| state.inner().clone());
tauri::async_runtime::spawn(async move {
let Some(migration_gate) = migration_gate else {
return;
};
migration_gate.wait().await;
if let Some(state) = app_handle.try_state::<AppState>() {
state.refresh_connections().await;
}
});
}
});
}