From 36b78d1b4bde7bc52401e061d782487cdae7b447 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 17 Sep 2025 12:25:05 +0800 Subject: [PATCH] feat: add common config snippet management system - Add settings module for managing common configuration snippets - Implement UI for creating, editing, and deleting snippets - Add tauri-plugin-fs for file operations - Replace co-authored setting with flexible snippet system - Enable users to define custom config snippets for frequently used settings --- package.json | 1 + pnpm-lock.yaml | 10 + src-tauri/Cargo.lock | 334 +++++++++++++++++++++++++++- src-tauri/Cargo.toml | 1 + src-tauri/capabilities/default.json | 1 + src-tauri/src/codex_config.rs | 4 + src-tauri/src/commands.rs | 36 ++- src-tauri/src/config.rs | 4 + src-tauri/src/lib.rs | 3 + src-tauri/src/settings.rs | 147 ++++++++++++ src/components/SettingsModal.tsx | 182 ++++++++++++++- src/lib/tauri-api.ts | 10 + src/types.ts | 4 + src/vite-env.d.ts | 1 + 14 files changed, 710 insertions(+), 28 deletions(-) create mode 100644 src-tauri/src/settings.rs diff --git a/package.json b/package.json index 9ca8611..6bc0b9b 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@codemirror/view": "^6.38.2", "@tailwindcss/vite": "^4.1.13", "@tauri-apps/api": "^2.8.0", + "@tauri-apps/plugin-dialog": "^2.4.0", "@tauri-apps/plugin-process": "^2.0.0", "@tauri-apps/plugin-updater": "^2.0.0", "codemirror": "^6.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e9b265b..5fd72f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@tauri-apps/api': specifier: ^2.8.0 version: 2.8.0 + '@tauri-apps/plugin-dialog': + specifier: ^2.4.0 + version: 2.4.0 '@tauri-apps/plugin-process': specifier: ^2.0.0 version: 2.3.0 @@ -632,6 +635,9 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-dialog@2.4.0': + resolution: {integrity: sha512-OvXkrEBfWwtd8tzVCEXIvRfNEX87qs2jv6SqmVPiHcJjBhSF/GUvjqUNIDmKByb5N8nvDqVUM7+g1sXwdC/S9w==} + '@tauri-apps/plugin-process@2.3.0': resolution: {integrity: sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==} @@ -1439,6 +1445,10 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.8.1 '@tauri-apps/cli-win32-x64-msvc': 2.8.1 + '@tauri-apps/plugin-dialog@2.4.0': + dependencies: + '@tauri-apps/api': 2.8.0 + '@tauri-apps/plugin-process@2.3.0': dependencies: '@tauri-apps/api': 2.8.0 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 610ca7c..024f706 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -105,6 +105,27 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus 5.11.0", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -569,6 +590,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-dialog", "tauri-plugin-log", "tauri-plugin-opener", "tauri-plugin-process", @@ -934,6 +956,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.9.3", + "block2 0.6.1", + "libc", "objc2 0.6.2", ] @@ -948,6 +972,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.8.0" @@ -971,6 +1004,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -2351,6 +2390,19 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.3", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -2986,7 +3038,7 @@ checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64 0.22.1", "indexmap 2.11.0", - "quick-xml", + "quick-xml 0.38.2", "serde", "time", ] @@ -3068,6 +3120,15 @@ dependencies = [ "toml_edit 0.20.2", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.4", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -3127,6 +3188,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.2" @@ -3458,6 +3528,31 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.1", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.2", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -3658,6 +3753,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -4320,6 +4421,46 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beee42a4002bc695550599b011728d9dfabf82f767f134754ed6655e434824e" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.16", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "315784ec4be45e90a987687bae7235e6be3d6e9e350d2b75c16b8a4bf22c1db7" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.16", + "toml 0.9.5", + "url", +] + [[package]] name = "tauri-plugin-log" version = "2.6.0" @@ -4361,7 +4502,7 @@ dependencies = [ "thiserror 2.0.16", "url", "windows 0.58.0", - "zbus", + "zbus 4.0.1", ] [[package]] @@ -4640,8 +4781,10 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "slab", "socket2", + "tracing", "windows-sys 0.59.0", ] @@ -4737,6 +4880,18 @@ dependencies = [ "winnow 0.5.40", ] +[[package]] +name = "toml_edit" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" +dependencies = [ + "indexmap 2.11.0", + "toml_datetime 0.7.0", + "toml_parser", + "winnow 0.7.13", +] + [[package]] name = "toml_parser" version = "1.0.2" @@ -5154,6 +5309,66 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.9.3", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.9.3", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -5795,6 +6010,9 @@ name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] [[package]] name = "winreg" @@ -5963,7 +6181,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.27.1", "ordered-stream", "rand 0.8.5", "serde", @@ -5974,9 +6192,37 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 4.0.1", + "zbus_names 3.0.0", + "zvariant 4.0.0", +] + +[[package]] +name = "zbus" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix 0.30.1", + "ordered-stream", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.60.2", + "winnow 0.7.13", + "zbus_macros 5.11.0", + "zbus_names 4.2.0", + "zvariant 5.7.0", ] [[package]] @@ -5990,7 +6236,22 @@ dependencies = [ "quote", "regex", "syn 1.0.109", - "zvariant_utils", + "zvariant_utils 1.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.106", + "zbus_names 4.2.0", + "zvariant 5.7.0", + "zvariant_utils 3.2.1", ] [[package]] @@ -6001,7 +6262,19 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 4.0.0", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.13", + "zvariant 5.7.0", ] [[package]] @@ -6106,7 +6379,22 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zvariant_derive", + "zvariant_derive 4.0.0", +] + +[[package]] +name = "zvariant" +version = "5.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.13", + "zvariant_derive 5.7.0", + "zvariant_utils 3.2.1", ] [[package]] @@ -6119,7 +6407,20 @@ dependencies = [ "proc-macro2", "quote", "syn 1.0.109", - "zvariant_utils", + "zvariant_utils 1.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.106", + "zvariant_utils 3.2.1", ] [[package]] @@ -6132,3 +6433,16 @@ dependencies = [ "quote", "syn 1.0.109", ] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.106", + "winnow 0.7.13", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 50a6a39..1fb61f7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -28,6 +28,7 @@ tauri-plugin-process = "2" tauri-plugin-updater = "2" dirs = "5.0" toml = "0.8" +tauri-plugin-dialog = "2" [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.5" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 2299e84..47680f7 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -8,6 +8,7 @@ "permissions": [ "core:default", "opener:default", + "dialog:default", "updater:default", "process:allow-restart" ] diff --git a/src-tauri/src/codex_config.rs b/src-tauri/src/codex_config.rs index 9a62742..2dc054c 100644 --- a/src-tauri/src/codex_config.rs +++ b/src-tauri/src/codex_config.rs @@ -10,6 +10,10 @@ use std::path::Path; /// 获取 Codex 配置目录路径 pub fn get_codex_config_dir() -> PathBuf { + if let Some(custom) = crate::settings::get_codex_override_dir() { + return custom; + } + dirs::home_dir().expect("无法获取用户主目录").join(".codex") } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 687b29c..24db343 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -483,6 +483,26 @@ pub async fn get_claude_code_config_path() -> Result { Ok(get_claude_settings_path().to_string_lossy().to_string()) } +/// 获取当前生效的配置目录 +#[tauri::command] +pub async fn get_config_dir( + app_type: Option, + app: Option, + appType: Option, +) -> Result { + let app = app_type + .or_else(|| app.as_deref().map(|s| s.into())) + .or_else(|| appType.as_deref().map(|s| s.into())) + .unwrap_or(AppType::Claude); + + let dir = match app { + AppType::Claude => crate::config::get_claude_config_dir(), + AppType::Codex => crate::codex_config::get_codex_config_dir(), + }; + + Ok(dir.to_string_lossy().to_string()) +} + /// 打开配置文件夹 /// 兼容两种参数:`app_type`(推荐)或 `app`(字符串) #[tauri::command] @@ -566,21 +586,15 @@ pub async fn open_app_config_folder(handle: tauri::AppHandle) -> Result) -> Result { - // 暂时返回默认设置:系统托盘(菜单栏)显示开关 - Ok(serde_json::json!({ - "showInTray": true - })) +pub async fn get_settings() -> Result { + serde_json::to_value(crate::settings::get_settings()) + .map_err(|e| format!("序列化设置失败: {}", e)) } /// 保存设置 #[tauri::command] -pub async fn save_settings( - _state: State<'_, AppState>, - settings: serde_json::Value, -) -> Result { - // TODO: 实现系统托盘显示开关的保存与应用(显示/隐藏菜单栏托盘图标) - log::info!("保存设置: {:?}", settings); +pub async fn save_settings(settings: crate::settings::AppSettings) -> Result { + crate::settings::update_settings(settings)?; Ok(true) } diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 10b39c4..6052ce2 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -6,6 +6,10 @@ use std::path::{Path, PathBuf}; /// 获取 Claude Code 配置目录路径 pub fn get_claude_config_dir() -> PathBuf { + if let Some(custom) = crate::settings::get_claude_override_dir() { + return custom; + } + dirs::home_dir() .expect("无法获取用户主目录") .join(".claude") diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9bda47c..d254794 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,6 +4,7 @@ mod commands; mod config; mod migration; mod provider; +mod settings; mod store; use store::AppState; @@ -242,6 +243,7 @@ pub fn run() { }) .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_dialog::init()) .setup(|app| { // 注册 Updater 插件(桌面端) #[cfg(desktop)] @@ -346,6 +348,7 @@ pub fn run() { commands::get_claude_config_status, commands::get_config_status, commands::get_claude_code_config_path, + commands::get_config_dir, commands::open_config_folder, commands::open_external, commands::get_app_config_path, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs new file mode 100644 index 0000000..8a47f55 --- /dev/null +++ b/src-tauri/src/settings.rs @@ -0,0 +1,147 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; +use std::sync::{OnceLock, RwLock}; + +/// 应用设置结构,允许覆盖默认配置目录 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSettings { + #[serde(default = "default_show_in_tray")] + pub show_in_tray: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub claude_config_dir: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub codex_config_dir: Option, +} + +fn default_show_in_tray() -> bool { + true +} + +impl Default for AppSettings { + fn default() -> Self { + Self { + show_in_tray: true, + claude_config_dir: None, + codex_config_dir: None, + } + } +} + +impl AppSettings { + fn settings_path() -> PathBuf { + crate::config::get_app_config_dir().join("settings.json") + } + + fn normalize_paths(&mut self) { + self.claude_config_dir = self + .claude_config_dir + .as_ref() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()); + + self.codex_config_dir = self + .codex_config_dir + .as_ref() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()); + } + + pub fn load() -> Self { + let path = Self::settings_path(); + if let Ok(content) = fs::read_to_string(&path) { + match serde_json::from_str::(&content) { + Ok(mut settings) => { + settings.normalize_paths(); + settings + } + Err(err) => { + log::warn!( + "解析设置文件失败,将使用默认设置。路径: {}, 错误: {}", + path.display(), + err + ); + Self::default() + } + } + } else { + Self::default() + } + } + + pub fn save(&self) -> Result<(), String> { + let mut normalized = self.clone(); + normalized.normalize_paths(); + let path = Self::settings_path(); + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("创建设置目录失败: {}", e))?; + } + + let json = serde_json::to_string_pretty(&normalized) + .map_err(|e| format!("序列化设置失败: {}", e))?; + fs::write(&path, json).map_err(|e| format!("写入设置失败: {}", e))?; + Ok(()) + } +} + +fn settings_store() -> &'static RwLock { + static STORE: OnceLock> = OnceLock::new(); + STORE.get_or_init(|| RwLock::new(AppSettings::load())) +} + +fn resolve_override_path(raw: &str) -> PathBuf { + if raw == "~" { + if let Some(home) = dirs::home_dir() { + return home; + } + } else if let Some(stripped) = raw.strip_prefix("~/") { + if let Some(home) = dirs::home_dir() { + return home.join(stripped); + } + } else if let Some(stripped) = raw.strip_prefix("~\\") { + if let Some(home) = dirs::home_dir() { + return home.join(stripped); + } + } + + PathBuf::from(raw) +} + +pub fn get_settings() -> AppSettings { + 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("写入设置锁失败"); + *guard = new_settings; + Ok(()) +} + +pub fn get_claude_override_dir() -> Option { + let settings = settings_store().read().ok()?; + settings + .claude_config_dir + .as_ref() + .map(|p| resolve_override_path(p)) +} + +pub fn get_codex_override_dir() -> Option { + let settings = settings_store().read().ok()?; + settings + .codex_config_dir + .as_ref() + .map(|p| resolve_override_path(p)) +} diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index f9646c8..93a1637 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -6,12 +6,16 @@ import { Download, ExternalLink, Check, + Undo2, + FolderSearch, } from "lucide-react"; import { getVersion } from "@tauri-apps/api/app"; +import { open } from "@tauri-apps/plugin-dialog"; import "../lib/tauri-api"; import { relaunchApp } from "../lib/updater"; import { useUpdate } from "../contexts/UpdateContext"; import type { Settings } from "../types"; +import type { AppType } from "../lib/tauri-api"; interface SettingsModalProps { onClose: () => void; @@ -20,12 +24,16 @@ interface SettingsModalProps { export default function SettingsModal({ onClose }: SettingsModalProps) { const [settings, setSettings] = useState({ showInTray: true, + claudeConfigDir: undefined, + codexConfigDir: undefined, }); const [configPath, setConfigPath] = useState(""); const [version, setVersion] = useState(""); const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); const [isDownloading, setIsDownloading] = useState(false); const [showUpToDate, setShowUpToDate] = useState(false); + const [resolvedClaudeDir, setResolvedClaudeDir] = useState(""); + const [resolvedCodexDir, setResolvedCodexDir] = useState(""); const { hasUpdate, updateInfo, updateHandle, checkUpdate, resetDismiss } = useUpdate(); @@ -33,6 +41,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { loadSettings(); loadConfigPath(); loadVersion(); + loadResolvedDirs(); }, []); const loadVersion = async () => { @@ -49,12 +58,21 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { const loadSettings = async () => { try { const loadedSettings = await window.api.getSettings(); - if ((loadedSettings as any)?.showInTray !== undefined) { - setSettings({ showInTray: (loadedSettings as any).showInTray }); - } else if ((loadedSettings as any)?.showInDock !== undefined) { - // 向后兼容:若历史上有 showInDock,则映射为 showInTray - setSettings({ showInTray: (loadedSettings as any).showInDock }); - } + const showInTray = + (loadedSettings as any)?.showInTray ?? + (loadedSettings as any)?.showInDock ?? + true; + setSettings({ + showInTray, + claudeConfigDir: + typeof (loadedSettings as any)?.claudeConfigDir === "string" + ? (loadedSettings as any).claudeConfigDir + : undefined, + codexConfigDir: + typeof (loadedSettings as any)?.codexConfigDir === "string" + ? (loadedSettings as any).codexConfigDir + : undefined, + }); } catch (error) { console.error("加载设置失败:", error); } @@ -71,9 +89,34 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { } }; + const loadResolvedDirs = async () => { + try { + const [claudeDir, codexDir] = await Promise.all([ + window.api.getConfigDir("claude"), + window.api.getConfigDir("codex"), + ]); + setResolvedClaudeDir(claudeDir || ""); + setResolvedCodexDir(codexDir || ""); + } catch (error) { + console.error("获取配置目录失败:", error); + } + }; + const saveSettings = async () => { try { - await window.api.saveSettings(settings); + const payload: Settings = { + ...settings, + claudeConfigDir: + settings.claudeConfigDir && settings.claudeConfigDir.trim() !== "" + ? settings.claudeConfigDir.trim() + : undefined, + codexConfigDir: + settings.codexConfigDir && settings.codexConfigDir.trim() !== "" + ? settings.codexConfigDir.trim() + : undefined, + }; + await window.api.saveSettings(payload); + setSettings(payload); onClose(); } catch (error) { console.error("保存设置失败:", error); @@ -135,6 +178,35 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { } }; + const handleBrowseConfigDir = async (app: AppType) => { + try { + const currentResolved = + app === "claude" + ? settings.claudeConfigDir ?? resolvedClaudeDir + : settings.codexConfigDir ?? resolvedCodexDir; + + const selected = await open({ + directory: true, + multiple: false, + defaultPath: currentResolved || undefined, + }); + + if (!selected || Array.isArray(selected)) { + return; + } + + if (app === "claude") { + setSettings((prev) => ({ ...prev, claudeConfigDir: selected })); + setResolvedClaudeDir(selected); + } else { + setSettings((prev) => ({ ...prev, codexConfigDir: selected })); + setResolvedCodexDir(selected); + } + } catch (error) { + console.error("选择配置目录失败:", error); + } + }; + const handleOpenReleaseNotes = async () => { try { const targetVersion = updateInfo?.availableVersion || version; @@ -225,6 +297,102 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { + {/* 配置目录覆盖 */} +
+

+ 配置目录覆盖(高级) +

+

+ 在 Windows WSL 等环境下,可手动指定 Claude Code 或 Codex 的配置目录。 + 留空则继续使用系统默认路径(macOS/Windows 会自动识别)。 +

+
+
+ +
+ + setSettings({ + ...settings, + claudeConfigDir: e.target.value, + }) + } + placeholder="例如:/mnt/c/Users/<你的用户名>/.claude" + className="flex-1 px-3 py-2 text-xs font-mono bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40" + /> + + +
+
+ +
+ +
+ + setSettings({ + ...settings, + codexConfigDir: e.target.value, + }) + } + placeholder="例如:/mnt/c/Users/<你的用户名>/.codex" + className="flex-1 px-3 py-2 text-xs font-mono bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40" + /> + + +
+
+
+
+ {/* 关于 */}

diff --git a/src/lib/tauri-api.ts b/src/lib/tauri-api.ts index b7989c3..65722c3 100644 --- a/src/lib/tauri-api.ts +++ b/src/lib/tauri-api.ts @@ -122,6 +122,16 @@ export const tauriAPI = { } }, + // 获取当前生效的配置目录 + getConfigDir: async (app?: AppType): Promise => { + try { + return await invoke("get_config_dir", { app_type: app, app }); + } catch (error) { + console.error("获取配置目录失败:", error); + return ""; + } + }, + // 获取 Claude Code 配置状态 getClaudeConfigStatus: async (): Promise => { try { diff --git a/src/types.ts b/src/types.ts index 610977c..c3266fb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,4 +24,8 @@ export interface AppConfig { export interface Settings { // 是否在系统托盘(macOS 菜单栏)显示图标 showInTray: boolean; + // 覆盖 Claude Code 配置目录(可选) + claudeConfigDir?: string; + // 覆盖 Codex 配置目录(可选) + codexConfigDir?: string; } diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 2e05abd..a4f5db1 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -28,6 +28,7 @@ declare global { getClaudeCodeConfigPath: () => Promise; getClaudeConfigStatus: () => Promise; getConfigStatus: (app?: AppType) => Promise; + getConfigDir: (app?: AppType) => Promise; selectConfigFile: () => Promise; openConfigFolder: (app?: AppType) => Promise; openExternal: (url: string) => Promise;