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