use inquire::Select; use std::fs::{self, File}; use std::io::{self, Write}; use std::path::{Path, PathBuf}; use tempfile::NamedTempFile; // 引入你的 mirror-core 类型 use mirror_core::{MirrorRegistry, TargetConfig}; #[derive(Debug)] pub enum UseError { ToolNotFound(String), NotVersioned(String), HomeDirNotFound(PathBuf), NoVersionsAvailable(PathBuf), UserCancelled, Io(io::Error), TargetValidationFailed(PathBuf), } /// 执行版本切换主流程 pub fn switch_version( registry: &MirrorRegistry, tool_name: &str, target_version: Option<&str>, ) -> Result<(), UseError> { // 1. 获取配置并断言必须是多版本工具 let spec = registry .targets .get(tool_name) .ok_or_else(|| UseError::ToolNotFound(tool_name.to_string()))?; let (home, version_file) = match &spec.target { TargetConfig::Versioned { home, version_file, .. } => { let v_file = version_file.as_deref().unwrap_or(".version"); (home, v_file) } TargetConfig::Static(_) => { return Err(UseError::NotVersioned(tool_name.to_string())); } }; if !home.exists() { return Err(UseError::HomeDirNotFound(home.clone())); } // 2. 扫描候选版本目录 let available_versions = scan_versions(home)?; if available_versions.is_empty() { return Err(UseError::NoVersionsAvailable(home.clone())); } // 3. 确定目标版本(命令行传入 OR 唤起 Inquire 单选菜单) let selected_ver = match target_version { Some(ver) => { // 如果用户传了参,校验是否在候选集中(防呆) if !available_versions.iter().any(|v| v == ver) { eprintln!( "警告: 目录 {:?} 下未发现版本 '{}',仍将强制写入。", home, ver ); } ver.to_string() } None => { // 尝试读取当前已激活的版本,作为 Inquire 的默认初始高亮项 let current_version_path = home.join(version_file); let current_ver = fs::read_to_string(¤t_version_path) .map(|s| s.trim().to_string()) .ok(); prompt_user_select(&available_versions, current_ver.as_deref())? } }; // 4. 原子化持久化写入版本文件 let dest_version_path = home.join(version_file); atomic_write(&dest_version_path, &selected_ver).map_err(UseError::Io)?; println!("✔ 已成功将 '{}' 切换至版本: {}", tool_name, selected_ver); Ok(()) } /// 扫描 home 目录下的非隐藏有效子文件夹作为版本列表 fn scan_versions(home: &Path) -> Result, UseError> { let mut versions = Vec::new(); let entries = fs::read_dir(home).map_err(UseError::Io)?; for entry in entries.filter_map(|e| e.ok()) { let path = entry.path(); if path.is_dir() { if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) { // 排除以点开头的隐藏目录(如 .git, .cache) if !dir_name.starts_with('.') { versions.push(dir_name.to_string()); } } } } // 自然排序版本列表 versions.sort(); Ok(versions) } /// 唤起 Inquire 进行交互单选 fn prompt_user_select( versions: &[String], default_version: Option<&str>, ) -> Result { let mut prompt = Select::new("请选择要激活的目标版本:", versions.to_vec()); // 如果能读取到当前版本,默认选中该项 if let Some(def) = default_version { if let Some(idx) = versions.iter().position(|v| v == def) { prompt.starting_cursor = idx; } } prompt.prompt().map_err(|_| UseError::UserCancelled) } /// 使用临时文件同目录重命名实现“原子写入” /// /// 杜绝中途掉电、杀死进程导致的空文件损坏 fn atomic_write(dest_path: &Path, content: &str) -> io::Result<()> { let parent_dir = dest_path .parent() .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Parent dir not found"))?; // 必须在目标文件的同级目录下创建临时文件,跨盘符/分区会导致原子重命名 (Rename) 失败 let mut temp_file = NamedTempFile::new_in(parent_dir)?; // 写入纯净内容,并强制落盘刷新 (Flush) temp_file.write_all(content.trim().as_bytes())?; temp_file.write_all(b"\n")?; // 末尾留标准换行符 temp_file.as_file().sync_all()?; // 核心操作:原子覆盖(在 Windows 上内部会调用 ReplaceFileW,在 Unix 则是 rename) temp_file.persist(dest_path).map_err(|e| e.error)?; Ok(()) }