- 将 MirrorRegistry MirrorTarget 迁移到 model.rs - TargetConfig 独立实现 FromLua,解耦反序列化与业务校验 - mirror—shim 新增 command.rs 并将 to_command 移动到 command.rs - 其他优化
146 lines
4.7 KiB
Rust
146 lines
4.7 KiB
Rust
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<Vec<String>, 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<String, UseError> {
|
||
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(())
|
||
}
|