dev-manager-tauri/src-tauri/src/commands/settings.rs
lanrtop 0baa304fbe feat: WSL路径支持、项目发布到GitHub、三灯状态指示
- 新增发布到GitHub功能(scan/init/commit/push/register全流程)
- 新增WSL发行版检测与UNC路径自动转换,路径存储改为Windows可访问格式
- 修复libgit2访问WSL UNC路径的owner验证问题(启动时全局禁用)
- ProjectCard新增本地项目/本地仓库/远程仓库三灯状态指示
- ProjectModal重构为基本信息+部署信息双标签页,固定高度稳定切换
- 添加项目支持顶部快速选择文件夹并自动填充ID和名称
- 设置页WSL区块改为下拉检测选择发行版
- Dialog组件支持fixedHeight固定高度模式
2026-04-01 01:36:21 +09:00

193 lines
6.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::db::pool;
use rusqlite::params;
use serde_json::{Map, Value};
use std::collections::HashMap;
/// 全局 key不按空间隔离属于整个应用级别配置
const GLOBAL_KEYS: &[&str] = &["github_client_id", "github_client_secret"];
/// 获取 settings
/// - 有 space_id返回该空间的 space_settings + 全局 keygithub_client_id/secret
/// - 无 space_id返回全局 settings 表
#[tauri::command]
pub fn get_settings(space_id: Option<String>) -> Result<HashMap<String, String>, String> {
let conn = pool().get().map_err(|e| e.to_string())?;
if let Some(ref sid) = space_id {
// 1. 只读当前空间的 space_settings
let mut map: HashMap<String, String> = {
let mut stmt = conn
.prepare("SELECT key, value FROM space_settings WHERE space_id = ?1")
.map_err(|e| e.to_string())?;
let x = stmt
.query_map(params![sid], |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)))
.map_err(|e| e.to_string())?
.collect::<Result<HashMap<_, _>, _>>()
.map_err(|e| e.to_string())?; x
};
// 2. 补充全局 key应用级非空间相关
for key in GLOBAL_KEYS {
if let Ok(val) = conn.query_row(
"SELECT value FROM settings WHERE key = ?1",
params![key],
|row| row.get::<_, String>(0),
) {
map.insert(key.to_string(), val);
}
}
Ok(map)
} else {
// 无空间上下文:返回全局表(应用设置弹窗使用)
let mut stmt = conn.prepare("SELECT key, value FROM settings").map_err(|e| e.to_string())?;
let map = stmt
.query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)))
.map_err(|e| e.to_string())?
.collect::<Result<HashMap<_, _>, _>>()
.map_err(|e| e.to_string())?;
Ok(map)
}
}
/// 更新 settings全局 key 写入 settings 表;其余写入 space_settings 表
#[tauri::command]
pub fn update_settings(settings: HashMap<String, String>, space_id: Option<String>) -> Result<(), String> {
let conn = pool().get().map_err(|e| e.to_string())?;
for (k, v) in settings {
if GLOBAL_KEYS.contains(&k.as_str()) || space_id.is_none() {
conn.execute(
"INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
params![k, v],
)
.map_err(|e| e.to_string())?;
} else {
conn.execute(
"INSERT OR REPLACE INTO space_settings (space_id, key, value) VALUES (?1, ?2, ?3)",
params![space_id, k, v],
)
.map_err(|e| e.to_string())?;
}
}
Ok(())
}
/// 检测系统中已安装的常见编辑器,返回 [{name, cmd, path, found}]
#[tauri::command]
pub fn detect_editors() -> Result<Vec<Value>, String> {
// (显示名, 命令名, 常见安装路径列表)
let candidates: &[(&str, &str, &[&str])] = &[
("Cursor", "cursor", &[
r"%LOCALAPPDATA%\Programs\cursor\Cursor.exe",
r"%USERPROFILE%\AppData\Local\Programs\cursor\Cursor.exe",
]),
("VS Code", "code", &[
r"%LOCALAPPDATA%\Programs\Microsoft VS Code\Code.exe",
r"C:\Program Files\Microsoft VS Code\Code.exe",
]),
("VS Code Insiders", "code-insiders", &[
r"%LOCALAPPDATA%\Programs\Microsoft VS Code Insiders\Code - Insiders.exe",
]),
("Windsurf", "windsurf", &[
r"%LOCALAPPDATA%\Programs\windsurf\Windsurf.exe",
]),
];
let mut results = Vec::new();
for (name, cmd, fallback_paths) in candidates {
// 先用 where 命令查 PATH
let where_out = std::process::Command::new("cmd")
.args(["/c", &format!("where {cmd} 2>nul")])
.output()
.ok();
let path_from_where = where_out
.as_ref()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout.clone()).ok())
.and_then(|s| s.lines().next().map(|l| l.trim().to_string()))
.filter(|s| !s.is_empty());
// 如果 where 找不到,尝试常见安装路径(展开 %LOCALAPPDATA% 等)
let resolved_path = path_from_where.or_else(|| {
for &p in *fallback_paths {
let expanded = expand_env(p);
if std::path::Path::new(&expanded).exists() {
return Some(expanded);
}
}
None
});
let found = resolved_path.is_some();
let mut obj = Map::new();
obj.insert("name".into(), Value::String(name.to_string()));
obj.insert("cmd".into(), Value::String(cmd.to_string()));
obj.insert("path".into(), match &resolved_path {
Some(p) => Value::String(p.clone()),
None => Value::Null,
});
obj.insert("found".into(), Value::Bool(found));
results.push(Value::Object(obj));
}
Ok(results)
}
/// 搜索指定命令名对应的可执行路径
#[tauri::command]
pub fn search_editor_path(cmd: String) -> Result<Option<String>, String> {
// 先 where
let where_out = std::process::Command::new("cmd")
.args(["/c", &format!("where {cmd} 2>nul")])
.output()
.map_err(|e| e.to_string())?;
if where_out.status.success() {
if let Ok(s) = String::from_utf8(where_out.stdout) {
if let Some(line) = s.lines().next() {
let p = line.trim().to_string();
if !p.is_empty() {
return Ok(Some(p));
}
}
}
}
// 再搜 Program Files / LocalAppData
let search_dirs = [
std::env::var("LOCALAPPDATA").unwrap_or_default(),
std::env::var("PROGRAMFILES").unwrap_or_default(),
std::env::var("PROGRAMFILES(X86)").unwrap_or_default(),
];
let extensions = ["exe", "cmd"];
for dir in &search_dirs {
if dir.is_empty() { continue; }
for ext in &extensions {
let candidate = format!("{dir}\\{cmd}.{ext}");
if std::path::Path::new(&candidate).exists() {
return Ok(Some(candidate));
}
// 一层子目录
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let sub = entry.path().join(format!("{cmd}.{ext}"));
if sub.exists() {
return Ok(Some(sub.to_string_lossy().into_owned()));
}
}
}
}
}
Ok(None)
}
fn expand_env(s: &str) -> String {
let mut result = s.to_string();
for (key, val) in std::env::vars() {
result = result.replace(&format!("%{key}%"), &val);
}
result
}