feat: CLI 补全至 GUI 功能 100% 对等 — 新增 9 条命令

新增: edit, move-up, move-down, clean, enable, disable, import, export, backup
core: registry.rs +clean_paths, fs.rs +import_paths +export_paths
CLI 特有增强: move-up/move-down 支持 --steps N 一次移动多格

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 23:43:05 +08:00
parent c181fe15d4
commit a553a16a64
3 changed files with 328 additions and 18 deletions
+202 -17
View File
@@ -28,18 +28,67 @@ enum Command {
index: usize,
#[arg(short, long)] system: bool,
},
/// 编辑指定位置的路径
Edit {
index: usize,
new_path: String,
#[arg(short, long)] system: bool,
},
/// 上移路径(--steps 指定移动格数,默认 1)
MoveUp {
index: usize,
#[arg(long, default_value = "1")] steps: usize,
#[arg(short, long)] system: bool,
},
/// 下移路径(--steps 指定移动格数,默认 1)
MoveDown {
index: usize,
#[arg(long, default_value = "1")] steps: usize,
#[arg(short, long)] system: bool,
},
/// 清理无效和重复路径
Clean {
#[arg(short, long)] system: bool,
#[arg(short, long)] user: bool,
#[arg(long)] dry_run: bool,
#[arg(long)] json: bool,
},
/// 启用指定位置的路径
Enable {
index: usize,
#[arg(short, long)] system: bool,
#[arg(short, long)] user: bool,
},
/// 禁用指定位置的路径
Disable {
index: usize,
#[arg(short, long)] system: bool,
#[arg(short, long)] user: bool,
},
/// 从文件导入 PATHJSON/CSV/TXT
Import {
file: String,
#[arg(long, default_value = "both")] target: String,
},
/// 导出 PATH 为文件
Export {
#[arg(long, default_value = "json")] format: String,
#[arg(short, long)] output: Option<String>,
},
/// 创建注册表备份
Backup,
/// 检测可执行文件冲突
Conflicts { #[arg(long)] json: bool },
/// 列出 PATH 可执行文件
/// 列出 PATH 目录中的可执行文件
Scan {
#[arg(long)] query: Option<String>,
#[arg(long)] json: bool,
},
/// 检查管理员权限
CheckAdmin { #[arg(long)] json: bool },
/// 管理配置文件
#[command(subcommand)]
Profile(ProfileCmd),
/// 检查管理员权限
CheckAdmin { #[arg(long)] json: bool },
}
#[derive(Subcommand)]
@@ -65,6 +114,21 @@ fn pick_target(system: bool, _user: bool) -> &'static str {
if system { "system" } else { "user" }
}
type SaveFn = fn(Vec<String>) -> Result<(), String>;
fn load_and_save(system: bool, f: impl FnOnce(Vec<String>) -> Vec<String>) {
let target = pick_target(system, false);
let (list, save): (Vec<String>, SaveFn) = if target == "system" {
(core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e)),
core::registry::save_system_paths)
} else {
(core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e)),
core::registry::save_user_paths)
};
let new_list = f(list);
save(new_list).unwrap_or_else(|e| exit_err(&e));
}
// ── 命令实现 ──
fn cmd_list(system: bool, user: bool, json_out: bool) {
@@ -93,34 +157,146 @@ fn cmd_list(system: bool, user: bool, json_out: bool) {
fn cmd_add(path: String, system: bool, user: bool) {
let target = pick_target(system, user);
let (mut list, save_fn): (Vec<String>, _) = if target == "system" {
(core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e)),
core::registry::save_system_paths as fn(Vec<String>) -> Result<(), String>)
} else {
(core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e)),
core::registry::save_user_paths as fn(Vec<String>) -> Result<(), String>)
};
load_and_save(system || false, |mut list| {
list.push(path.clone());
save_fn(list).unwrap_or_else(|e| exit_err(&e));
list
});
let label = if target == "system" { "系统" } else { "用户" };
println!("已添加到{} PATH: {path}", label);
}
fn cmd_remove(index: usize, system: bool) {
let target = pick_target(system, false);
let (mut list, save_fn): (Vec<String>, _) = if target == "system" {
(core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e)),
core::registry::save_system_paths as fn(Vec<String>) -> Result<(), String>)
let mut list = if target == "system" {
core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e))
} else {
(core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e)),
core::registry::save_user_paths as fn(Vec<String>) -> Result<(), String>)
core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e))
};
if index >= list.len() { exit_err(&format!("索引 {index} 超出范围 (共 {} 条)", list.len())); }
let removed = list.remove(index);
save_fn(list).unwrap_or_else(|e| exit_err(&e));
let save: SaveFn = if target == "system" { core::registry::save_system_paths } else { core::registry::save_user_paths };
save(list).unwrap_or_else(|e| exit_err(&e));
println!("已删除: {removed}");
}
fn cmd_edit(index: usize, new_path: String, system: bool) {
let target = pick_target(system, false);
let mut list = if target == "system" {
core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e))
} else {
core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e))
};
if index >= list.len() { exit_err(&format!("索引 {index} 超出范围 (共 {} 条)", list.len())); }
let old = std::mem::replace(&mut list[index], new_path.clone());
let save: SaveFn = if target == "system" { core::registry::save_system_paths } else { core::registry::save_user_paths };
save(list).unwrap_or_else(|e| exit_err(&e));
println!("已编辑: {old}{new_path}");
}
fn cmd_move(index: usize, steps: usize, system: bool, up: bool) {
load_and_save(system || false, |mut list| {
if index >= list.len() { exit_err(&format!("索引 {index} 超出范围 (共 {} 条)", list.len())); }
let end = if up {
if steps > index { 0 } else { index - steps }
} else {
let max = list.len() - 1;
if index + steps > max { max } else { index + steps }
};
let removed = list.remove(index);
list.insert(end, removed);
list
});
let dir = if up { "上移" } else { "下移" };
println!("{dir} {steps} 格完成");
}
fn cmd_clean(system: bool, user: bool, dry_run: bool, json_out: bool) {
let target = pick_target(system, user);
let list = if target == "system" {
core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e))
} else {
core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e))
};
let (kept, removed) = core::registry::clean_paths(list);
if json_out {
println!("{}", json!({ "kept": kept, "removed": removed, "kept_count": kept.len(), "removed_count": removed.len() }).to_string());
} else if dry_run {
println!("═══ 将被移除({} 条)═══", removed.len());
for r in &removed { println!("{}", r); }
println!("═══ 将保留({} 条)═══", kept.len());
for k in &kept { println!("{}", k); }
} else {
let kept_count = kept.len();
let save: SaveFn = if target == "system" { core::registry::save_system_paths } else { core::registry::save_user_paths };
save(kept).unwrap_or_else(|e| exit_err(&e));
println!("清理完成:移除 {} 条,保留 {}", removed.len(), kept_count);
if !removed.is_empty() {
for r in &removed { println!(" 已移除: {}", r); }
}
}
}
fn cmd_toggle(index: usize, system: bool, user: bool, enable: bool) {
let target = pick_target(system, user);
let list = if target == "system" {
core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e))
} else {
core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e))
};
if index >= list.len() { exit_err(&format!("索引 {index} 超出范围 (共 {} 条)", list.len())); }
let path = &list[index];
let (mut sys_dis, mut usr_dis) = core::disabled::load_disabled_state().unwrap_or_else(|_| (vec![], vec![]));
let target_list: &mut Vec<String> = if target == "system" { &mut sys_dis } else { &mut usr_dis };
if enable {
target_list.retain(|p| p != path);
} else if !target_list.contains(path) {
target_list.push(path.clone());
}
core::disabled::save_disabled_state(sys_dis, usr_dis).unwrap_or_else(|e| exit_err(&e));
let action = if enable { "启用" } else { "禁用" };
println!("{action}: {path}");
}
fn cmd_import(file: String, target: String) {
let content = core::fs::read_text_file(&file).unwrap_or_else(|e| exit_err(&e));
let (sys, usr) = core::fs::import_paths(&file, &content).unwrap_or_else(|e| exit_err(&e));
match target.as_str() {
"system" => {
core::registry::save_system_paths(sys).unwrap_or_else(|e| exit_err(&e));
println!("已导入到系统 PATH");
}
"user" => {
core::registry::save_user_paths(usr).unwrap_or_else(|e| exit_err(&e));
println!("已导入到用户 PATH");
}
_ => {
core::registry::save_system_paths(sys).unwrap_or_else(|e| exit_err(&e));
core::registry::save_user_paths(usr).unwrap_or_else(|e| exit_err(&e));
println!("已导入到系统 + 用户 PATH");
}
}
}
fn cmd_export(format: String, output: Option<String>) {
let sys = core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e));
let usr = core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e));
let content = core::fs::export_paths(&sys, &usr, &format);
if let Some(path) = output {
std::fs::write(&path, &content).unwrap_or_else(|e| exit_err(&format!("无法写入文件: {e}")));
println!("已导出到: {path}");
} else {
println!("{content}");
}
}
fn cmd_backup() {
let path = core::backup::backup_registry(None).unwrap_or_else(|e| exit_err(&e));
println!("备份已保存: {path}");
}
fn cmd_conflicts(json_out: bool) {
let mut paths: Vec<String> = vec![];
if let Ok(sys) = core::registry::load_system_paths() { paths.extend(sys); }
@@ -216,6 +392,15 @@ fn main() {
Command::List { system, user, json } => cmd_list(system, user, json),
Command::Add { path, system, user } => cmd_add(path, system, user),
Command::Remove { index, system } => cmd_remove(index, system),
Command::Edit { index, new_path, system } => cmd_edit(index, new_path, system),
Command::MoveUp { index, steps, system } => cmd_move(index, steps, system, true),
Command::MoveDown { index, steps, system } => cmd_move(index, steps, system, false),
Command::Clean { system, user, dry_run, json } => cmd_clean(system, user, dry_run, json),
Command::Enable { index, system, user } => cmd_toggle(index, system, user, true),
Command::Disable { index, system, user } => cmd_toggle(index, system, user, false),
Command::Import { file, target } => cmd_import(file, target),
Command::Export { format, output } => cmd_export(format, output),
Command::Backup => cmd_backup(),
Command::Conflicts { json } => cmd_conflicts(json),
Command::Scan { query, json } => cmd_scan(query, json),
Command::CheckAdmin { json } => cmd_check_admin(json),
+102
View File
@@ -2,3 +2,105 @@
pub fn read_text_file(path: &str) -> Result<String, String> {
std::fs::read_to_string(path).map_err(|e| format!("无法读取文件: {}", e))
}
/// 导入路径文件(JSON / CSV / TXT),返回 (系统路径, 用户路径)
pub fn import_paths(path: &str, content: &str) -> Result<(Vec<String>, Vec<String>), String> {
let ext = std::path::Path::new(path)
.extension()
.map(|e| e.to_ascii_lowercase())
.unwrap_or_default();
let ext = ext.to_string_lossy();
match ext.as_ref() {
"json" => import_json(content),
"csv" => import_csv(content),
"txt" => import_txt(content),
_ => Err(format!("不支持的格式: .{}", ext)),
}
}
fn import_json(content: &str) -> Result<(Vec<String>, Vec<String>), String> {
#[derive(serde::Deserialize)]
struct ImportData {
#[serde(default)]
system: Vec<String>,
#[serde(default)]
user: Vec<String>,
}
let data: ImportData =
serde_json::from_str(content).map_err(|e| format!("JSON 解析失败: {}", e))?;
Ok((data.system, data.user))
}
fn import_csv(content: &str) -> Result<(Vec<String>, Vec<String>), String> {
let mut sys = Vec::new();
let mut usr = Vec::new();
for line in content.lines() {
let fields: Vec<&str> = line.split(',').collect();
if fields.len() >= 2 {
match fields[0].trim() {
"system" | "sys" => sys.push(fields[1].trim().to_string()),
"user" | "usr" => usr.push(fields[1].trim().to_string()),
_ => {}
}
}
}
if sys.is_empty() && usr.is_empty() {
return Err("CSV 文件中未找到有效路径".into());
}
Ok((sys, usr))
}
fn import_txt(content: &str) -> Result<(Vec<String>, Vec<String>), String> {
let paths: Vec<String> = content
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect();
if paths.is_empty() {
return Err("TXT 文件中未找到路径".into());
}
// TXT 格式全部导入为用户路径
Ok((vec![], paths))
}
/// 导出 PATH 为指定格式字符串
pub fn export_paths(sys: &[String], usr: &[String], format: &str) -> String {
match format {
"json" => {
let data = serde_json::json!({
"version": "5.0.0",
"timestamp": chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string(),
"system": sys,
"user": usr,
});
serde_json::to_string_pretty(&data).unwrap_or_default()
}
"csv" => {
let mut out = String::from("type,path\n");
for p in sys {
out.push_str(&format!("system,{}\n", p));
}
for p in usr {
out.push_str(&format!("user,{}\n", p));
}
out
}
_ => {
let mut out = String::new();
if !sys.is_empty() {
out.push_str(&format!("# 系统 PATH ({})\n", sys.len()));
for p in sys {
out.push_str(&format!("{}\n", p));
}
}
if !usr.is_empty() {
out.push_str(&format!("# 用户 PATH ({})\n", usr.len()));
for p in usr {
out.push_str(&format!("{}\n", p));
}
}
out
}
}
}
+23
View File
@@ -81,6 +81,29 @@ fn join_path(paths: &[String]) -> String {
.join(";")
}
/// 清理路径列表:移除不存在的目录 + 重复路径(保留首次出现)
/// 返回 (保留的路径, 被移除的路径)
pub fn clean_paths(paths: Vec<String>) -> (Vec<String>, Vec<String>) {
use std::collections::HashSet;
let mut seen: HashSet<String> = HashSet::new();
let mut kept = Vec::new();
let mut removed = Vec::new();
for p in paths {
let key = p.trim().to_lowercase();
if seen.contains(&key) {
removed.push(p);
continue;
}
seen.insert(key);
if !p.contains('%') && !std::path::Path::new(&p).is_dir() {
removed.push(p);
continue;
}
kept.push(p);
}
(kept, removed)
}
#[cfg(test)]
mod tests {
use super::*;