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>, } 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) { 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, 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 { match target_os { "windows" | "linux" => Some(false), _ => None, } } #[cfg(target_os = "macos")] fn build_app_menu(app_handle: &tauri::AppHandle) -> tauri::Result> { 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(¤t_app_locale(app_handle)), true, None::<&str>, )?; let quit_item = MenuItem::with_id( app_handle, APP_MENU_QUIT_ID, app_menu_quit_label(¤t_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(¤t_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))?, ©_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, 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 { 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 { 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::>(); devices.sort_by(|left, right| left.device_file.cmp(&right.device_file)); devices } #[cfg(target_os = "linux")] fn linux_drm_render_devices() -> Vec { 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(app: &tauri::AppHandle) -> 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(app: &tauri::AppHandle) -> 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(app: &tauri::AppHandle) { 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(app: &tauri::AppHandle) { 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(app: &tauri::AppHandle, window: &tauri::Window) { 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(app: &tauri::AppHandle, target: &str) { let frontend_ready = app.try_state::().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::() { 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) { if links.is_empty() { return; } let should_emit = app .try_state::() .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) { if links.is_empty() { return; } let should_emit = app .try_state::() .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) { if links.is_empty() { return; } let should_emit = app .try_state::() .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>(manager: &M) -> String { match manager.try_state::() { Some(state) => state.get(), None => sys_locale::get_locale().unwrap_or_default(), } } fn build_tray_menu>(manager: &M) -> tauri::Result> { let (show_label, quit_label) = tray_menu_labels_for_locale(¤t_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>( manager: &M, _icon_theme: DesktopIconTheme, ) -> tauri::Result<()> { let menu = build_tray_menu(manager)?; let mut tray = TrayIconBuilder::::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::() { backups.shutdown().await; } if let Some(server) = app_handle.try_state::() { server.shutdown(Duration::from_secs(1)).await; } if let Some(state) = app_handle.try_state::>() { 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) -> bool + Send + Sync + 'static, ) -> impl Fn(tauri::ipc::Invoke) -> 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::() { 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::() { 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::() { 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 = 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 = 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::() { 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::() { 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::().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::() .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 = 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 = 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 = 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 = 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::() { 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 = 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::() { 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::>().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::() { state.refresh_connections().await; } }); } if let RunEvent::Resumed = &event { let app_handle = app_handle.clone(); let migration_gate = app_handle.try_state::>().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::() { state.refresh_connections().await; } }); } }); }