feat: add config directory override support for WSL

- Add persistent app settings with custom Claude Code and Codex config directories
- Add config directory override UI in settings modal with manual input, browse, and reset options
- Integrate tauri-plugin-dialog for native directory picker
- Support WSL and other special environments where config paths need manual specification

Changes:
- settings.rs: Implement settings load/save and directory override logic
- SettingsModal: Add config directory override UI components
- API: Add get_config_dir and pick_directory commands
This commit is contained in:
Jason
2025-09-20 21:20:07 +08:00
parent b8d2daccde
commit 54f1357bcc
14 changed files with 789 additions and 39 deletions

View File

@@ -32,10 +32,11 @@
"@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",
"jsonc-parser": "^3.2.1",
"codemirror": "^6.0.2",
"jsonc-parser": "^3.2.1",
"lucide-react": "^0.542.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",

10
pnpm-lock.yaml generated
View File

@@ -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
@@ -635,6 +638,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==}
@@ -1445,6 +1451,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

334
src-tauri/Cargo.lock generated
View File

@@ -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",
]

View File

@@ -26,6 +26,7 @@ tauri-plugin-log = "2"
tauri-plugin-opener = "2"
tauri-plugin-process = "2"
tauri-plugin-updater = "2"
tauri-plugin-dialog = "2"
dirs = "5.0"
toml = "0.8"

View File

@@ -9,6 +9,7 @@
"core:default",
"opener:default",
"updater:default",
"process:allow-restart"
"process:allow-restart",
"dialog:default"
]
}

View File

@@ -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")
}

View File

@@ -3,14 +3,14 @@
use std::collections::HashMap;
use tauri::State;
use tauri_plugin_opener::OpenerExt;
use tauri_plugin_dialog::DialogExt;
use crate::app_config::AppType;
use crate::codex_config;
use crate::config::{get_claude_settings_path, ConfigStatus};
use crate::vscode;
use crate::config;
use crate::config::{self, get_claude_settings_path, ConfigStatus};
use crate::provider::Provider;
use crate::store::AppState;
use crate::vscode;
fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), String> {
match app_type {
@@ -520,6 +520,26 @@ pub async fn get_claude_code_config_path() -> Result<String, String> {
Ok(get_claude_settings_path().to_string_lossy().to_string())
}
/// 获取当前生效的配置目录
#[tauri::command]
pub async fn get_config_dir(
app_type: Option<AppType>,
app: Option<String>,
appType: Option<String>,
) -> Result<String, String> {
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 => config::get_claude_config_dir(),
AppType::Codex => codex_config::get_codex_config_dir(),
};
Ok(dir.to_string_lossy().to_string())
}
/// 打开配置文件夹
/// 兼容两种参数:`app_type`(推荐)或 `app`(字符串)
#[tauri::command]
@@ -553,6 +573,38 @@ pub async fn open_config_folder(
Ok(true)
}
/// 弹出系统目录选择器并返回用户选择的路径
#[tauri::command]
pub async fn pick_directory(
app: tauri::AppHandle,
default_path: Option<String>,
) -> Result<Option<String>, String> {
let initial = default_path
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty());
let result = tauri::async_runtime::spawn_blocking(move || {
let mut builder = app.dialog().file();
if let Some(path) = initial {
builder = builder.set_directory(path);
}
builder.blocking_pick_folder()
})
.await
.map_err(|e| format!("弹出目录选择器失败: {}", e))?;
match result {
Some(file_path) => {
let resolved = file_path
.simplified()
.into_path()
.map_err(|e| format!("解析选择的目录失败: {}", e))?;
Ok(Some(resolved.to_string_lossy().to_string()))
}
None => Ok(None),
}
}
/// 打开外部链接
#[tauri::command]
pub async fn open_external(app: tauri::AppHandle, url: String) -> Result<bool, String> {
@@ -603,21 +655,15 @@ pub async fn open_app_config_folder(handle: tauri::AppHandle) -> Result<bool, St
/// 获取设置
#[tauri::command]
pub async fn get_settings(_state: State<'_, AppState>) -> Result<serde_json::Value, String> {
// 暂时返回默认设置:系统托盘(菜单栏)显示开关
Ok(serde_json::json!({
"showInTray": true
}))
pub async fn get_settings() -> Result<serde_json::Value, String> {
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<bool, String> {
// TODO: 实现系统托盘显示开关的保存与应用(显示/隐藏菜单栏托盘图标)
log::info!("保存设置: {:?}", settings);
pub async fn save_settings(settings: crate::settings::AppSettings) -> Result<bool, String> {
crate::settings::update_settings(settings)?;
Ok(true)
}

View File

@@ -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")

View File

@@ -2,10 +2,11 @@ mod app_config;
mod codex_config;
mod commands;
mod config;
mod vscode;
mod migration;
mod provider;
mod settings;
mod store;
mod vscode;
use store::AppState;
#[cfg(target_os = "macos")]
@@ -246,6 +247,7 @@ pub fn run() {
_ => {}
})
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_opener::init())
.setup(|app| {
// 注册 Updater 插件(桌面端)
@@ -351,7 +353,9 @@ 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::pick_directory,
commands::open_external,
commands::get_app_config_path,
commands::open_app_config_folder,

147
src-tauri/src/settings.rs Normal file
View File

@@ -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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub codex_config_dir: Option<String>,
}
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::<AppSettings>(&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<AppSettings> {
static STORE: OnceLock<RwLock<AppSettings>> = 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<PathBuf> {
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<PathBuf> {
let settings = settings_store().read().ok()?;
settings
.codex_config_dir
.as_ref()
.map(|p| resolve_override_path(p))
}

View File

@@ -6,12 +6,16 @@ import {
Download,
ExternalLink,
Check,
Undo2,
FolderSearch,
} from "lucide-react";
import { getVersion } from "@tauri-apps/api/app";
import { homeDir, join } from "@tauri-apps/api/path";
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";
import { isLinux } from "../lib/platform";
interface SettingsModalProps {
@@ -21,12 +25,16 @@ interface SettingsModalProps {
export default function SettingsModal({ onClose }: SettingsModalProps) {
const [settings, setSettings] = useState<Settings>({
showInTray: true,
claudeConfigDir: undefined,
codexConfigDir: undefined,
});
const [configPath, setConfigPath] = useState<string>("");
const [version, setVersion] = useState<string>("");
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
const [isDownloading, setIsDownloading] = useState(false);
const [showUpToDate, setShowUpToDate] = useState(false);
const [resolvedClaudeDir, setResolvedClaudeDir] = useState<string>("");
const [resolvedCodexDir, setResolvedCodexDir] = useState<string>("");
const { hasUpdate, updateInfo, updateHandle, checkUpdate, resetDismiss } =
useUpdate();
@@ -34,6 +42,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
loadSettings();
loadConfigPath();
loadVersion();
loadResolvedDirs();
}, []);
const loadVersion = async () => {
@@ -50,12 +59,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);
}
@@ -72,9 +90,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);
@@ -136,6 +179,68 @@ 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 window.api.selectConfigDirectory(currentResolved);
if (!selected) {
return;
}
const sanitized = selected.trim();
if (sanitized === "") {
return;
}
if (app === "claude") {
setSettings((prev) => ({ ...prev, claudeConfigDir: sanitized }));
setResolvedClaudeDir(sanitized);
} else {
setSettings((prev) => ({ ...prev, codexConfigDir: sanitized }));
setResolvedCodexDir(sanitized);
}
} catch (error) {
console.error("选择配置目录失败:", error);
}
};
const computeDefaultConfigDir = async (app: AppType) => {
try {
const home = await homeDir();
const folder = app === "claude" ? ".claude" : ".codex";
return await join(home, folder);
} catch (error) {
console.error("获取默认配置目录失败:", error);
return "";
}
};
const handleResetConfigDir = async (app: AppType) => {
setSettings((prev) => ({
...prev,
...(app === "claude"
? { claudeConfigDir: undefined }
: { codexConfigDir: undefined }),
}));
const defaultDir = await computeDefaultConfigDir(app);
if (!defaultDir) {
return;
}
if (app === "claude") {
setResolvedClaudeDir(defaultDir);
} else {
setResolvedCodexDir(defaultDir);
}
};
const handleOpenReleaseNotes = async () => {
try {
const targetVersion = updateInfo?.availableVersion || version;
@@ -232,6 +337,92 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
</div>
</div>
{/* 配置目录覆盖 */}
<div>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3 leading-relaxed">
Windows WSL Claude Code Codex
使macOS/Windows
</p>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Claude Code
</label>
<div className="flex gap-2">
<input
type="text"
value={
settings.claudeConfigDir ?? resolvedClaudeDir ?? ""
}
onChange={(e) =>
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"
/>
<button
type="button"
onClick={() => handleBrowseConfigDir("claude")}
className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
title="浏览目录"
>
<FolderSearch size={16} />
</button>
<button
type="button"
onClick={() => handleResetConfigDir("claude")}
className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
title="恢复默认目录(需保存后生效)"
>
<Undo2 size={16} />
</button>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Codex
</label>
<div className="flex gap-2">
<input
type="text"
value={settings.codexConfigDir ?? resolvedCodexDir ?? ""}
onChange={(e) =>
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"
/>
<button
type="button"
onClick={() => handleBrowseConfigDir("codex")}
className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
title="浏览目录"
>
<FolderSearch size={16} />
</button>
<button
type="button"
onClick={() => handleResetConfigDir("codex")}
className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
title="恢复默认目录(需保存后生效)"
>
<Undo2 size={16} />
</button>
</div>
</div>
</div>
</div>
{/* 关于 */}
<div>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">

View File

@@ -122,6 +122,16 @@ export const tauriAPI = {
}
},
// 获取当前生效的配置目录
getConfigDir: async (app?: AppType): Promise<string> => {
try {
return await invoke("get_config_dir", { app_type: app, app });
} catch (error) {
console.error("获取配置目录失败:", error);
return "";
}
},
// 获取 Claude Code 配置状态
getClaudeConfigStatus: async (): Promise<ConfigStatus> => {
try {
@@ -189,10 +199,22 @@ export const tauriAPI = {
// (保留空位,取消迁移提示)
// 选择配置文件Tauri 暂不实现,保留接口兼容性)
selectConfigFile: async (): Promise<string | null> => {
console.warn("selectConfigFile 在 Tauri 版本中暂不支持");
return null;
// 选择配置目录
selectConfigDirectory: async (
defaultPath?: string,
): Promise<string | null> => {
try {
const sanitized =
defaultPath && defaultPath.trim() !== ""
? defaultPath
: undefined;
return await invoke<string | null>("pick_directory", {
defaultPath: sanitized,
});
} catch (error) {
console.error("选择配置目录失败:", error);
return null;
}
},
// 获取设置

View File

@@ -24,4 +24,8 @@ export interface AppConfig {
export interface Settings {
// 是否在系统托盘macOS 菜单栏)显示图标
showInTray: boolean;
// 覆盖 Claude Code 配置目录(可选)
claudeConfigDir?: string;
// 覆盖 Codex 配置目录(可选)
codexConfigDir?: string;
}

3
src/vite-env.d.ts vendored
View File

@@ -28,7 +28,8 @@ declare global {
getClaudeCodeConfigPath: () => Promise<string>;
getClaudeConfigStatus: () => Promise<ConfigStatus>;
getConfigStatus: (app?: AppType) => Promise<ConfigStatus>;
selectConfigFile: () => Promise<string | null>;
getConfigDir: (app?: AppType) => Promise<string>;
selectConfigDirectory: (defaultPath?: string) => Promise<string | null>;
openConfigFolder: (app?: AppType) => Promise<void>;
openExternal: (url: string) => Promise<void>;
updateTrayMenu: () => Promise<boolean>;