diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index cfb37e3493..c717586a55 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -18,6 +18,7 @@ import { EDITOR_THEMES, FONT_FAMILIES, DEFAULT_EDITOR_SETTINGS, + DEFAULT_DESKTOP_SETTINGS, type AiProvider, type AiApiStyle, type EditorTheme, @@ -59,6 +60,7 @@ const editTheme = ref(settingsStore.editorSettings.theme); const editExecuteMode = ref(settingsStore.editorSettings.executeMode); const editWordWrap = ref(settingsStore.editorSettings.wordWrap); const editAppLayout = ref(settingsStore.editorSettings.appLayout); +const editRunInBackground = ref(settingsStore.desktopSettings.run_in_background); const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize); const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings.shortcuts)); const editSidebarActivation = ref(settingsStore.editorSettings.sidebarActivation); @@ -122,6 +124,7 @@ watch( editExecuteMode.value = settingsStore.editorSettings.executeMode; editWordWrap.value = settingsStore.editorSettings.wordWrap; editAppLayout.value = settingsStore.editorSettings.appLayout; + editRunInBackground.value = settingsStore.desktopSettings.run_in_background; editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize; editShortcuts.value = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts); editSidebarActivation.value = settingsStore.editorSettings.sidebarActivation; @@ -152,6 +155,7 @@ function hasChanges(): boolean { editExecuteMode.value !== settingsStore.editorSettings.executeMode || editWordWrap.value !== settingsStore.editorSettings.wordWrap || editAppLayout.value !== settingsStore.editorSettings.appLayout || + editRunInBackground.value !== settingsStore.desktopSettings.run_in_background || editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize || JSON.stringify(editShortcuts.value) !== JSON.stringify(settingsStore.editorSettings.shortcuts) || editSidebarActivation.value !== settingsStore.editorSettings.sidebarActivation || @@ -161,7 +165,7 @@ function hasChanges(): boolean { ); } -function applySettings() { +async function applySettings() { if (hasBlockingShortcutConflicts.value) return; settingsStore.updateEditorSettings({ fontFamily: editFontFamily.value, @@ -176,6 +180,9 @@ function applySettings() { autoSelectActiveSidebarNode: editAutoSelectActiveSidebarNode.value, sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value), }); + await settingsStore.updateDesktopSettings({ + run_in_background: editRunInBackground.value, + }); emit("update:open", false); } @@ -186,6 +193,7 @@ function resetDefaults() { editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode; editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap; editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout; + editRunInBackground.value = DEFAULT_DESKTOP_SETTINGS.run_in_background; editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize; editShortcuts.value = normalizeShortcutSettings(DEFAULT_EDITOR_SETTINGS.shortcuts); editSidebarActivation.value = DEFAULT_EDITOR_SETTINGS.sidebarActivation; @@ -292,6 +300,8 @@ watch( newPassword.value = ""; confirmNewPassword.value = ""; await settingsStore.initAiConfig(); + await settingsStore.initDesktopSettings(); + editRunInBackground.value = settingsStore.desktopSettings.run_in_background; syncAiEditState(); } }, @@ -825,6 +835,16 @@ watch( +
+
+ +

{{ t("settings.runInBackgroundDescription") }}

+
+ +
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 4a05db5c9d..5fb636e57f 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1194,6 +1194,9 @@ export default { appLayoutSeparatedDescription: "Clear panel separation with block-style tabs.", appLayoutClassic: "Classic compact", appLayoutClassicDescription: "Continuous tab bar with compact panel headers.", + runInBackground: "Keep running after closing window", + runInBackgroundDescription: + "When enabled, DBX shows a tray/menu bar icon and closing the window keeps it in the background. When disabled, no icon is shown and closing the window quits the app.", sidebarActivation: "Sidebar activation", sidebarActivationSingle: "Single click", sidebarActivationSingleDescription: "Open actionable sidebar items with one click.", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index a1db082bda..b2aab95c70 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1092,6 +1092,9 @@ export default { appLayoutSeparatedDescription: "Separación clara de paneles con pestañas en bloque.", appLayoutClassic: "Clásico compacto", appLayoutClassicDescription: "Barra de pestañas continua con encabezados de panel compactos.", + runInBackground: "Seguir ejecutando al cerrar la ventana", + runInBackgroundDescription: + "Si está activado, DBX muestra un icono en la bandeja/barra de menú y cerrar la ventana lo deja en segundo plano. Si está desactivado, no se muestra el icono y cerrar la ventana sale de la app.", sidebarActivation: "Activación de la barra lateral", sidebarActivationSingle: "Un clic", sidebarActivationSingleDescription: "Abrir elementos accionables de la barra lateral con un clic.", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 6dda0a7932..5051d719a1 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1172,6 +1172,9 @@ export default { appLayoutSeparatedDescription: "面板分隔更清晰,标签页为块状样式。", appLayoutClassic: "经典紧凑", appLayoutClassicDescription: "使用连续标签栏和更紧凑的面板标题。", + runInBackground: "关闭窗口后后台驻留", + runInBackgroundDescription: + "开启时显示系统托盘/菜单栏图标,关闭窗口会隐藏到后台;关闭后不显示图标,关闭窗口将退出应用。", sidebarActivation: "侧边栏打开方式", sidebarActivationSingle: "单击打开", sidebarActivationSingleDescription: "单击即可打开侧边栏中的可操作项目。", diff --git a/apps/desktop/src/lib/api.ts b/apps/desktop/src/lib/api.ts index fac2242ab4..6fcbd68527 100644 --- a/apps/desktop/src/lib/api.ts +++ b/apps/desktop/src/lib/api.ts @@ -95,6 +95,8 @@ export const aiTestConnection = forward("aiTestConnection"); export const aiListModels = forward("aiListModels"); export const saveAiConfig = forward("saveAiConfig"); export const loadAiConfig = forward("loadAiConfig"); +export const loadDesktopSettings = forward("loadDesktopSettings"); +export const saveDesktopSettings = forward("saveDesktopSettings"); export const saveAiConversation = forward("saveAiConversation"); export const loadAiConversations = forward("loadAiConversations"); export const deleteAiConversation = forward("deleteAiConversation"); diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts index 6068c394cc..8a4c0ea8e5 100644 --- a/apps/desktop/src/lib/http.ts +++ b/apps/desktop/src/lib/http.ts @@ -25,6 +25,7 @@ import type { AiStreamChunk, AiConversation, AiModelInfo, + DesktopSettings, DriverInstallProgress, JavaRuntimeConfig, UpdateInfo, @@ -466,6 +467,14 @@ export async function loadAiConfig(): Promise { return get("/api/ai/config"); } +export async function loadDesktopSettings(): Promise { + return { run_in_background: true }; +} + +export async function saveDesktopSettings(_settings: DesktopSettings): Promise { + return; +} + // --- AI Conversations --- export async function saveAiConversation(conversation: AiConversation): Promise { diff --git a/apps/desktop/src/lib/tauri.ts b/apps/desktop/src/lib/tauri.ts index 8022f4fb3d..faa56cd1d4 100644 --- a/apps/desktop/src/lib/tauri.ts +++ b/apps/desktop/src/lib/tauri.ts @@ -40,6 +40,10 @@ export interface JavaRuntimeConfig { custom_java_path: string | null; } +export interface DesktopSettings { + run_in_background: boolean; +} + export interface DriverInstallProgress { step: string; downloaded?: number; @@ -117,6 +121,14 @@ export async function loadAiConfig(): Promise { return invoke("load_ai_config"); } +export async function loadDesktopSettings(): Promise { + return invoke("load_desktop_settings"); +} + +export async function saveDesktopSettings(settings: DesktopSettings): Promise { + return invoke("save_desktop_settings", { settings }); +} + export async function listSystemFonts(): Promise { return invoke("list_system_fonts"); } diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 7f4697657b..93679c2748 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -34,6 +34,14 @@ export interface AiConfig { enableThinking?: boolean; } +export interface DesktopSettings { + run_in_background: boolean; +} + +export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = { + run_in_background: true, +}; + export interface AiProviderPreset extends Omit { label: string; iconSlug?: string; @@ -311,9 +319,32 @@ function saveEditorSettings(settings: EditorSettings) { export const useSettingsStore = defineStore("settings", () => { const aiConfig = ref(normalizeAiConfig({ provider: "claude" })); const isAiConfigLoaded = ref(false); + const desktopSettings = ref({ ...DEFAULT_DESKTOP_SETTINGS }); + const isDesktopSettingsLoaded = ref(false); const editorSettings = ref(loadEditorSettings()); + async function initDesktopSettings() { + if (isDesktopSettingsLoaded.value) return; + desktopSettings.value = await api.loadDesktopSettings().catch(() => ({ ...DEFAULT_DESKTOP_SETTINGS })); + isDesktopSettingsLoaded.value = true; + } + + async function updateDesktopSettings(partial: Partial) { + const previous = desktopSettings.value; + const next = { + ...desktopSettings.value, + ...partial, + }; + desktopSettings.value = next; + try { + await api.saveDesktopSettings(next); + } catch (error) { + desktopSettings.value = previous; + throw error; + } + } + async function initAiConfig() { if (isAiConfigLoaded.value) return; const legacy = localStorage.getItem("dbx-ai-config"); @@ -397,7 +428,10 @@ export const useSettingsStore = defineStore("settings", () => { updateAiConfig, isConfigured, editorSettings, + desktopSettings, updateEditorSettings, + initDesktopSettings, + updateDesktopSettings, updateColumnFormatter, upsertCustomColumnFormatter, deleteCustomColumnFormatter, diff --git a/crates/dbx-core/src/storage.rs b/crates/dbx-core/src/storage.rs index 98ee04464e..303e5854e6 100644 --- a/crates/dbx-core/src/storage.rs +++ b/crates/dbx-core/src/storage.rs @@ -1,6 +1,7 @@ use std::path::Path; use std::str::FromStr; +use serde::{Deserialize, Serialize}; use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; use crate::ai::{AiChatMessage, AiConfig, AiConversation}; @@ -12,6 +13,17 @@ pub struct Storage { db: SqlitePool, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DesktopSettings { + pub run_in_background: bool, +} + +impl Default for DesktopSettings { + fn default() -> Self { + Self { run_in_background: true } + } +} + const SCHEMA_STATEMENTS: &[&str] = &[ "CREATE TABLE IF NOT EXISTS connections ( id TEXT PRIMARY KEY, @@ -274,8 +286,25 @@ impl Storage { // --------------------------------------------------------------------------- impl Storage { - pub async fn save_password_hash(&self, hash: &str) -> Result<(), String> { - let json = serde_json::json!({ "password_hash": hash }).to_string(); + async fn load_app_settings_json(&self) -> Result, String> { + let row: Option<(String,)> = sqlx::query_as("SELECT settings_json FROM app_settings WHERE id = 1") + .fetch_optional(&self.db) + .await + .map_err(|e| e.to_string())?; + let Some((json,)) = row else { + return Ok(serde_json::Map::new()); + }; + match serde_json::from_str::(&json).map_err(|e| e.to_string())? { + serde_json::Value::Object(map) => Ok(map), + _ => Ok(serde_json::Map::new()), + } + } + + async fn save_app_settings_json( + &self, + settings: &serde_json::Map, + ) -> Result<(), String> { + let json = serde_json::Value::Object(settings.clone()).to_string(); sqlx::query("INSERT OR REPLACE INTO app_settings (id, settings_json) VALUES (1, ?)") .bind(&json) .execute(&self.db) @@ -284,18 +313,31 @@ impl Storage { Ok(()) } + pub async fn save_password_hash(&self, hash: &str) -> Result<(), String> { + let mut settings = self.load_app_settings_json().await?; + settings.insert("password_hash".to_string(), serde_json::Value::String(hash.to_string())); + self.save_app_settings_json(&settings).await + } + pub async fn load_password_hash(&self) -> Result, String> { - let row: Option<(String,)> = sqlx::query_as("SELECT settings_json FROM app_settings WHERE id = 1") - .fetch_optional(&self.db) - .await - .map_err(|e| e.to_string())?; - match row { - Some((json,)) => { - let v: serde_json::Value = serde_json::from_str(&json).map_err(|e| e.to_string())?; - Ok(v.get("password_hash").and_then(|v| v.as_str()).map(|s| s.to_string())) - } - None => Ok(None), - } + let settings = self.load_app_settings_json().await?; + Ok(settings.get("password_hash").and_then(|v| v.as_str()).map(|s| s.to_string())) + } + + pub async fn save_desktop_settings(&self, desktop_settings: &DesktopSettings) -> Result<(), String> { + let mut settings = self.load_app_settings_json().await?; + settings.insert("run_in_background".to_string(), serde_json::Value::Bool(desktop_settings.run_in_background)); + self.save_app_settings_json(&settings).await + } + + pub async fn load_desktop_settings(&self) -> Result { + let settings = self.load_app_settings_json().await?; + Ok(DesktopSettings { + run_in_background: settings + .get("run_in_background") + .and_then(|value| value.as_bool()) + .unwrap_or_else(|| DesktopSettings::default().run_in_background), + }) } } @@ -930,3 +972,46 @@ async fn persist_secret_in_tx( } Ok(()) } + +#[cfg(test)] +mod tests { + use super::{DesktopSettings, Storage}; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_db_path(name: &str) -> std::path::PathBuf { + let stamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); + std::env::temp_dir().join(format!("dbx-storage-{name}-{}-{stamp}.db", std::process::id())) + } + + #[tokio::test] + async fn desktop_settings_default_to_background_enabled() { + let path = temp_db_path("desktop-settings-default"); + let storage = Storage::open(&path).await.unwrap(); + + assert_eq!(storage.load_desktop_settings().await.unwrap(), DesktopSettings { run_in_background: true }); + } + + #[tokio::test] + async fn desktop_settings_preserve_existing_password_hash() { + let path = temp_db_path("desktop-settings-preserve-password"); + let storage = Storage::open(&path).await.unwrap(); + + storage.save_password_hash("hash-1").await.unwrap(); + storage.save_desktop_settings(&DesktopSettings { run_in_background: false }).await.unwrap(); + + assert_eq!(storage.load_password_hash().await.unwrap(), Some("hash-1".to_string())); + assert_eq!(storage.load_desktop_settings().await.unwrap(), DesktopSettings { run_in_background: false }); + } + + #[tokio::test] + async fn password_hash_preserves_existing_desktop_settings() { + let path = temp_db_path("password-preserve-desktop-settings"); + let storage = Storage::open(&path).await.unwrap(); + + storage.save_desktop_settings(&DesktopSettings { run_in_background: false }).await.unwrap(); + storage.save_password_hash("hash-2").await.unwrap(); + + assert_eq!(storage.load_password_hash().await.unwrap(), Some("hash-2".to_string())); + assert_eq!(storage.load_desktop_settings().await.unwrap(), DesktopSettings { run_in_background: false }); + } +} diff --git a/src-tauri/src/commands/app_settings.rs b/src-tauri/src/commands/app_settings.rs new file mode 100644 index 0000000000..d74d79a81b --- /dev/null +++ b/src-tauri/src/commands/app_settings.rs @@ -0,0 +1,25 @@ +use std::sync::Arc; + +use dbx_core::storage::DesktopSettings; +use tauri::{AppHandle, Manager, State}; + +use super::connection::AppState; +use crate::{apply_desktop_tray_preference, WindowBehaviorState}; + +#[tauri::command] +pub async fn load_desktop_settings(state: State<'_, Arc>) -> Result { + state.storage.load_desktop_settings().await +} + +#[tauri::command] +pub async fn save_desktop_settings( + app: AppHandle, + state: State<'_, Arc>, + settings: DesktopSettings, +) -> Result<(), String> { + state.storage.save_desktop_settings(&settings).await?; + if let Some(window_behavior) = app.try_state::() { + window_behavior.set_run_in_background(settings.run_in_background); + } + apply_desktop_tray_preference(&app, settings.run_in_background).map_err(|err| err.to_string()) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 7a5dc86b56..8d15dce38b 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,5 +1,6 @@ pub mod agents; pub mod ai; +pub mod app_settings; pub mod connection; #[allow(dead_code, unused_imports)] mod connection_secrets; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 845d2155cc..f0d99718da 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,8 +5,11 @@ mod models; mod window_state_guard; use commands::connection::AppState; -use dbx_core::storage::Storage; -use std::sync::Arc; +use dbx_core::storage::{DesktopSettings, Storage}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; use std::time::Instant; use tauri::{ menu::MenuBuilder, @@ -16,19 +19,39 @@ use tauri::{Emitter, Manager, RunEvent}; #[cfg(any(windows, target_os = "linux"))] use tauri_plugin_deep_link::DeepLinkExt; -fn should_hide_window_on_close(target_os: &str) -> bool { - matches!(target_os, "macos" | "windows") +const DESKTOP_TRAY_ID: &str = "main-tray"; + +pub(crate) struct WindowBehaviorState { + run_in_background: AtomicBool, } -fn should_setup_desktop_tray(target_os: &str) -> bool { - matches!(target_os, "macos" | "windows") +impl WindowBehaviorState { + fn new(settings: &DesktopSettings) -> Self { + Self { run_in_background: AtomicBool::new(settings.run_in_background) } + } + + pub(crate) fn run_in_background(&self) -> bool { + self.run_in_background.load(Ordering::Relaxed) + } + + pub(crate) fn set_run_in_background(&self, value: bool) { + self.run_in_background.store(value, Ordering::Relaxed); + } +} + +fn should_hide_window_on_close(target_os: &str, run_in_background: bool) -> bool { + run_in_background && matches!(target_os, "macos" | "windows") +} + +fn should_setup_desktop_tray(target_os: &str, run_in_background: bool) -> bool { + run_in_background && matches!(target_os, "macos" | "windows") } fn should_show_main_window_after_setup() -> bool { true } -fn show_main_window(app: &tauri::AppHandle) { +fn show_main_window(app: &tauri::AppHandle) { if let Some(window) = app.get_webview_window("main") { let _ = window.show(); let _ = window.unminimize(); @@ -48,11 +71,12 @@ fn open_connection_deep_links(app: &tauri::AppHandle, links: Vec) { } #[cfg_attr(not(any(target_os = "macos", target_os = "windows")), allow(dead_code))] -fn setup_desktop_tray(app: &mut tauri::App) -> tauri::Result<()> { - let menu = MenuBuilder::new(app).text("show", "Show DBX").separator().text("quit", "Quit DBX").build()?; - let mut tray = TrayIconBuilder::with_id("main-tray").tooltip("DBX").menu(&menu).show_menu_on_left_click(false); +fn setup_desktop_tray>(manager: &M) -> tauri::Result<()> { + let menu = MenuBuilder::new(manager).text("show", "Show DBX").separator().text("quit", "Quit DBX").build()?; + let mut tray = + TrayIconBuilder::::with_id(DESKTOP_TRAY_ID).tooltip("DBX").menu(&menu).show_menu_on_left_click(false); - if let Some(icon) = app.default_window_icon().cloned() { + if let Some(icon) = manager.app_handle().default_window_icon().cloned() { tray = tray.icon(icon); } @@ -68,34 +92,49 @@ fn setup_desktop_tray(app: &mut tauri::App) -> tauri::Result<()> { | TrayIconEvent::DoubleClick { button: MouseButton::Left, .. } => show_main_window(tray.app_handle()), _ => {} }) - .build(app)?; + .build(manager)?; Ok(()) } +pub(crate) fn apply_desktop_tray_preference(app: &tauri::AppHandle, run_in_background: bool) -> tauri::Result<()> { + if should_setup_desktop_tray(std::env::consts::OS, run_in_background) { + if app.tray_by_id(DESKTOP_TRAY_ID).is_none() { + setup_desktop_tray(app)?; + } + } else { + let _ = app.remove_tray_by_id(DESKTOP_TRAY_ID); + } + Ok(()) +} + #[cfg(test)] mod tests { use super::{should_hide_window_on_close, should_setup_desktop_tray, should_show_main_window_after_setup}; #[test] fn hides_window_on_close_for_windows_and_macos() { - assert!(should_hide_window_on_close("windows")); - assert!(should_hide_window_on_close("macos")); + assert!(should_hide_window_on_close("windows", true)); + assert!(should_hide_window_on_close("macos", true)); + assert!(!should_hide_window_on_close("windows", false)); + assert!(!should_hide_window_on_close("macos", false)); } #[test] fn does_not_hide_window_on_close_for_other_platforms() { - assert!(!should_hide_window_on_close("linux")); + assert!(!should_hide_window_on_close("linux", true)); } #[test] fn sets_up_desktop_tray_for_windows_and_macos() { - assert!(should_setup_desktop_tray("windows")); - assert!(should_setup_desktop_tray("macos")); - assert!(!should_setup_desktop_tray("linux")); + assert!(should_setup_desktop_tray("windows", true)); + assert!(should_setup_desktop_tray("macos", true)); + assert!(!should_setup_desktop_tray("windows", false)); + assert!(!should_setup_desktop_tray("macos", false)); + assert!(!should_setup_desktop_tray("linux", true)); let source = include_str!("lib.rs"); assert!(source.contains( - "if should_setup_desktop_tray(std::env::consts::OS) {\n setup_desktop_tray(app)?;" + "if should_setup_desktop_tray(std::env::consts::OS, desktop_settings.run_in_background) {\n setup_desktop_tray(app)?;" )); } @@ -158,6 +197,7 @@ pub fn run() { eprintln!("[STARTUP] migrate_from_json in {:?}", t2.elapsed()); s }); + let desktop_settings = tauri::async_runtime::block_on(storage.load_desktop_settings()).unwrap_or_default(); eprintln!("[STARTUP] storage ready in {:?}", t.elapsed()); let state = Arc::new(AppState::new_with_plugin_dir_and_app_version( @@ -166,6 +206,7 @@ pub fn run() { env!("CARGO_PKG_VERSION"), )); app.manage(state.clone()); + app.manage(WindowBehaviorState::new(&desktop_settings)); app.manage(commands::external_sql::ExternalSqlOpenState::default()); app.manage(commands::deep_link::DeepLinkOpenState::default()); let startup_links = commands::deep_link::connection_deep_links_from_args(std::env::args().skip(1)); @@ -181,7 +222,7 @@ pub fn run() { let _ = window.set_decorations(false); } } - if should_setup_desktop_tray(std::env::consts::OS) { + if should_setup_desktop_tray(std::env::consts::OS, desktop_settings.run_in_background) { setup_desktop_tray(app)?; } window_state_guard::enforce_main_window_bounds(app.handle()); @@ -195,9 +236,16 @@ pub fn run() { }) .on_window_event(|window, event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { - if should_hide_window_on_close(std::env::consts::OS) { + let run_in_background = window + .app_handle() + .try_state::() + .map(|state| state.run_in_background()) + .unwrap_or_else(|| DesktopSettings::default().run_in_background); + if should_hide_window_on_close(std::env::consts::OS, run_in_background) { let _ = window.hide(); api.prevent_close(); + } else { + window.app_handle().exit(0); } } }) @@ -212,6 +260,8 @@ pub fn run() { commands::ai::save_ai_conversation, commands::ai::load_ai_conversations, commands::ai::delete_ai_conversation, + commands::app_settings::load_desktop_settings, + commands::app_settings::save_desktop_settings, commands::connection::test_connection, commands::connection::connect_db, commands::connection::disconnect_db,