- 新增发布到GitHub功能(scan/init/commit/push/register全流程) - 新增WSL发行版检测与UNC路径自动转换,路径存储改为Windows可访问格式 - 修复libgit2访问WSL UNC路径的owner验证问题(启动时全局禁用) - ProjectCard新增本地项目/本地仓库/远程仓库三灯状态指示 - ProjectModal重构为基本信息+部署信息双标签页,固定高度稳定切换 - 添加项目支持顶部快速选择文件夹并自动填充ID和名称 - 设置页WSL区块改为下拉检测选择发行版 - Dialog组件支持fixedHeight固定高度模式
193 lines
6.9 KiB
Rust
193 lines
6.9 KiB
Rust
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 + 全局 key(github_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
|
||
}
|