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) -> Result, 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 = { 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::, _>>() .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::, _>>() .map_err(|e| e.to_string())?; Ok(map) } } /// 更新 settings:全局 key 写入 settings 表;其余写入 space_settings 表 #[tauri::command] pub fn update_settings(settings: HashMap, space_id: Option) -> 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, 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, 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 }