From 5d2d15690ce7f89123a92535ec945ee7d940a780 Mon Sep 17 00:00:00 2001 From: ShaSan Date: Fri, 26 Sep 2025 20:18:11 +0800 Subject: [PATCH] feat: support minimizing window to tray on close (#41) fix: grant set_skip_taskbar permission through default capability chore: align settings casing and defaults between Rust and frontend Co-authored-by: Jason --- src-tauri/capabilities/default.json | 1 + src-tauri/src/commands.rs | 17 ++++++---- src-tauri/src/lib.rs | 40 +++++++++++++++------- src-tauri/src/settings.rs | 19 ++++++----- src-tauri/src/vscode.rs | 14 +++++--- src/components/SettingsModal.tsx | 51 +++++++++++++++++++---------- src/lib/tauri-api.ts | 2 +- src/types.ts | 2 ++ 8 files changed, 95 insertions(+), 51 deletions(-) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 80de359..37ad55b 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -9,6 +9,7 @@ "core:default", "opener:default", "updater:default", + "core:window:allow-set-skip-taskbar", "process:allow-restart", "dialog:default" ] diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 6512371..631fb6f 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -2,8 +2,8 @@ use std::collections::HashMap; use tauri::State; -use tauri_plugin_opener::OpenerExt; use tauri_plugin_dialog::DialogExt; +use tauri_plugin_opener::OpenerExt; use crate::app_config::AppType; use crate::codex_config; @@ -655,9 +655,8 @@ pub async fn open_app_config_folder(handle: tauri::AppHandle) -> Result Result { - serde_json::to_value(crate::settings::get_settings()) - .map_err(|e| format!("序列化设置失败: {}", e)) +pub async fn get_settings() -> Result { + Ok(crate::settings::get_settings()) } /// 保存设置 @@ -697,11 +696,17 @@ pub async fn is_portable_mode() -> Result { #[tauri::command] pub async fn get_vscode_settings_status() -> Result { if let Some(p) = vscode::find_existing_settings() { - Ok(ConfigStatus { exists: true, path: p.to_string_lossy().to_string() }) + Ok(ConfigStatus { + exists: true, + path: p.to_string_lossy().to_string(), + }) } else { // 默认返回 macOS 稳定版路径(或其他平台首选项的第一个候选),但标记不存在 let preferred = vscode::candidate_settings_paths().into_iter().next(); - Ok(ConfigStatus { exists: false, path: preferred.unwrap_or_default().to_string_lossy().to_string() }) + Ok(ConfigStatus { + exists: false, + path: preferred.unwrap_or_default().to_string_lossy().to_string(), + }) } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 250a4da..e58ca2c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -123,6 +123,10 @@ fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) { match event_id { "show_main" => { if let Some(window) = app.get_webview_window("main") { + #[cfg(target_os = "windows")] + { + let _ = window.set_skip_taskbar(false); + } let _ = window.unminimize(); let _ = window.show(); let _ = window.set_focus(); @@ -241,23 +245,31 @@ pub fn run() { #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] { - builder = builder.plugin(tauri_plugin_single_instance::init( - |app, _args, _cwd| { - if let Some(window) = app.get_webview_window("main") { - let _ = window.unminimize(); - let _ = window.show(); - let _ = window.set_focus(); - } - }, - )); + builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { + if let Some(window) = app.get_webview_window("main") { + let _ = window.unminimize(); + let _ = window.show(); + let _ = window.set_focus(); + } + })); } let builder = builder - // 拦截窗口关闭:仅隐藏窗口,保持进程与托盘常驻 + // 拦截窗口关闭:根据设置决定是否最小化到托盘 .on_window_event(|window, event| match event { tauri::WindowEvent::CloseRequested { api, .. } => { - api.prevent_close(); - let _ = window.hide(); + let settings = crate::settings::get_settings(); + + if settings.minimize_to_tray_on_close { + api.prevent_close(); + let _ = window.hide(); + #[cfg(target_os = "windows")] + { + let _ = window.set_skip_taskbar(true); + } + } else { + window.app_handle().exit(0); + } } _ => {} }) @@ -394,6 +406,10 @@ pub fn run() { match event { RunEvent::Reopen { .. } => { if let Some(window) = app_handle.get_webview_window("main") { + #[cfg(target_os = "windows")] + { + let _ = window.set_skip_taskbar(false); + } let _ = window.unminimize(); let _ = window.show(); let _ = window.set_focus(); diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 8a47f55..3fbe938 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -9,6 +9,8 @@ use std::sync::{OnceLock, RwLock}; pub struct AppSettings { #[serde(default = "default_show_in_tray")] pub show_in_tray: bool, + #[serde(default = "default_minimize_to_tray_on_close")] + pub minimize_to_tray_on_close: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub claude_config_dir: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -19,10 +21,15 @@ fn default_show_in_tray() -> bool { true } +fn default_minimize_to_tray_on_close() -> bool { + true +} + impl Default for AppSettings { fn default() -> Self { Self { show_in_tray: true, + minimize_to_tray_on_close: true, claude_config_dir: None, codex_config_dir: None, } @@ -78,8 +85,7 @@ impl AppSettings { let path = Self::settings_path(); if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .map_err(|e| format!("创建设置目录失败: {}", e))?; + fs::create_dir_all(parent).map_err(|e| format!("创建设置目录失败: {}", e))?; } let json = serde_json::to_string_pretty(&normalized) @@ -113,19 +119,14 @@ fn resolve_override_path(raw: &str) -> PathBuf { } pub fn get_settings() -> AppSettings { - settings_store() - .read() - .expect("读取设置锁失败") - .clone() + settings_store().read().expect("读取设置锁失败").clone() } pub fn update_settings(mut new_settings: AppSettings) -> Result<(), String> { new_settings.normalize_paths(); new_settings.save()?; - let mut guard = settings_store() - .write() - .expect("写入设置锁失败"); + let mut guard = settings_store().write().expect("写入设置锁失败"); *guard = new_settings; Ok(()) } diff --git a/src-tauri/src/vscode.rs b/src-tauri/src/vscode.rs index be50924..dbcfdf2 100644 --- a/src-tauri/src/vscode.rs +++ b/src-tauri/src/vscode.rs @@ -1,12 +1,12 @@ -use std::path::{PathBuf}; +use std::path::PathBuf; /// 枚举可能的 VS Code 发行版配置目录名称 fn vscode_product_dirs() -> Vec<&'static str> { vec![ - "Code", // VS Code Stable + "Code", // VS Code Stable "Code - Insiders", // VS Code Insiders - "VSCodium", // VSCodium - "Code - OSS", // OSS 发行版 + "VSCodium", // VSCodium + "Code - OSS", // OSS 发行版 ] } @@ -19,7 +19,11 @@ pub fn candidate_settings_paths() -> Vec { if let Some(home) = dirs::home_dir() { for prod in vscode_product_dirs() { paths.push( - home.join("Library").join("Application Support").join(prod).join("User").join("settings.json") + home.join("Library") + .join("Application Support") + .join(prod) + .join("User") + .join("settings.json"), ); } } diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 1e00d84..1528be7 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -25,6 +25,7 @@ interface SettingsModalProps { export default function SettingsModal({ onClose }: SettingsModalProps) { const [settings, setSettings] = useState({ showInTray: true, + minimizeToTrayOnClose: true, claudeConfigDir: undefined, codexConfigDir: undefined, }); @@ -65,8 +66,13 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { (loadedSettings as any)?.showInTray ?? (loadedSettings as any)?.showInDock ?? true; + const minimizeToTrayOnClose = + (loadedSettings as any)?.minimizeToTrayOnClose ?? + (loadedSettings as any)?.minimize_to_tray_on_close ?? + true; setSettings({ showInTray, + minimizeToTrayOnClose, claudeConfigDir: typeof (loadedSettings as any)?.claudeConfigDir === "string" ? (loadedSettings as any).claudeConfigDir @@ -305,26 +311,35 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { {/* 设置内容 */}
- {/* 系统托盘设置(未实现) - 说明:此开关用于控制是否在系统托盘/菜单栏显示应用图标。 */} - {/*
+ {/* 窗口行为设置 */} +

- 显示设置(系统托盘) + 窗口行为

- -
*/} +
+ +
+
{/* VS Code 自动同步设置已移除 */} diff --git a/src/lib/tauri-api.ts b/src/lib/tauri-api.ts index 29ca442..ac9b6de 100644 --- a/src/lib/tauri-api.ts +++ b/src/lib/tauri-api.ts @@ -223,7 +223,7 @@ export const tauriAPI = { return await invoke("get_settings"); } catch (error) { console.error("获取设置失败:", error); - return { showInTray: true }; + return { showInTray: true, minimizeToTrayOnClose: true }; } }, diff --git a/src/types.ts b/src/types.ts index c3266fb..90f5a6c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,6 +24,8 @@ export interface AppConfig { export interface Settings { // 是否在系统托盘(macOS 菜单栏)显示图标 showInTray: boolean; + // 点击关闭按钮时是否最小化到托盘而不是关闭应用 + minimizeToTrayOnClose: boolean; // 覆盖 Claude Code 配置目录(可选) claudeConfigDir?: string; // 覆盖 Codex 配置目录(可选)