mod commands; mod data_dir; mod db; #[cfg(target_os = "macos")] mod macos_app_delegate; mod models; mod window_state_guard; use commands::connection::AppState; 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::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; 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"; pub struct CloseBehaviorState { confirmed_exit: AtomicBool, } impl CloseBehaviorState { fn new() -> Self { Self { confirmed_exit: 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) } } #[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") } 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)) } #[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() } }) } #[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_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 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, "Copy Support Info", true, None::<&str>)?; let quit_item = MenuItem::with_id(app_handle, APP_MENU_QUIT_ID, format!("Quit {app_name}"), true, Some("Cmd+Q"))?; 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, &[&PredefinedMenuItem::close_window(app_handle, None)?])?, &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)?, &PredefinedMenuItem::separator(app_handle)?, &PredefinedMenuItem::close_window(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, } #[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(target_os = "linux")] fn linux_drm_render_devices() -> 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_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"); Some(LinuxDrmRenderDevice { device_file: std::path::Path::new("/dev/dri").join(node_name), driver, boot_vga, }) }) .collect::>(); devices.sort_by(|left, right| left.device_file.cmp(&right.device_file)); devices } #[cfg(target_os = "linux")] fn linux_nvidia_driver() -> LinuxNvidiaDriver { let devices = linux_drm_render_devices(); 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 render_driver = linux_selected_drm_render_device(explicit_device_file.as_deref(), &devices) .and_then(|device| device.driver.as_deref()); linux_nvidia_driver_from_state( std::path::Path::new("/dev/nvidiactl").exists(), std::path::Path::new("/proc/driver/nvidia/version").exists(), render_driver, ) } #[cfg_attr(not(target_os = "linux"), allow(dead_code))] fn linux_webkit_rendering_workarounds(driver: LinuxNvidiaDriver) -> &'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 => { // 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_system_gtk3_immodules_cache_path() -> Option<&'static str> { [ "/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules.cache", "/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules.cache", "/usr/lib64/gtk-3.0/3.0.0/immodules.cache", "/usr/lib/gtk-3.0/3.0.0/immodules.cache", ] .iter() .copied() .find(|path| std::path::Path::new(path).is_file()) } #[cfg_attr(not(target_os = "linux"), allow(dead_code))] fn linux_appimage_wayland_backend_override( appimage: Option<&std::ffi::OsStr>, wayland_display: Option<&std::ffi::OsStr>, gdk_backend: Option<&std::ffi::OsStr>, ) -> Option<&'static str> { if appimage.is_some() && wayland_display.is_some() && gdk_backend.is_none() { // AppImage uses the host GTK/WebKitGTK stack. Prefer XWayland for the // affected Wayland/EGL path, but keep Wayland and other compiled // backends as fallbacks for systems without XWayland. Some("x11,wayland,*") } else { None } } #[cfg_attr(not(target_os = "linux"), allow(dead_code))] fn linux_appimage_system_gtk_immodules_cache( appimage: Option<&std::ffi::OsStr>, appdir: Option<&std::ffi::OsStr>, gtk_im_module: Option<&std::ffi::OsStr>, gtk_im_module_file: Option<&std::ffi::OsStr>, system_cache_path: Option<&'static str>, ) -> Option<&'static str> { let system_cache_path = system_cache_path?; if appimage.is_none() || gtk_im_module.is_none() { return None; } let Some(gtk_im_module_file) = gtk_im_module_file else { return Some(system_cache_path); }; let appdir = appdir?; if std::path::Path::new(gtk_im_module_file).starts_with(std::path::Path::new(appdir)) { Some(system_cache_path) } else { None } } #[cfg(target_os = "linux")] fn apply_linux_webkit_rendering_workarounds() { for (key, value) in linux_webkit_rendering_workarounds(linux_nvidia_driver()) { if std::env::var_os(key).is_none() { std::env::set_var(key, value); } } if let Some(gdk_backend) = linux_appimage_wayland_backend_override( std::env::var_os("APPIMAGE").as_deref(), std::env::var_os("WAYLAND_DISPLAY").as_deref(), std::env::var_os("GDK_BACKEND").as_deref(), ) { std::env::set_var("GDK_BACKEND", gdk_backend); } if let Some(gtk_im_module_file) = linux_appimage_system_gtk_immodules_cache( std::env::var_os("APPIMAGE").as_deref(), std::env::var_os("APPDIR").as_deref(), std::env::var_os("GTK_IM_MODULE").as_deref(), std::env::var_os("GTK_IM_MODULE_FILE").as_deref(), linux_system_gtk3_immodules_cache_path(), ) { // linuxdeploy-plugin-gtk points GTK_IM_MODULE_FILE at the bundled // cache. That hides host IM modules such as fcitx5/ibus, so prefer the // host GTK cache when the user has configured a GTK input method. std::env::set_var("GTK_IM_MODULE_FILE", gtk_im_module_file); } } fn show_main_window(app: &tauri::AppHandle) { if let Some(window) = app.get_webview_window("main") { let _ = window.show(); let _ = window.unminimize(); let _ = window.set_focus(); } } 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) { 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; } if let Some(state) = app.try_state::() { state.push(links.clone()); } let _ = app.emit("dbx-open-connection-links", links); show_main_window(app); } #[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 = MenuBuilder::new(manager).text("show", "Show DBX").separator().text("quit", "Quit DBX").build()?; 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) -> tauri::Result<()> { use objc2::{AllocAnyThread, MainThreadMarker}; use objc2_app_kit::{NSApplication, NSImage}; use objc2_foundation::NSData; 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 data = NSData::with_bytes(icon_bytes); if let Some(icon) = NSImage::initWithData(NSImage::alloc(), &data) { unsafe { application.setApplicationIconImage(Some(&icon)) }; } else { log::warn!("Failed to decode the selected macOS application icon"); } }) } 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) } #[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(()) } #[cfg(test)] #[allow(clippy::items_after_test_module)] mod tests { use super::{ linux_appimage_system_gtk_immodules_cache, linux_appimage_wayland_backend_override, linux_nvidia_driver_from_state, linux_selected_drm_render_device, linux_webkit_rendering_workarounds, native_window_decorations_override, should_confirm_app_exit_request, should_hide_window_on_close, should_setup_desktop_tray, should_show_main_window_after_setup, uses_application_level_icon, LinuxDrmRenderDevice, LinuxNvidiaDriver, }; use std::ffi::OsStr; use std::path::{Path, PathBuf}; const TEST_GTK3_IMMODULES_CACHE: &str = "/usr/lib/test/gtk-3.0/3.0.0/immodules.cache"; #[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 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)); } #[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 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 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 } } #[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), &[("WEBKIT_DISABLE_DMABUF_RENDERER", "1"), ("__NV_DISABLE_EXPLICIT_SYNC", "1")] ); assert_eq!( linux_webkit_rendering_workarounds(LinuxNvidiaDriver::Nouveau), &[("WEBKIT_DISABLE_DMABUF_RENDERER", "1")] ); assert_eq!(linux_webkit_rendering_workarounds(LinuxNvidiaDriver::None), &[]); } #[test] fn prefers_x11_for_appimage_wayland_when_backend_is_not_user_configured() { assert_eq!( linux_appimage_wayland_backend_override( Some(OsStr::new("/tmp/DBX.AppImage")), Some(OsStr::new("wayland-0")), None ), Some("x11,wayland,*") ); assert_eq!( linux_appimage_wayland_backend_override( Some(OsStr::new("/tmp/DBX.AppImage")), Some(OsStr::new("wayland-0")), Some(OsStr::new("wayland")) ), None ); assert_eq!(linux_appimage_wayland_backend_override(Some(OsStr::new("/tmp/DBX.AppImage")), None, None), None); assert_eq!(linux_appimage_wayland_backend_override(None, Some(OsStr::new("wayland-0")), None), None); } #[test] fn prefers_system_gtk_immodules_cache_for_appimage_input_methods() { assert_eq!( linux_appimage_system_gtk_immodules_cache( Some(OsStr::new("/tmp/DBX.AppImage")), Some(OsStr::new("/tmp/.mount_DBX123")), Some(OsStr::new("fcitx5")), Some(OsStr::new("/tmp/.mount_DBX123/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules.cache")), Some(TEST_GTK3_IMMODULES_CACHE), ), Some(TEST_GTK3_IMMODULES_CACHE) ); assert_eq!( linux_appimage_system_gtk_immodules_cache( Some(OsStr::new("/tmp/DBX.AppImage")), Some(OsStr::new("/tmp/.mount_DBX123")), Some(OsStr::new("ibus")), None, Some(TEST_GTK3_IMMODULES_CACHE), ), Some(TEST_GTK3_IMMODULES_CACHE) ); } #[test] fn preserves_external_gtk_immodules_cache_overrides() { assert_eq!( linux_appimage_system_gtk_immodules_cache( Some(OsStr::new("/tmp/DBX.AppImage")), Some(OsStr::new("/tmp/.mount_DBX123")), Some(OsStr::new("fcitx5")), Some(OsStr::new("/opt/custom/immodules.cache")), Some(TEST_GTK3_IMMODULES_CACHE), ), None ); } #[test] fn skips_system_gtk_immodules_cache_without_required_context() { assert_eq!( linux_appimage_system_gtk_immodules_cache( None, Some(OsStr::new("/tmp/.mount_DBX123")), Some(OsStr::new("fcitx5")), Some(OsStr::new("/tmp/.mount_DBX123/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules.cache")), Some(TEST_GTK3_IMMODULES_CACHE), ), None ); assert_eq!( linux_appimage_system_gtk_immodules_cache( Some(OsStr::new("/tmp/DBX.AppImage")), Some(OsStr::new("/tmp/.mount_DBX123")), None, Some(OsStr::new("/tmp/.mount_DBX123/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules.cache")), Some(TEST_GTK3_IMMODULES_CACHE), ), None ); assert_eq!( linux_appimage_system_gtk_immodules_cache( Some(OsStr::new("/tmp/DBX.AppImage")), Some(OsStr::new("/tmp/.mount_DBX123")), Some(OsStr::new("fcitx5")), Some(OsStr::new("/tmp/.mount_DBX123/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules.cache")), None, ), None ); } } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { rustls::crypto::aws_lc_rs::default_provider().install_default().expect("Failed to install rustls crypto provider"); #[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(tauri_plugin_single_instance::init(|app, args, cwd| { let links = commands::deep_link::connection_deep_links_from_args(args.clone()); open_connection_deep_links(app, 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); } show_main_window(app); })) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_window_state::Builder::default().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}"); } } }); builder .setup(move |app| { let setup_start = Instant::now(); eprintln!("[STARTUP] plugins registered in {:?}", startup_begin.elapsed()); 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(); std::fs::create_dir_all(&data_dir).expect("Failed to create data dir"); 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"); let t = Instant::now(); let storage = tauri::async_runtime::block_on(async { let s = Storage::open(&db_path).await.expect("Failed to open storage"); eprintln!("[STARTUP] Storage::open in {:?}", t.elapsed()); let t2 = Instant::now(); s.migrate_from_json(&data_dir).await.expect("Failed to migrate JSON data"); eprintln!("[STARTUP] migrate_from_json in {:?}", t2.elapsed()); s }); 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()); 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")) }; 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 state = Arc::new(state); app.manage(state.clone()); 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(CloseBehaviorState::new()); #[cfg(target_os = "macos")] macos_app_delegate::install_dock_quit_handler(app.handle()); let startup_links = commands::deep_link::connection_deep_links_from_args(std::env::args().skip(1)); open_connection_deep_links(app.handle(), startup_links); let app_handle = app.handle().clone(); commands::mcp_bridge::start(app_handle, state, data_dir); eprintln!("[STARTUP] setup complete in {:?} (total {:?})", setup_start.elapsed(), startup_begin.elapsed()); 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); } } 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)?; window_state_guard::enforce_main_window_bounds(app.handle()); if should_show_main_window_after_setup() { show_main_window(app.handle()); } #[cfg(any(windows, target_os = "linux"))] let _ = app.deep_link().register_all(); Ok(()) }) .on_window_event(|window, event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { 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(tauri::generate_handler![ commands::ai::ai_complete, commands::ai::ai_stream, commands::ai::ai_agent_stream, commands::ai::ai_cancel_stream, commands::ai::ai_test_connection, commands::ai::ai_list_models, 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_conversation, commands::ai::load_ai_conversations, commands::ai::delete_ai_conversation, 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::app_settings::load_desktop_settings, commands::app_settings::save_desktop_settings, commands::app_settings::complete_app_close, commands::app_settings::request_app_close_from_window_controls, commands::window_controls::set_macos_traffic_light_position, 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_editor_settings, commands::app_settings::save_editor_settings, commands::app_settings::load_open_tabs_state, commands::app_settings::save_open_tabs_state, commands::app_settings::load_saved_sql_editor_positions, commands::app_settings::save_saved_sql_editor_positions, commands::app_settings::load_native_debug_logs, commands::support_info::get_app_support_info, commands::cloud_sync::webdav_sync_test, 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::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_upload, commands::cloud_sync::snippet_sync_download, commands::connection::test_connection, commands::connection::test_connection_with_info, commands::connection::connect_db, commands::connection::connection_final_proxy_port, commands::connection::disconnect_db, commands::connection::close_database_connection, commands::connection::refresh_connections, commands::connection::check_connection_health, commands::connection::connection_identifier_quote, commands::connection::connection_database_info, commands::connection::save_connection_database_info, commands::connection::save_connections, commands::connection::load_connections, commands::connection::save_sidebar_layout, commands::connection::load_sidebar_layout, commands::plugins::list_plugins, 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::list_databases, commands::schema::list_doris_catalogs, commands::schema::list_doris_catalog_databases, commands::schema::list_sqlserver_linked_servers, commands::schema::list_sqlserver_linked_server_catalogs, commands::schema::list_sqlserver_linked_server_schemas, commands::schema::list_sqlserver_linked_server_tables, commands::schema::list_tables, commands::schema::get_table_comment, commands::schema::list_objects, commands::schema::list_object_statistics, commands::schema::list_completion_objects, commands::schema::completion_assistant_search, commands::schema::get_object_source, commands::schema::list_schemas, commands::schema::list_schema_infos, commands::schema::list_data_types, commands::schema::get_columns, commands::schema::get_sqlserver_column_metadata, commands::schema::list_indexes, commands::schema::list_foreign_keys, commands::schema::list_triggers, commands::schema::get_table_ddl, commands::schema::list_functions, commands::schema::list_sequences, commands::schema::list_rules, commands::schema::list_owners, commands::schema::list_extensions, commands::schema::list_available_extensions, commands::schema_diff::prepare_schema_diff, commands::schema_diff::generate_schema_sync_sql, 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_multi, commands::query::cancel_query, commands::query::close_query_session, commands::query::close_client_connection_session, commands::query::execute_batch, commands::query::execute_script, commands::query::execute_in_transaction, 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::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_search_result_where, commands::query::build_rename_object_sql, commands::query::build_create_database_sql, #[cfg(feature = "duckdb-bundled")] 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_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::build_executable_object_source_sql, commands::query::build_editable_object_source, commands::query::build_routine_rename_object_source_statements, commands::query::build_view_ddl_sql, commands::query::build_table_structure_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_single_column_alter_sql, commands::query::analyze_editable_query_editability, commands::query::prepare_data_grid_save, commands::query::build_data_grid_copy_update_statements, commands::query::build_data_grid_copy_insert_statement, 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_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::execute_sql_file, commands::sql_file::cancel_sql_file_execution, commands::external_sql::pending_open_sql_files, commands::external_sql::read_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::external_db::pending_open_db_files, commands::keychain::read_keychain_password, commands::keychain::read_keychain_passwords, commands::deep_link::pending_open_connection_links, commands::table_import::preview_table_import_file, commands::table_import::import_table_file, commands::table_import::cancel_table_import, 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_set_string, commands::redis_cmd::redis_delete_key, commands::redis_cmd::redis_hash_set, commands::redis_cmd::redis_hash_del, 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_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_delete_keys, 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_list_prefix, commands::etcd_cmd::etcd_get, commands::etcd_cmd::etcd_put, commands::etcd_cmd::etcd_delete, 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_create_namespace, commands::nacos_cmd::nacos_update_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_list_services, commands::nacos_cmd::nacos_list_instances, commands::nacos_cmd::nacos_update_instance, commands::nacos_cmd::nacos_raw_request, commands::saved_sql::load_saved_sql_library, 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::fs_open::reveal_path_in_file_manager, commands::fs_open::is_sqlite_database_file, commands::fs_open::delete_database_backup_files, commands::sqlite_backup::backup_sqlite_database, commands::mongo_cmd::mongo_list_databases, commands::mongo_cmd::mongo_list_collections, commands::mongo_cmd::vector_collection_detail, commands::mongo_cmd::mongo_create_database, commands::mongo_cmd::mongo_drop_database, commands::mongo_cmd::mongo_drop_collection, commands::document_cmd::document_list_databases, commands::document_cmd::document_list_collections, commands::document_cmd::document_find_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_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_create_index, 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::document_cmd::document_delete_document, 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_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_subscriptions, #[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_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_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_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_raw_request, #[cfg(feature = "mq-admin")] commands::mq_cmd::mq_send_message, commands::history::save_history, commands::history::load_history, commands::history::clear_history, commands::history::delete_history_entry, commands::mcp::check_mcp_server_status, commands::mcp::install_mcp_server, commands::update::check_for_updates, commands::update::fetch_changelog, commands::update::get_system_proxy_url, commands::update::download_and_install_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::table_export::start_table_export, commands::table_export::cancel_table_export, commands::query_result_export::start_query_result_export, 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::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::upgrade_all_agents, 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::import_agent_jar_cmd, commands::system_fonts::list_system_fonts, commands::ssh_config::list_ssh_config_hosts, commands::tunnel_profiles::load_tunnel_profiles, commands::tunnel_profiles::save_tunnel_profiles, commands::tunnel_profiles::test_tunnel_profile, ]) .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, 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 if let Some(state) = app_handle.try_state::>() { tauri::async_runtime::block_on(state.shutdown_background_tasks(Duration::from_secs(3))); } } #[cfg(target_os = "macos")] if let RunEvent::Opened { urls } = &event { 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 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); } let app_handle = app_handle.clone(); tauri::async_runtime::spawn(async move { if let Some(state) = app_handle.try_state::() { state.refresh_connections().await; } }); } if let RunEvent::Resumed = &event { let app_handle = app_handle.clone(); tauri::async_runtime::spawn(async move { if let Some(state) = app_handle.try_state::() { state.refresh_connections().await; } }); } }); }