vkit 0.1.4

Fast Rust dev CLI: manage git worktrees, Node ports, run scripts, install & sync VS Code / Cursor extensions.
//! `~/.config/vkit/config.toml` 配置加载。

use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use serde::Deserialize;

const DEFAULT_GLOBAL_ROOT_REL: &str = "worktrees";

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
    /// Global Placement 根目录(默认 `~/worktrees`)。
    pub global_root: PathBuf,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            global_root: default_global_root(),
        }
    }
}

#[derive(Debug, Default, Deserialize)]
struct FileConfig {
    #[serde(default)]
    worktree: WorktreeSection,
}

#[derive(Debug, Default, Deserialize)]
struct WorktreeSection {
    /// 覆盖 Global Placement 根;支持 `~` 前缀。
    global_root: Option<String>,
}

/// 从默认路径加载;文件不存在则用默认值。
pub fn load() -> Result<Config> {
    let path = config_path();
    load_from(&path)
}

/// 从指定路径加载(测试用)。
pub fn load_from(path: &Path) -> Result<Config> {
    if !path.exists() {
        return Ok(Config::default());
    }
    let raw = fs::read_to_string(path)
        .with_context(|| format!("读取配置失败:{}", path.display()))?;
    parse_toml(&raw)
}

fn parse_toml(raw: &str) -> Result<Config> {
    let file: FileConfig = toml::from_str(raw).context("解析 vkit 配置失败")?;
    let global_root = match file.worktree.global_root {
        Some(s) => expand_tilde(&s),
        None => default_global_root(),
    };
    Ok(Config { global_root })
}

pub fn config_path() -> PathBuf {
    dirs::config_dir()
        .unwrap_or_else(|| PathBuf::from(".").join(".config"))
        .join("vkit")
        .join("config.toml")
}

fn default_global_root() -> PathBuf {
    dirs::home_dir()
        .unwrap_or_else(|| PathBuf::from("/"))
        .join(DEFAULT_GLOBAL_ROOT_REL)
}

fn expand_tilde(s: &str) -> PathBuf {
    if let Some(rest) = s.strip_prefix("~/") {
        return dirs::home_dir()
            .unwrap_or_else(|| PathBuf::from("/"))
            .join(rest);
    }
    if s == "~" {
        return dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
    }
    PathBuf::from(s)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn missing_file_uses_default() {
        let cfg = load_from(Path::new("/nonexistent/vkit-config.toml")).unwrap();
        assert!(cfg.global_root.ends_with("worktrees"));
    }

    #[test]
    fn parses_global_root_override() {
        let cfg = parse_toml(
            r#"
[worktree]
global_root = "/tmp/my-worktrees"
"#,
        )
        .unwrap();
        assert_eq!(cfg.global_root, PathBuf::from("/tmp/my-worktrees"));
    }

    #[test]
    fn empty_toml_uses_default() {
        let cfg = parse_toml("").unwrap();
        assert!(cfg.global_root.ends_with("worktrees"));
    }
}